阅读 504

Element组件源码研究-Checkbox多选框

前言

本文的研究思路是通过阅读Element源码,然后自动动手一步一步编写组件,完善其对应功能。

相信量变引起质变,我们继续来研究Element的组件。

以前的研究:

Element组件源码研究-Button

Element组件源码研究-Input输入框

Element组件源码研究-Layout,Link,Radio

还是老套路,先实现最简单的组件封装,再不断给其添加功能。

最基本的实现

区别于原生checkbox只有一个方形选择框,element的checkbox有内容区域。

<el-checkbox v-model="checked">内容区域</el-checkbox>
复制代码

且通过v-model来绑定是否选中。

下面贴出基本实现代码:

<template>
    <label
        class="el-checkbox"
        :class="[
             { 'is-checked': isChecked }
        ]"
    >
        <span class="el-checkbox__input"
            :class="{
                'is-checked': isChecked,
            }"
        >
            <span class="el-checkbox__inner"></span>
            <input
                ref="checkbox"
                class="el-checkbox__original"
                type="checkbox"
                :checked="isChecked"
                @input="handleInput"
                @change="handleChange"
            >
        </span>
        <span class="el-checkbox__label" v-if="$slots.default">
            <slot></slot>
        </span>
    </label>
</template>

<script>
export default {
    name: 'ElCheckbox',
    props: {
        value: {},
    },
    computed: {
        isChecked() {
            return this.value
        }
    },
    methods: {
        handleChange(ev) {
            this.$emit('change', ev.target.checked, ev);
        },
        handleInput(ev) {
            this.$emit('input', ev.target.checked, ev);
        }
    },
}
</script>
复制代码

之前我一直以为v-model就是:value和@input的结合体。原来在原生checkbox上,是:check和@input的结合体。所以上面也可以通过v-model实现对原生checkbox的绑定:

 <!-- :checked="isChecked"
@input="$emit('input', $event.target.checked , ev)" --> 
// 将上面的代码替换为v-model="model",model是一个计算属性
<input
    ref="checkbox"
    class="el-checkbox__original"
    type="checkbox"
    v-model="model"
    @change="handleChange"
>
...

computed: {
    model: {
        get() {
            return this.value
        },
        set(val) {
            this.$emit('input', val);
        }
    },
    isChecked() {
        return this.model
    }
},

复制代码

这样,我们的实现方式就贴近于源码的实现方式了。

禁用状态

先写测试代码

 <el-checkbox v-model="checked" disabled>内容区域</el-checkbox>
复制代码

再为组件添加disabled,根据属性判断样式,给input标签添加:disabled="disabled"即可。

效果如下:

多选框组

测试代码:

<el-checkbox-group v-model="checkList" @change="onChange">
    <el-checkbox label="复选框 A"></el-checkbox>
    <el-checkbox label="复选框 B"></el-checkbox>
    <el-checkbox label="复选框 C"></el-checkbox>
    <el-checkbox label="禁用" disabled></el-checkbox>
    <el-checkbox label="选中且禁用" disabled></el-checkbox>
</el-checkbox-group>
复制代码

多选框组的工作方式和单个Checkbox有区别,值不是true/false了,而是绑定在group组件上的一个数组,选中的checkbox的label会被添加到此数组中。

写一个CheckboxGroup组件。实现思路和RadioGroup非常类似。

<template>
  <div class="el-checkbox-group" role="group">
    <slot></slot>
  </div>
</template>

<script>
  export default {
    name: 'ElCheckboxGroup',
    componentName: 'ElCheckboxGroup',
    props: {
      value: {},
      disabled: Boolean,
    },
  };
</script>
复制代码

CheckboxGroup组件只是一个容器,主要的实现逻辑还在Checkbox组件中。

多选框组工作方式的不同,我们也可以猜到Checkbox组件中的逻辑实现也不一样。代码大体如下:

model: {
    get() {
        return this.isGroup() ? this.store : this.value
    },
    set(val) {
        if (this.isGroup()) {
            this.dispatch('ElCheckboxGroup', 'input', [val]);
        } else {
            this.$emit('input', val);
        }
    }
},
...

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;
},
复制代码

上面的代码实现了在CheckBox中操作父Group的:value的值。

通过isGroup方法判断是否在group中,如果在,就走另一套逻辑。这时候model的set方法的参数val不是true/false。而是checkList:['复选框 A','选中且禁用']这样的数组了。绑定组件的value值彻底变化了。在原生组件上添加:value="label",这里面利用了一个知识点,当value有值时(label有值,也就是在group中),原生组件的v-model绑定的不再是checked属性,而是value属性了。

此时再判断当前CheckBox有没有被选中:

isChecked() {
    if ({}.toString.call(this.model) === '[object Boolean]') {
        return this.model;
    } else if (Array.isArray(this.model)) {
        // 重点是这句,如果model是数组,说明组件在group中,判断lable在不在group的value中
        return this.model.indexOf(this.label) > -1;
    } 
    return false;
},
复制代码

效果如下:

indeterminate 状态

indeterminate 属性用以表示 checkbox 的不确定状态,一般用于实现全选的效果。

测试代码如下:

<el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">全选</el-checkbox>
<div style="margin: 15px 0;"></div>
<el-checkbox-group v-model="checkedCities" @change="handleCheckedCitiesChange">
    <el-checkbox v-for="city in cities" :label="city" :key="city">{{city}}</el-checkbox>
</el-checkbox-group>

const cityOptions = ['上海', '北京', '广州', '深圳'];
data() {
  return {
    checkAll: false,
    checkedCities: ['上海', '北京'],
    cities: cityOptions,
    isIndeterminate: true
  };
},
methods: {
    handleCheckAllChange(val) {
        this.checkedCities = val ? cityOptions : [];
        this.isIndeterminate = false;
    },
    handleCheckedCitiesChange(value) {
        let checkedCount = value.length;
        this.checkAll = checkedCount === this.cities.length;
        this.isIndeterminate = checkedCount > 0 && checkedCount < this.cities.length;
    }
},
复制代码

组件中添加indeterminate属性,控制样式即可,没有啥复杂度,效果如下:

可选项目数量的限制

  1. 组件添加isLimitExceeded:false

  2. 添加isLimitDisabled计算属性。根据group的max和min属性判断自己还能不能被选择。

isLimitDisabled() {
    const { max, min } = this._checkboxGroup;
    return !!(max || min) &&
    (this.model.length >= max && !this.isChecked) ||
    (this.model.length <= min && this.isChecked);
},
复制代码
  1. 最后在isDisabled计算属性中添加this.isLimitDisabled的禁用逻辑。

效果如下:

按钮样式

新写一个组件CheckboxButton,逻辑与Checkbox基本类似,不再赘述。详细代码可以下下面的码云仓库。

带有边框

给组件添加属性,增加样式判断。不再赘述。

效果如下:

总结

Checkbox组件的研究就到这里。代码在码云:https://gitee.com/DaBuChen/my-element-ui/tree/checkbox/

其他组件源码研究:

Element组件源码研究-Button

Element组件源码研究-Input输入框

Element组件源码研究-Layout,Link,Radio

Element组件源码研究-Checkbox多选框

Element组件源码研究-InputNumber 计数器

Element组件源码研究-Loading组件

Element组件源码研究-Message组件