
前言
Checkbox 作為表單中最常見的一類元素,使用方式分為單值和多值,其中單值的綁定很簡單,就是 true 和 false,但是多值(Checkbox Group)的綁定就有一點復雜了。在實際工作中發(fā)現(xiàn)很多組件庫關(guān)于 checkbox-group 的雙向綁定一直很別扭,或者說多多少少都有一些瑕疵。
開始本文之前,我們先假定有如下需求:

數(shù)據(jù)列表和輸出值都是對象數(shù)組。能否只用一個雙向綁定就完成數(shù)據(jù)的輸入輸出,而不是在得到綁定的數(shù)據(jù)之后再使用數(shù)組的 filter、map 這些方法去過濾和篩選。
著急的同學可以直接看最終的實現(xiàn)方案:Checkbox Group
現(xiàn)有組件庫的實現(xiàn)及缺陷
調(diào)研一下市面上的組件庫會發(fā)現(xiàn),checkbox-group 并不是一個通用組件,很多組件庫并沒有這個組件,其中 Ant Design 的 checkbox-group 的設(shè)計方案算是比較完善的。簡單看一下 Ant Design 是如何設(shè)計這個組件的。
1、Ant Design React 版的實現(xiàn):
<Checkbox.Group options={options} defaultValue={['Pear']} onChange={onChange} />
options 和 defaultValue 的類型定義如下:
interface Option {
label: string;
value: string;
disabled?: boolean;
}
defaultValue: string[];
2、Ant Design Angular 版的實現(xiàn):
<nz-checkbox-group [(ngModel)]="options" (ngModelChange)="log(checkOptions)">
</nz-checkbox-group>
其中雙向綁定的數(shù)據(jù)類型如下:
options : Array<{ label: string; value: string; checked?: boolean; disabled?: boolean; }>
問題剖析
不管是 React 版還是 Angular 版,它們的 checkbox-group 都有一個共同點或者說缺陷,那就是 Option 的類型是固定的,假設(shè)需要綁定的數(shù)據(jù)如下:
cars = [
{ id: 1, name: 'Ford' },
{ id: 2, name: 'Chevrolet' },
{ id: 3, name: 'Dodge' },
];
那我們必須先將這個 cars 數(shù)組 map 成 Option 類型,然后才能綁定渲染。
另外,React 版和 Angular 版的輸出值類型也是固定的,其中 React 版輸出的是一個關(guān)于 value 的字符串數(shù)組,Angular 版是則是一個雙向綁定 checked 的原數(shù)組(個人覺得 Angular 版的綁定比 React 版的要靈活,至少從原數(shù)組取值更容易一點)。
還是以上面的 cars 數(shù)組為例,如果后端同事告訴我們想要一個完整的對象數(shù)組,比如下面這樣:
selectedCars = [
{ id: 2, name: 'Chevrolet' }
];
那我們就必須再遍歷一次 selectedCars 數(shù)組才能得到需要的數(shù)據(jù)。也就是說,對于上面展示的這種情況,我們必須要做一些額外的數(shù)據(jù)處理工作才能完成目標,但是這對于雙向綁定功能來說顯得有些繁瑣。
那到底應(yīng)該怎樣設(shè)計 checkbox-group 的雙向數(shù)據(jù)綁定才能更靈活的使用呢?
如何設(shè)計 Checkbox Group
在介紹如何設(shè)計之前,我們先嘗試能否從其它組件設(shè)計中找到靈感。
Checkbox 與 Select 的共性

Checkbox Group 和 Multiple Select 除了很細小的交互差異之外,幾乎看不出太大的不同。大多數(shù)情況下兩者可以相互替換,所以很多人總是困惑兩種組件到底應(yīng)該如何選擇。這里 有篇文章 專門對比了兩種組件的交互場景,甚至使用 A/B test 去分析用戶的偏好。
好像有點跑題了,言歸正傳,基于這種相似性,我們完全可以仿照 Select 的雙向綁定機制去設(shè)計 Checkbox Group。
Select 的雙向數(shù)據(jù)綁定
下面我們看一下 Material Select 和 Ng-Select 是如何設(shè)計雙向綁定的,數(shù)據(jù)就以上面的 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 在設(shè)計上稍微有一些差別。Material Select 完全基于模板渲染,Ng-Select 則是屬性配置優(yōu)先,兩者的數(shù)據(jù)回顯都是通過 compareWith。它們的雙向綁定都非常簡單,我們沒有寫任何多余的代碼就按規(guī)定的格式完成了數(shù)據(jù)的輸入輸出,這種設(shè)計思路同樣可以用在 Checkbox Group 上面。
Checkbox Group 的設(shè)計實現(xiàn)
看完上面關(guān)于 Select 的兩個例子,或許已經(jīng)不需要我再多說什么了,最終設(shè)計的 Checkbox Group 代碼如下:
<mtx-checkbox-group [items]="cars"
bindLabel="name"
[(ngModel)]="selectedCars"
[compareWith]="compareWith">
</mtx-checkbox-group>
上面的代碼沒有任何多余的過濾篩選就完成了開篇提出的需求,對數(shù)據(jù)的操作全都隱藏在雙向綁定的內(nèi)部。
如果后端同事希望 selectedCars 是一個 id 數(shù)組,比如 selectedCars=[2],那么只需要設(shè)置一下 bindValue 就可以了。
<mtx-checkbox-group [items]="cars"
bindLabel="name"
bindValue="id"
[(ngModel)]="selectedCars">
</mtx-checkbox-group>
非對象數(shù)據(jù)的回顯就不用 compareWith 了。
總結(jié)
這篇文章拖沓了非常久,一方面是自己工作很忙,另一方面做開源項目占據(jù)了大部分時間。
從最開始考慮 Checkbox Group 的重構(gòu)方案到最終實現(xiàn)差不多用了半年多的時間,不過實際開發(fā)時間大概也就一周吧。相比之前借鑒 Ant Design 的方案來說,現(xiàn)在的方案更加靈活,有效減少了數(shù)據(jù)操作的代碼,不過仍然有很大的優(yōu)化和提升空間。
如果大家發(fā)現(xiàn)本文有不當之處,歡迎交流指正!