淺談 Checkbox Group 的雙向數據綁定

前言

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

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 的方案來說,現在的方案更加靈活,有效減少了數據操作的代碼,不過仍然有很大的優化和提升空間。

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

Tags: