實戰(zhàn) 2:組合多選框組件——CheckboxGroup & Checkbox
在第 5 節(jié)省咨,我們完成了具有數(shù)據(jù)校驗功能的組件 Form,本小節(jié)繼續(xù)開發(fā)一個新的組件——組合多選框 Checkbox。它作為基礎(chǔ)組件执俩,也能集成在 Form 內(nèi)并應(yīng)用其驗證規(guī)則蚂维。
Checkbox 組件概覽
多選框組件也是由兩個組件組成:CheckboxGroup 和 Checkbox。單獨(dú)使用時毙籽,只需要一個 Checkbox洞斯,組合使用時,兩者都要用到坑赡。效果如下圖所示:
單獨(dú)使用烙如,常見的場景有注冊時勾選以同意注冊條款,它只有一個獨(dú)立的 Checkbox 組件毅否,并且綁定一個布爾值亚铁,示例如下:
<template>
<i-checkbox v-model="single">單獨(dú)選項</i-checkbox>
</template>
<script>
export default {
data () {
return {
single: false
}
}
}
</script>
而組合使用的場景就很多了,填寫表單時會經(jīng)常用到螟加,它的結(jié)構(gòu)如下:
<template>
<i-checkbox-group v-model="multiple">
<i-checkbox label="option1">選項 1</i-checkbox>
<i-checkbox label="option2">選項 2</i-checkbox>
<i-checkbox label="option3">選項 3</i-checkbox>
<i-checkbox label="option4">選項 4</i-checkbox>
</i-checkbox-group>
</template>
<script>
export default {
data () {
return {
multiple: ['option1', 'option3']
}
}
}
</script>
v-model
用在了 CheckboxGroup 上徘溢,綁定的值為一個數(shù)組,數(shù)組的值就是內(nèi)部 Checkbox 綁定的 label捆探。
用法看起來比 Form 要簡單多然爆,不過也有兩個個技術(shù)難點:
- Checkbox 要同時支持單獨(dú)使用和組合使用的場景;
- CheckboxGroup 和 Checkbox 內(nèi)可能嵌套其它的布局組件黍图。
對于第一點曾雕,要在 Checkbox 初始化時判斷是否父級有 CheckboxGroup,如果有就是組合使用的助被,否則就是單獨(dú)使用剖张。而第二點,正好可以用上一節(jié)的通信方法揩环,很容易就能解決搔弄。
兩個組件并行開發(fā),會容易理不清邏輯丰滑,不妨我們先開發(fā)獨(dú)立的 Checkbox 組件肯污。
單獨(dú)使用的 Checkbox
設(shè)計一個組件時,還是要從它的 3 個 API 入手:prop吨枉、event蹦渣、slot。
因為要在 Checkbox 組件上直接使用 v-model
來雙向綁定數(shù)據(jù)貌亭,那必不可少的一個 prop 就是 value
柬唯,還有 event input
,因為 v-model 本質(zhì)上是一個語法糖(如果你還不清楚這種用法圃庭,可以閱讀最后的擴(kuò)展閱讀 1)锄奢。
理論上失晴,我們只需要給 value
設(shè)置為布爾值即可,也就是 true / false拘央,不過為了擴(kuò)展性涂屁,我們再定義兩個 props:trueValue
和 falseValue
,它們允許用戶指定 value
用什么值來判斷是否選中灰伟。因為實際開發(fā)中拆又,數(shù)據(jù)庫中并不直接保存 true / false,而是 1 / 0 或其它字符串栏账,如果強(qiáng)制使用 Boolean帖族,使用者就要再額外轉(zhuǎn)換一次,這樣的 API 設(shè)計不太友好挡爵。
除此之外竖般,還需要一個 disabled
屬性來表示是否禁用。
自定義事件 events 上文已經(jīng)說了一個 input
茶鹃,用于實現(xiàn) v-model 語法糖涣雕;另一個就是 on-change
,當(dāng)選中 / 取消選中時觸發(fā)闭翩,用于通知父級狀態(tài)發(fā)生了變化挣郭。
slot 使用默認(rèn)的就好,顯示輔助文本男杈。
理清楚了 API丈屹,先來寫一個基礎(chǔ)的 v-model
功能调俘,這在大部分組件中都類似伶棒。
在 src/components
下新建目錄 checkbox
,并新建兩個文件 checkbox.vue
和 checkbox-group.vue
彩库。我們先來看 Checkbox:
<!-- checkbox.vue -->
<template>
<label>
<span>
<input
type="checkbox"
:disabled="disabled"
:checked="currentValue"
@change="change">
</span>
<slot></slot>
</label>
</template>
<script>
export default {
name: 'iCheckbox',
props: {
disabled: {
type: Boolean,
default: false
},
value: {
type: [String, Number, Boolean],
default: false
},
trueValue: {
type: [String, Number, Boolean],
default: true
},
falseValue: {
type: [String, Number, Boolean],
default: false
}
},
data () {
return {
currentValue: this.value
};
},
methods: {
change (event) {
if (this.disabled) {
return false;
}
const checked = event.target.checked;
this.currentValue = checked;
const value = checked ? this.trueValue : this.falseValue;
this.$emit('input', value);
this.$emit('on-change', value);
}
}
}
</script>
因為 value
被定義為 prop肤无,它只能由父級修改,本身是不能修改的骇钦,在 <input>
觸發(fā) change 事件宛渐,也就是點擊選擇時,不能由 Checkbox 來修改這個 value眯搭,所以我們在 data 里定義了一個 currentValue
窥翩,并把它綁定在 <input :checked="currentValue">
,這樣就可以在 Checkbox 內(nèi)修改 currentValue
鳞仙。這是自定義組件使用 v-model
的“慣用伎倆”寇蚊。
代碼看起來都很簡單,但有三個細(xì)節(jié)需要額外說明:
- 選中的控件棍好,直接使用了
<input type="checkbox">
仗岸,而沒有用 div + css 來自己實現(xiàn)選擇的邏輯和樣式允耿,這樣的好處是,使用 input 元素扒怖,你的自定義組件仍然為 html 內(nèi)置的基礎(chǔ)組件较锡,可以使用瀏覽器默認(rèn)的行為和快捷鍵,也就是說盗痒,瀏覽器知道這是一個選擇框蚂蕴,而換成 div + css,瀏覽器可不知道這是個什么鬼积糯。如果你覺得原生的 input 丑掂墓,沒關(guān)系,是可以用 css 美化的看成,不過這不是本小冊的重點君编,在此就不介紹了。 -
<input>
川慌、<slot>
都是包裹在一個<label>
元素內(nèi)的吃嘿,這樣做的好處是,當(dāng)點擊<slot>
里的文字時梦重,<input>
選框也會被觸發(fā)兑燥,否則只有點擊那個小框才會觸發(fā),那樣不太容易選中琴拧,影響用戶體驗降瞳。 -
currentValue
仍然是布爾值(true / false),因為它是組件 Checkbox 自己使用的蚓胸,對于使用者無需關(guān)心挣饥,而 value 可以是 String、Number 或 Boolean沛膳,這取決于trueValue
和falseValue
的定義扔枫。
現(xiàn)在實現(xiàn)的 v-model
,只是由內(nèi)而外的,也就是說,通過點擊 <input>
選擇浦夷,會通知到使用者徙瓶,而使用者手動修改了 prop value
,Checkbox 是沒有做響應(yīng)的,那繼續(xù)補(bǔ)充代碼:
<!-- checkbox.vue,部分代碼省略 -->
<script>
export default {
watch: {
value (val) {
if (val === this.trueValue || val === this.falseValue) {
this.updateModel();
} else {
throw 'Value should be trueValue or falseValue.';
}
}
},
methods: {
updateModel () {
this.currentValue = this.value === this.trueValue;
}
}
}
</script>
我們對 prop value
使用 watch 進(jìn)行了監(jiān)聽,當(dāng)父級修改它時糠排,會調(diào)用 updateModel
方法,同步修改內(nèi)部的 currentValue
泊交。不過乳讥,不是所有的值父級都能修改的柱查,所以用 if 條件判斷了父級修改的值是否符合 trueValue / falseValue 所設(shè)置的,否則會拋錯云石。
Checkbox 也是一個基礎(chǔ)的表單類組件唉工,它完全可以集成到 Form 里,所以汹忠,我們使用 Emitter 在 change 事件觸發(fā)時淋硝,向 Form 派發(fā)一個事件,這樣你就可以用第 5 節(jié)的 Form 組件來做數(shù)據(jù)校驗了:
<!-- checkbox.vue宽菜,部分代碼省略 -->
<script>
import Emitter from '../../mixins/emitter.js';
export default {
mixins: [ Emitter ],
methods: {
change (event) {
// ...
this.$emit('input', value);
this.$emit('on-change', value);
this.dispatch('iFormItem', 'on-form-change', value);
}
},
}
</script>
至此谣膳,Checkbox 已經(jīng)可以單獨(dú)使用了,并支持 Form 的數(shù)據(jù)校驗铅乡。下面來看組合使用继谚。
組合使用的 CheckboxGroup
友情提示:請先閱讀 Vue.js 文檔的 https://cn.vuejs.org/v2/guide/forms.html#復(fù)選框 內(nèi)容。
CheckboxGroup 的 API 很簡單:
- props:
value
阵幸,與 Checkbox 的類似花履,用于 v-model 雙向綁定數(shù)據(jù),格式為數(shù)組挚赊; - events:
on-change
诡壁,同 Checkbox; - slots:默認(rèn)荠割,用于放置 Checkbox妹卿。
如果寫了 CheckboxGroup,那就代表你要組合使用多選框蔑鹦,而非單獨(dú)使用夺克,兩種模式,只能用其一举反,而判斷的依據(jù)懊直,就是是否用了 CheckboxGroup 組件扒吁。所以在 Checkbox 組件內(nèi)火鼻,我們用上一節(jié)的 findComponentUpward
方法判斷父組件是否有 CheckboxGroup
:
<!-- checkbox.vue,部分代碼省略 -->
<template>
<label>
<span>
<input
v-if="group"
type="checkbox"
:disabled="disabled"
:value="label"
v-model="model"
@change="change">
<input
v-else
type="checkbox"
:disabled="disabled"
:checked="currentValue"
@change="change">
</span>
<slot></slot>
</label>
</template>
<script>
import { findComponentUpward } from '../../utils/assist.js';
export default {
name: 'iCheckbox',
props: {
label: {
type: [String, Number, Boolean]
}
},
data () {
return {
model: [],
group: false,
parent: null
};
},
mounted () {
this.parent = findComponentUpward(this, 'iCheckboxGroup');
if (this.parent) {
this.group = true;
}
if (this.group) {
this.parent.updateModel(true);
} else {
this.updateModel();
}
},
}
</script>
在 mounted 時雕崩,通過 findComponentUpward 方法魁索,來判斷父級是否有 CheckboxGroup 組件,如果有盼铁,就將 group
置為 true粗蔚,并觸發(fā) CheckboxGroup 的 updateModel
方法,下文會介紹它的作用饶火。
在 template 里鹏控,我們又寫了一個 <input>
來區(qū)分是否是 group 模式致扯。Checkbox 的 data 里新增加的 model
數(shù)據(jù),其實就是父級 CheckboxGroup 的 value
当辐,會在下文的 updateModel
方法里給 Checkbox 賦值抖僵。
Checkbox 新增的 prop: label
只會在組合使用時有效,結(jié)合 model
來使用缘揪,用法已在 Vue.js 文檔中介紹了 https://cn.vuejs.org/v2/guide/forms.html#復(fù)選框耍群。
在組合模式下,Checkbox 選中找筝,就不用對 Form 派發(fā)事件了蹈垢,應(yīng)該在 CheckboxGroup 中派發(fā),所以對 Checkbox 做最后的修改:
<!-- checkbox.vue袖裕,部分代碼省略 -->
<script>
export default {
methods: {
change (event) {
if (this.disabled) {
return false;
}
const checked = event.target.checked;
this.currentValue = checked;
const value = checked ? this.trueValue : this.falseValue;
this.$emit('input', value);
if (this.group) {
this.parent.change(this.model);
} else {
this.$emit('on-change', value);
this.dispatch('iFormItem', 'on-form-change', value);
}
},
updateModel () {
this.currentValue = this.value === this.trueValue;
},
},
}
</script>
剩余的工作曹抬,就是完成 checkbox-gourp.vue 文件:
<!-- checkbox-group.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<script>
import { findComponentsDownward } from '../../utils/assist.js';
import Emitter from '../../mixins/emitter.js';
export default {
name: 'iCheckboxGroup',
mixins: [ Emitter ],
props: {
value: {
type: Array,
default () {
return [];
}
}
},
data () {
return {
currentValue: this.value,
childrens: []
};
},
methods: {
updateModel (update) {
this.childrens = findComponentsDownward(this, 'iCheckbox');
if (this.childrens) {
const { value } = this;
this.childrens.forEach(child => {
child.model = value;
if (update) {
child.currentValue = value.indexOf(child.label) >= 0;
child.group = true;
}
});
}
},
change (data) {
this.currentValue = data;
this.$emit('input', data);
this.$emit('on-change', data);
this.dispatch('iFormItem', 'on-form-change', data);
}
},
mounted () {
this.updateModel(true);
},
watch: {
value () {
this.updateModel(true);
}
}
};
</script>
代碼很容易理解,需要介紹的就是 updateModel
方法急鳄°宓唬可以看到,一共有 3 個地方調(diào)用了 updateModel
攒岛,其中兩個是 CheckboxGroup 的 mounted 初始化和 watch 監(jiān)聽的 value 變化時調(diào)用赖临;另一個是在 Checkbox 里的 mounted 初始化時調(diào)用。這個方法的作用就是在 CheckboxGroup 里通過 findComponentsDownward
方法找到所有的 Checkbox灾锯,然后把 CheckboxGroup 的 value
兢榨,賦值給 Checkbox 的 model
,并根據(jù) Checkbox 的 label
顺饮,設(shè)置一次當(dāng)前 Checkbox 的選中狀態(tài)吵聪。這樣無論是由內(nèi)而外選擇,或由外向內(nèi)修改數(shù)據(jù)兼雄,都是雙向綁定的吟逝,而且支持動態(tài)增加 Checkbox 的數(shù)量。
以上就是組合多選組件——CheckboxGroup & Checkbox 的全部內(nèi)容赦肋,不知道你是否 get 到了呢块攒!
留兩個小作業(yè):
- 將 CheckboxGroup 和 Checkbox 組件集成在 Form 里完成一個數(shù)據(jù)校驗的示例;
- 參考本節(jié)的代碼佃乘,實現(xiàn)一個單選組件 Radio 和 RadioGroup囱井。
結(jié)語
你看到的簡單組件,其實都不簡單趣避。
擴(kuò)展閱讀
注:本節(jié)部分代碼參考 iView庞呕。