複選框的邏輯比單選框更爲複雜,代碼量也更多,這裏只介紹其與單選框不一樣的邏輯,其他的分析參考單選框css
先上代碼,官網代碼 點此<template>
<label
class="el-checkbox"
:class="[ border && checkboxSize ? 'el-checkbox--' + checkboxSize : '', { 'is-disabled': isDisabled }, { 'is-bordered': border }, { 'is-checked': isChecked } ]"
role="checkbox"
:aria-checked="indeterminate ? 'mixed': isChecked"
:aria-disabled="isDisabled"
:id="id"
>
<span class="el-checkbox__input"
:class="{ 'is-disabled': isDisabled, 'is-checked': isChecked, 'is-indeterminate': indeterminate, 'is-focus': focus }"
aria-checked="mixed"
>
<span class="el-checkbox__inner"></span>
<input
v-if="trueLabel || falseLabel"
class="el-checkbox__original"
type="checkbox"
aria-hidden="true"
:name="name"
:disabled="isDisabled"
:true-value="trueLabel"
:false-value="falseLabel"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
<input
v-else
class="el-checkbox__original"
type="checkbox"
aria-hidden="true"
:disabled="isDisabled"
:value="label"
:name="name"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
</span>
<span class="el-checkbox__label" v-if="$slots.default || label">
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElCheckbox',
mixins: [Emitter],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
componentName: 'ElCheckbox',
data() {
return {
selfModel: false,
focus: false,
isLimitExceeded: false
};
},
computed: {
model: {
get() {
return this.isGroup
? this.store : this.value !== undefined
? this.value : this.selfModel;
},
set(val) {
if (this.isGroup) {
this.isLimitExceeded = false;
(this._checkboxGroup.min !== undefined &&
val.length < this._checkboxGroup.min &&
(this.isLimitExceeded = true));
(this._checkboxGroup.max !== undefined &&
val.length > this._checkboxGroup.max &&
(this.isLimitExceeded = true));
this.isLimitExceeded === false &&
this.dispatch('ElCheckboxGroup', 'input', [val]);
} else {
this.$emit('input', val);
this.selfModel = val;
}
}
},
isChecked() {
if ({}.toString.call(this.model) === '[object Boolean]') {
return this.model;
} else if (Array.isArray(this.model)) {
return this.model.indexOf(this.label) > -1;
} else if (this.model !== null && this.model !== undefined) {
return this.model === this.trueLabel;
}
},
isGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElCheckboxGroup') {
parent = parent.$parent;
} else {
this._checkboxGroup = parent;
return true;
}
}
return false;
},
store() {
return this._checkboxGroup ? this._checkboxGroup.value : this.value;
},
isDisabled() {
return this.isGroup
? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled
: this.disabled || (this.elForm || {}).disabled;
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
checkboxSize() {
const temCheckboxSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
return this.isGroup
? this._checkboxGroup.checkboxGroupSize || temCheckboxSize
: temCheckboxSize;
}
},
props: {
value: {},
label: {},
indeterminate: Boolean,
disabled: Boolean,
checked: Boolean,
name: String,
trueLabel: [String, Number],
falseLabel: [String, Number],
id: String, /* 當indeterminate爲真時,爲controls提供相關連的checkbox的id,代表元素間的控制關係*/
controls: String, /* 當indeterminate爲真時,爲controls提供相關連的checkbox的id,代表元素間的控制關係*/
border: Boolean,
size: String
},
methods: {
addToStore() {
if (
Array.isArray(this.model) &&
this.model.indexOf(this.label) === -1
) {
this.model.push(this.label);
} else {
this.model = this.trueLabel || true;
}
},
handleChange(ev) {
if (this.isLimitExceeded) return;
let value;
if (ev.target.checked) {
value = this.trueLabel === undefined ? true : this.trueLabel;
} else {
value = this.falseLabel === undefined ? false : this.falseLabel;
}
this.$emit('change', value, ev);
this.$nextTick(() => {
if (this.isGroup) {
this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
}
});
}
},
created() {
this.checked && this.addToStore();
},
mounted() { // 爲indeterminate元素 添加aria-controls 屬性
if (this.indeterminate) {
this.$el.setAttribute('aria-controls', this.controls);
}
},
watch: {
value(value) {
this.dispatch('ElFormItem', 'el.form.change', value);
}
}
};
</script>
複製代碼
是否是看的一臉懵逼,最好是打開官網,對照checkbox用法一項項來分析其原理html
同單選框相似,複選框的示意圖以下,無非就是左右2部分組成,外層套一個label,並隱藏原生的<input type='checkbox'>
vue
<label ...>
<span class='el-checkbox__input'>
<span class='el-checkbox__inner'></span>
<input type='checkbox' .../>
</span>
<span class='el-checkbox__label'>
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
複製代碼
這裏具體參考上一篇單選按鈕的文章,重點說下上圖的藍色方框內的勾是怎麼實現的,也就是選中狀態,開始我覺得是一個相似Icon的東西,然而並非,查看css代碼以下git
&::after {
box-sizing: content-box;
content: "";
border: 1px solid $--checkbox-checked-icon-color;
border-left: 0;
border-top: 0;
height: 7px;
left: 4px;
position: absolute;
top: 1px;
transform: rotate(45deg) scaleY(0);
width: 3px;
transition: transform .15s ease-in .05s;
transform-origin: center;
}
複製代碼
很明顯,這是el-checkbox__inner
類的after僞元素,裏面是一個只有右下border的長方形通過旋轉45度後的圖形,也就是一個勾的形狀,因此這個勾只是純粹的css實現而已,好處是簡化了html結構,而且還用了transition
來添加點擊後勾變大的動畫效果,這裏是經過過渡transform
的scaleY
的值來實現,未選中時scaleY
爲0,選中時爲1,就實現了勾放大的效果
所以要善用僞元素,會簡化不少沒必要要的代碼github
首先來看Vue中的複選框是怎麼實現的,瞭解這個有助於理解Element的實現,官網介紹以下element-ui
上圖中單個複選框使用bool值,多個複選框使用數組便可,這裏其實Vue在幕後作了許多工做,找到Vue中相關源碼以下function genCheckboxModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
) {
const number = modifiers && modifiers.number
const valueBinding = getBindingAttr(el, 'value') || 'null'
const trueValueBinding = getBindingAttr(el, 'true-value') || 'true'
const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
addProp(el, 'checked',
`Array.isArray(${value})` +
`?_i(${value},${valueBinding})>-1` + (
trueValueBinding === 'true'
? `:(${value})`
: `:_q(${value},${trueValueBinding})`
)
)
addHandler(el, 'change',
`var $$a=${value},` +
'$$el=$event.target,' +
`$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
'if(Array.isArray($$a)){' +
`var $$v=${number ? '_n(' + valueBinding + ')' : valueBinding},` +
'$$i=_i($$a,$$v);' +
`if($$el.checked){$$i<0&&(${genAssignmentCode(value, '$$a.concat([$$v])')})}` +
`else{$$i>-1&&(${genAssignmentCode(value, '$$a.slice(0,$$i).concat($$a.slice($$i+1))')})}` +
`}else{${genAssignmentCode(value, '$$c')}}`,
null, true
)
}
複製代碼
這就是處理checkbox的v-model的代碼,咱們只須要知道這段代碼大概在作啥就行,細節不用太清楚,代碼中首先獲取v:bind
綁定的value的值,也就是下面示例代碼中的Jack
(注意代碼中其實處理了不是v:bind的狀況,具體看源碼),而後genCheckboxModel
這個函數的參數中的value
就是v-model的值,也就是下面的checkedNames
,接下來addProp
方法的邏輯:若是checkedNames
是數組,則經過indexOf查詢Jack
是否在checkedNames
中,若是在則給input添加checked屬性表明被選中,其次若是checkedNames
不是數組,則直接比較2者是否相等來決定是否給input添加checked屬性
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
由上面的分析可見,複選框的選中的checked屬性是Vue幕後添加的,經過值的比較來決定是否添加該屬性
而後來看addHandler
方法,這個方法給複選框添加了change事件,原生複選框點擊後它的checked屬性會改變(true或false),可是Vue中的checkedNames
的值會跟着變化,這裏就是addHandler
所作的工做了,該方法裏面整體邏輯就是首先判斷checkedNames
是不是數組,若是是且該複選框被選中,則將該複選框的值加入checkedNames
數組中,若是該複選框沒有被選中,則從數組中去掉它(注意這裏沒有用splice,而是2個slice後concat合併成一個數組,splice會改變原始數組,這樣就不會)數組
因此onchange這個事件也是Vue幕後處理的,所以checkedNames
數組就可以隨着咱們點擊不一樣的複選框而同步變化
bash
接下來咱們按官網羅列的功能依次分析函數
禁用功能最簡單,使用起來以下代碼,只需添加disabled
屬性便可源碼分析
<el-checkbox v-model="checked1" disabled>備選項1</el-checkbox>
複製代碼
源碼裏對應的:disabled
屬性
<input
v-else
class="el-checkbox__original"
type="checkbox"
aria-hidden="true"
:disabled="isDisabled"
:value="label"
:name="name"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false"
>
複製代碼
,這裏isDisabled
是個計算屬性,由於要考慮到複選框組組件的存在,複選框組組件<el-checkbox-group>
也有disabled
屬性,且複選框組組件是複選框組件的父級組件
isDisabled() {
return this.isGroup
? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled
: this.disabled || (this.elForm || {}).disabled;
},
複製代碼
這裏首先判斷本身是否是被包含在複選框組組件內,若是是的話那麼禁用屬性就是父級的複選框組組件的禁用屬性,不然就是本身的屬性,關於如何判斷是否被包含在複選框組組件內,前面系列文章已經介紹過了
這裏翻看Vue官網,示例代碼說明僅僅須要把多個複選框的input的v-model設置爲同一個數組就能達到複選框組的目的
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
複製代碼
可是查看Element的代碼,還單獨抽象出了一個<el-checkbox-group>
組件,這樣作的好處在於不用給每一個複選框組件設置v-model,只需給<el-checkbox-group>
設置v-model便可,且這個抽象出的組件還能添加其餘不少自定義的屬性,至關於添加一個父組件統一控制全部子複選框的某些行爲
<el-checkbox-group>
的代碼很簡單,html部分以下
<template>
<div class="el-checkbox-group" role="group" aria-label="checkbox-group">
<slot></slot>
</div>
</template>
複製代碼
就是一個div裏面放置了一個插槽,插槽的內容就是用戶放進去的<el-checkbox>
組件,多選框框組組件的props以下
props: {
value: {},
disabled: Boolean,
min: Number,
max: Number,
size: String,
fill: String,
textColor: String
},
複製代碼
其中value
是組件上v-model的用法,具體參考官網和前面文章的說明,這裏的min,max
屬性控制了複選框框組最多和最少能選擇的複選框數量,這是怎麼實現的呢?
首先查看源碼注意到,這裏的邏輯並無放在<el-checkbox-group>
裏實現,而是放在<el-checkbox>
裏實現,由於你實際點擊的是<el-checkbox>
的input,因此須要在複選框組件內實現邏輯,相關代碼以下
model: {
get() {
return this.isGroup
? this.store : this.value !== undefined
? this.value : this.selfModel;
},
set(val) {
if (this.isGroup) {
this.isLimitExceeded = false;
(this._checkboxGroup.min !== undefined &&
val.length < this._checkboxGroup.min &&
(this.isLimitExceeded = true));
(this._checkboxGroup.max !== undefined &&
val.length > this._checkboxGroup.max &&
(this.isLimitExceeded = true));
this.isLimitExceeded === false &&
this.dispatch('ElCheckboxGroup', 'input', [val]);
} else {
this.$emit('input', val);
this.selfModel = val;
}
}
},
複製代碼
這個model是計算屬性,在input裏面的v-model="model"
處使用,表明複選框組件v-model的值,計算屬性的get,set用法參考官網,先看get,首先判斷是否被包含在複選框組組件內,若是是的話,model的值就等於this.store
,這個store也是個計算屬性,以下
store() {
return this._checkboxGroup ? this._checkboxGroup.value : this.value;
},
複製代碼
它也要判斷是否被包含在複選框組組件內,若是是則返回複選框組組件的value,這個value就是下面示例代碼中的checkList
<el-checkbox-group v-model="checkList">
複製代碼
所以這裏就把用戶傳遞進去的checkList這個數組給傳遞到了子<el-checkbox>
內,而this._checkboxGroup
是在isGroup
這個計算屬性中賦值的,它就是本身外層的<el-checkbox-group>
組件
再來看set方法,set是給model賦值時觸發的方法,會在用戶點擊複選框時觸發複選框的onchange事件,在這個事件裏面賦值,從而觸發set方法,set方法裏面用一個isLimitExceeded
變量來判斷是否超出max和min的限制,若是min屬性存在,且val數組的長度小於min,則說明已經越界,此時設置isLimitExceeded
爲true,max同理,val的值是在Vue源碼裏處理的,這裏不用深究,後面當isLimitExceeded
爲false時也就是未越界時才用dispatch通知父組件本身更新後的val的值
一個疑惑是在<el-checkbox>
內的input上綁定了@change="handleChange"
,代碼以下
handleChange(ev) {
if (this.isLimitExceeded) return;
let value;
if (ev.target.checked) {
value = this.trueLabel === undefined ? true : this.trueLabel;
} else {
value = this.falseLabel === undefined ? false : this.falseLabel;
}
this.$emit('change', value, ev);
this.$nextTick(() => {
if (this.isGroup) {
this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
}
});
}
複製代碼
上面的handleChange一樣向父checkGroup組件dispatch了value,那麼這個dispatch和上面的dispatch的區別在哪裏呢?仔細分析後發現這裏的dispatch僅僅是通知<el-checkbox-group>
本身的值變化了,在<el-checkbox-group>
上能夠用@change來獲取變化後的值(用戶能夠拿到該值進行進一步處理),而前面的dispatch則更新了<el-checkbox-group>
的v-model屬性的值,這2個dispatch的做用是不一樣的,請仔細理解
而後handleChange裏this.$emit('change', value, ev)
表示將value和ev原生事件對象傳遞給<el-checkbox>
的onchange事件,由於用戶可能須要這個接口來獲取更新後的數據
最後再來看看當選中複選框時,css樣式變化的邏輯
<span class="el-checkbox__input"
:class="{ 'is-disabled': isDisabled, 'is-checked': isChecked, 'is-indeterminate': indeterminate, 'is-focus': focus }"
aria-checked="mixed"
>
複製代碼
這個span表明模擬的複選框按鈕,其中is-checked
類表明選中時的樣式類,這個類由isChecked
控制,這是個計算屬性,代碼以下
isChecked() {
if ({}.toString.call(this.model) === '[object Boolean]') {
return this.model;
} else if (Array.isArray(this.model)) {
return this.model.indexOf(this.label) > -1;
} else if (this.model !== null && this.model !== undefined) {
return this.model === this.trueLabel;
}
},
複製代碼
第一步判斷this.model
是否是bool類型,注意這裏的判斷方法,Object.prototype.toString.call
來判斷纔是最可靠的,當model是bool時說明這個值就控制這個複選框他本身,若是這個model是數組,則判斷label在不在該數組中,若是在則表示選中了該複選框,從而isChecked
爲true,label是用戶定義在複選框上的屬性,表明該複選框的值,具體看官網
主要內容差很少這麼多,其實還有不少細節沒寫完,具體能夠參考源碼啦