淺談 Checkbox Group 的雙向資料繫結
2021-01-14 00:00:27 軟體

前言

不曾想在忙碌的工作面前,寫一篇技術部落格也成了奢求。

Checkbox 作為表單中最常見的一類元素,使用方式分為單值和多值,其中單值的繫結很簡單,就是 truefalse,但是多值(Checkbox Group)的繫結就有一點複雜了。在實際工作中發現很多元件庫關於 checkbox-group 的雙向繫結一直很彆扭,或者說多多少少都有一些瑕疵。

開始本文之前,我們先假定有如下需求:

資料列表和輸出值都是物件陣列。能否只用一個雙向繫結就完成資料的輸入輸出,而不是在得到繫結的資料之後再使用陣列的 filtermap 這些方法去過濾和篩選。

著急的同學可以直接看最終的實現方案:Checkbox Group

現有元件庫的實現及缺陷

調研一下市面上的元件庫會發現,checkbox-group 並不是一個通用元件,很多元件庫並沒有這個元件,其中 Ant Design 的 checkbox-group 的設計方案算是比較完善的。簡單看一下 Ant Design 是如何設計這個元件的。

1、Ant Design React 版的實現:

<Checkbox.Group options={options} defaultValue={['Pear']} onChange={onChange} />

optionsdefaultValue 的型別定義如下:

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>

線上 DEMO

上面的程式碼沒有任何多餘的過濾篩選就完成了開篇提出的需求,對資料的操作全都隱藏在雙向繫結的內部。

總結

這篇文章拖沓了非常久,一方面是自己工作很忙,另一方面做開源專案佔據了大部分時間。

從最開始考慮 Checkbox Group 的重構方案到最終實現差不多用了半年多的時間,不過實際開發時間大概也就一週吧。相比之前借鑑 Ant Design 的方案來說,現在的方案更加靈活,有效減少了資料操作的程式碼,不過仍然有很大的優化和提升空間。

如果大家發現本文有不當之處,歡迎交流指正!