淺談 Checkbox Group 的雙向數據綁定
前言
不曾想在忙碌的工作面前,寫一篇技術博客也成了奢求。
Checkbox 作為表單中最常見的一類元素,使用方式分為單值和多值,其中單值的綁定很簡單,就是 true
和 false
,但是多值(Checkbox Group)的綁定就有一點複雜了。在實際工作中發現很多組件庫關於 checkbox-group
的雙向綁定一直很彆扭,或者說多多少少都有一些瑕疵。
開始本文之前,我們先假定有如下需求:
數據列表和輸出值都是對象數組。能否只用一個雙向綁定就完成數據的輸入輸出,而不是在得到綁定的數據之後再使用數組的 filter
、map
這些方法去過濾和篩選。
着急的同學可以直接看最終的實現方案:Checkbox Group
現有組件庫的實現及缺陷
調研一下市面上的組件庫會發現,checkbox-group
並不是一個通用組件,很多組件庫並沒有這個組件,其中 Ant Design 的 checkbox-group
的設計方案算是比較完善的。簡單看一下 Ant Design 是如何設計這個組件的。
1、Ant Design React 版的實現:
<Checkbox.Group options={options} defaultValue={['Pear']} onChange={onChange} />
options
和 defaultValue
的類型定義如下:
interface Option {
label: string;
value: string;
disabled?: boolean;
}
defaultValue: string[];
2、Ant Design Angular 版的實現:
<nz-checkbox-group [(ngModel)]="options" (ngModelChange)="log(checkOptions)">
</nz-checkbox-group>
其中雙向綁定的數據類型如下:
options : Array<{ label: string; value: string; checked?: boolean; disabled?: boolean; }>
問題剖析
不管是 React 版還是 Angular 版,它們的 checkbox-group
都有一個共同點或者說缺陷,那就是 Option
的類型是固定的,假設需要綁定的數據如下:
cars = [
{ id: 1, name: 'Ford' },
{ id: 2, name: 'Chevrolet' },
{ id: 3, name: 'Dodge' },
];
那我們必須先將這個 cars
數組 map 成 Option
類型,然後才能綁定渲染。
另外,React 版和 Angular 版的輸出值類型也是固定的,其中 React 版輸出的是一個關於 value
的字符串數組,Angular 版是則是一個雙向綁定 checked
的原數組(個人覺得 Angular 版的綁定比 React 版的要靈活,至少從原數組取值更容易一點)。
還是以上面的 cars 數組為例,如果後端同事告訴我們想要一個完整的對象數組,比如下面這樣:
selectedCars = [
{ id: 2, name: 'Chevrolet' }
];
那我們就必須再遍歷一次 selectedCars
數組才能得到需要的數據。也就是說,對於上面展示的這種情況,我們必須要做一些額外的數據處理工作才能完成目標,但是這對於雙向綁定功能來說顯得有些繁瑣。
那到底應該怎樣設計 checkbox-group
的雙向數據綁定才能更靈活的使用呢?
如何設計 Checkbox Group
在介紹如何設計之前,我們先嘗試能否從其它組件設計中找到靈感。
Checkbox 與 Select 的共性
Checkbox Group 和 Multiple Select 除了很細小的交互差異之外,幾乎看不出太大的不同。大多數情況下兩者可以相互替換,所以很多人總是困惑兩種組件到底應該如何選擇。這裡 有篇文章 專門對比了兩種組件的交互場景,甚至使用 A/B test 去分析用戶的偏好。
好像有點跑題了,言歸正傳,基於這種相似性,我們完全可以仿照 Select 的雙向綁定機制去設計 Checkbox Group。
Select 的雙向數據綁定
下面我們看一下 Material Select 和 Ng-Select 是如何設計雙向綁定的,數據就以上面的 cars 為例。
cars = [
{ id: 1, name: 'Ford' },
{ id: 2, name: 'Chevrolet' },
{ id: 3, name: 'Dodge' },
];
selectedCars = [
{ id: 2, name: 'Chevrolet' }
];
1、Material Select
<mat-select multiple [(ngModel)]="selectedCars" [compareWith]="compareWith">
<mat-option *ngFor="let car of cars" [value]="car">{{car.name}}</mat-option>
</mat-select>
2、Ng-Select
<ng-select [multiple]="true" [items]="cars" bindLabel="name"
[(ngModel)]="selectedCars" [compareWith]="compareWith">
</ng-select>
Material Select 和 Ng-Select 在設計上稍微有一些差別。Material Select 完全基於模板渲染,Ng-Select 則是屬性配置優先,兩者的數據回顯都是通過 compareWith
。它們的雙向綁定都非常簡單,我們沒有寫任何多餘的代碼就按規定的格式完成了數據的輸入輸出,這種設計思路同樣可以用在 Checkbox Group 上面。
Checkbox Group 的設計實現
看完上面關於 Select 的兩個例子,或許已經不需要我再多說什麼了,最終我設計的 Checkbox Group 代碼如下:
<mtx-checkbox-group [items]="cars"
bindLabel="name"
[(ngModel)]="selectedCars"
[compareWith]="compareWith">
</mtx-checkbox-group>
上面的代碼沒有任何多餘的過濾篩選就完成了開篇提出的需求,對數據的操作全都隱藏在雙向綁定的內部。
總結
這篇文章拖沓了非常久,一方面是自己工作很忙,另一方面做開源項目佔據了大部分時間。
從最開始考慮 Checkbox Group 的重構方案到最終實現差不多用了半年多的時間,不過實際開發時間大概也就一周吧。相比之前借鑒 Ant Design 的方案來說,現在的方案更加靈活,有效減少了數據操作的代碼,不過仍然有很大的優化和提升空間。
如果大家發現本文有不當之處,歡迎交流指正!