复选框的逻辑比单选框更为复杂,代码量也更多,这里只介绍其与单选框不一样的逻辑,其他的分析参考单选框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
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是用户定义在复选框上的属性,表明该复选框的值,具体看官网
主要内容差很少这么多,其实还有不少细节没写完,具体能够参考源码啦