前言
该文章是Vue复习姿势系列的第三篇,分享复选框组件的实现过程。
脚手架请参考第一篇: juejin.cn/post/701798…
组件示例地址: oversnail.github.io/over-snail-…
git地址: github.com/overSnail/o…
第一篇:Vue复习姿势系列之UI组件——按钮(Button)
第二篇:Vue复习姿势系列之UI组件——单选框(Radio)
介绍
基础表单组件,功能是提供一组选项给用户进行多选。
功能实现
1. 基础功能
src/packages目录下新建checkbox文件夹,文件夹内创建checkbox.vue和index.js。
src/styles目录下心新建checkbox.scss,并在src/styles/index.scss中引入。
结构上仍然是label元素包裹input[type="checbox"]和span元素,一个作为选项框,一个作为选项文本。
checkbox的打勾图标实现思路有两个。
iconfont图标+scale缩放,加缩放的原因是大部分浏览器的最小字体尺寸是12px,无法显示小于12px的字体图标。- 伪元素设置两个相邻边框,然后角度旋转,模仿一个打勾图标出来(
element-ui的方案)。 这里采用第二种
// checkbox.vue
<template>
<label
class="my-checkbox"
:class="{
'my-checkbox-checked': checked,
}"
>
<input type="checkbox" class="my-checkbox-input" @click="handleClick" />
<span
class="my-checkbox-icon"
:class="{
'my-checkbox-icon-checked': checked,
}"
>
</span>
<span class="my-checkbox-label">
<slot>选项一</slot>
</span>
</label>
</template>
<script>
export default {
name: 'MyCheckbox',
data() {
return {
checked: false, // 是否被选中
}
},
props: {
// v-model的值
value: {
type: [Boolean, Number, String],
default: false,
},
},
watch: {
value: {
handler(newVal) {
this.checked = newVal
},
immediate: true,
},
},
methods: {
/**
* @description 复选框点击事件
*/
handleClick() {
this.checked = !this.checked
this.$emit('input', this.checked)
this.$emit('change', this.checked)
},
},
}
</script>
// checkbox.scss
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(checkbox) {
box-sizing: border-box;
display: inline-block;
cursor: pointer;
height: 20px;
font-size: $--font-size-large;
vertical-align: top;
margin-right: 20px;
&-input {
display: none;
}
&-icon {
display: inline-block;
box-sizing: border-box;
position: relative;
top: 2px;
width: 14px;
height: 14px;
border-radius: 2px;
border: 1px solid $--border-color;
&:after {
content: "";
position: absolute;
box-sizing: border-box;
width: 7px;
height: 3px;
border-top: 1px solid #fff;
border-right: 1px solid #fff;
left: 3px;
top: 3.5px;
transform: rotate(135deg);
}
&-checked {
background: $--color-primary;
border-color: $--color-primary;
}
}
&-label {
display: inline-block;
height: 100%;
margin-left: 3px;
}
&-checked {
color: $--color-primary;
}
}
// styles/index.scss
@import "checkbox";
2. 禁用状态
因为label标签的作用将input的鼠标事件扩散到整个组件上,所以用input[type="checkbox"]的原生disabled属性即可。
// checkbox.vue 省略部分代码
<template>
<label
class="my-checkbox"
:class="{
'my-checkbox-checked': isChecked,
'my-checkbox-disabled': disabled
}"
>
<input
type="checkbox"
class="my-checkbox-input"
@click="handleClick"
:disabled="disabled"
/>
<span
class="my-checkbox-icon"
:class="{
'my-checkbox-icon-checked': isChecked,
'my-checkbox-icon-disabled': disabled,
'my-checkbox-icon-checked-disabled': disabled && isChecked
}"
>
</span>
<span class="my-checkbox-label">
<slot>选项一</slot>
</span>
</label>
</template>
<script>
export default {
......
props: {
// 是否禁用该组件
disabled: {
type: Boolean,
default: false,
},
}
}
</script>
// checkbox.scss 省略部分代码
......
@include b(checkbox) {
&-icon {
......
&-checked {
background: $--color-primary;
border-color: $--color-primary;
&-disabled {
border-color: $--color-disabled;
&:after {
border-color: $--color-disabled!important;
}
}
}
&-disabled {
background-color: #edf2fc;
&:after {
border-color: #edf2fc;
}
}
}
......
&-disabled {
cursor: not-allowed;
color: $--color-disabled;
}
}
3. 多选框组
- 该功能实现方式是创建个
checkbox-group组件将checkbox包裹,checkbox功能由父级接管。 - 会用组件通信中的
$dispatch和$broadcast,前者向上派发事件,后者向下广播事件。 - vue组件生命周期是
由内而外的:父created -> 子created -> 子mounted -> 父mounted,父组件要在created中监听事件,不能在mounted中监听。 checkbox-group的disabled具体逻辑比较简单,只需根据disabled值来调整checkbox组件内的myDisabled属性即可。而input[type="checkbox"]的disabled属性由myDisabled和disabled共同决定。
创建checkbox-group组件:
src/packages目录下新建checkbox-group文件夹,文件夹内创建checkbox-group.vue和index.js。
src/styles目录下心新建checkbox-group.scss,并在src/styles/index.scss中引入。
// checkbox-group.vue
<template>
<div class="my-checkbox-group">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'myCheckboxGroup',
data() {
return {
options: [], // 选项
}
},
props: {
// 选中值
value: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
},
watch: {
value: {
handler(newVal) {
this.initValue()
},
immediate: true,
},
// 禁用状态
disabled: {
handler(newVal) {
this.syncOptionsDisable(newVal)
},
immediate: true,
},
},
created() {
// 监听on-checkbox-add事件,将checkbox实例存到options上
this.$on('on-checkbox-add', (checkbox) => {
this.options.push(checkbox)
this.initValue()
this.syncOptionsDisable(this.disabled)
})
// 监听on-checkbox-remove事件,将checkbox实例从options中移除
this.$on('on-checkbox-remove', (checkbox) => {
this.options.splice(this.options.indexOf(checkbox), 1)
this.syncValue();
})
// 监听checkbox的选中事件,做value值同步
this.$on('on-checkbox-select', () => {
this.syncValue();
})
},
methods: {
initValue() {
this.options.forEach((cell) => {
cell.checked = this.value.find((d) => d === cell.label)
})
},
/**
* @description value值同步
*/
syncValue() {
let currentValue = this.options.reduce((currentArr, cell) => {
cell.checked && currentArr.push(cell.label);
return currentArr;
}, [])
this.$emit('input', currentValue)
},
/**
* @description 设置子选项的myDisabled属性
* @param {Boolean} disabled 是否禁用
*/
syncOptionsDisable(disabled) {
this.options.forEach((d) => {
d.myDisabled = disabled
})
},
},
}
</script>
// checkbox.vue 省略部分代码
<template>
<label
class="my-checkbox"
:class="{
'my-checkbox-checked': checked,
'my-checkbox-disabled': disabled || myDisabled
}"
>
<input
type="checkbox"
class="my-checkbox-input"
@click="handleClick"
:disabled="disabled || myDisabled"
/>
<span
class="my-checkbox-icon"
:class="{
'my-checkbox-icon-checked': checked,
'my-checkbox-icon-disabled': disabled || myDisabled,
'my-checkbox-icon-checked-disabled': (myDisabled || disabled) && checked
}"
>
</span>
<span class="my-checkbox-label">
<slot>选项一</slot>
</span>
</label>
</template>
<script>
export default {
......
data() {
return {
......
myDisabled: false, // 是否被禁用,该属性由父级控制
}
},
props: {
......
// 选中状态下的值,在多选时发挥作用。
label: {
type: [Boolean, Number, String],
default: ""
}
},
......
mounted() {
// 通知myCheckboxGroup组件调用on-checkbox-add方法,参数为当前checkbox实例
this.dispatch('myCheckboxGroup', 'on-checkbox-add', this)
},
beforeDestroy() {
// 移除时,调用myCheckboxGroup组件的on-checkbox-remove方法
this.dispatch('myCheckboxGroup', 'on-checkbox-remove', this)
},
methods: {
/**
* @description 复选框点击事件
*/
handleClick() {
......
this.dispatch('myCheckboxGroup', 'on-checkbox-select', this)
},
},
}
</script>
4. 可选项数量限制
- 数量限制具体表现为:当等于最小数量限制时,已选项不能取消;当等于最大数量时,未选项不可点击。
checkbox增加新属性limitDisabled用来控制因父级数量限制而导致的不可使用情况。所以checkbox的禁用功能由disabled、myDisabled、limitDisabled三个属性决定,为了方便,我们采用computed计算属性处理,computed属性为forbidden。
checkbox.vue 省略部分代码
<template>
<label
class="my-checkbox"
:class="{
'my-checkbox-checked': checked,
'my-checkbox-disabled': forbidden
}"
>
<input
type="checkbox"
class="my-checkbox-input"
@click="handleClick"
:disabled="forbidden"
/>
<span
class="my-checkbox-icon"
:class="{
'my-checkbox-icon-checked': checked,
'my-checkbox-icon-disabled': forbidden,
'my-checkbox-icon-checked-disabled': forbidden && checked
}"
>
</span>
<span class="my-checkbox-label">
<slot>选项一</slot>
</span>
</label>
</template>
<script>
export default {
......
data() {
return {
......
limitDisabled: false, // 是否因为父级数量限制而被禁用
}
},
......
computed: {
forbidden() {
return (this.disabled || this.myDisabled || this.limitDisabled)
}
},
......
}
</script>
// checkbox-group.vue 省略部分代码
......
<script>
export default {
props: {
......
// 限制最多选择数量, -1为不限制
max: {
type: Number,
default: -1,
},
// 限制最少选择数量, -1为不限制
min: {
type: Number,
default: -1,
},
},
......
methods: {
initValue() {
this.options.forEach((cell) => {
cell.checked = this.value.find((d) => d === cell.label)
})
this.$nextTick(() => {
this.optionsLimit()
})
},
/**
* @description value值同步
*/
syncValue() {
let currentValue = this.options.reduce((currentArr, cell) => {
cell.checked && currentArr.push(cell.label)
return currentArr
}, [])
this.$emit('input', currentValue)
this.optionsLimit()
},
/**
* @description 数量限制操作
*/
optionsLimit() {
this.$nextTick(() => {
this.options.forEach((cell) => {
cell.limitDisabled = false
})
const length = this.value.length
// 最大值限制
if (length >= this.max && this.max > -1) {
this.options.forEach((cell) => {
if (!cell.checked) {
cell.limitDisabled = true
}
})
}
// 最小值限制
if (length <= this.min && this.min > -1) {
this.options.forEach((cell) => {
if (cell.checked) {
cell.limitDisabled = true
}
})
}
})
},
},
}
</script>
5. 带有边框
- 增加
size和border属性,设计4种尺寸不同的带边框复选框。
// checkbox.vue 省略部分代码
<template>
<label
class="my-checkbox"
:class="{
'my-checkbox-checked': checked,
'my-checkbox-disabled': forbidden,
[`my-checkbox-${size}-border`]: border
}"
>
......
</label>
</template>
<script>
// 工具函数,用于判断传入的值是否符合条件
import { oneOf } from '../../utils/assist'
......
export default {
......
props: {
......
// 是否绘制边框
border: {
type: Boolean,
default: false,
},
// 尺寸
size: {
validator(value) {
return oneOf(value, ['large', 'medium', 'small', 'mini'])
},
type: String,
default: 'medium',
},
},
......
}
</script>
// checkbox.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(checkbox) {
......
// border相关样式
&-large-border {
height: 40px;
padding: 8px 8px 12px 8px;
border: 1px solid $--border-color;
border-radius: 4px;
}
&-medium-border {
height: 36px;
padding: 6px 8px 10px 8px;
border: 1px solid $--border-color;
border-radius: 4px;
}
&-small-border {
height: 32px;
padding: 4px 8px 8px 8px;
border: 1px solid $--border-color;
border-radius: 4px;
font-size: $--font-size-medium;
}
&-mini-border {
height: 28px;
padding: 2px 8px 6px 8px;
border: 1px solid $--border-color;
border-radius: 4px;
font-size: $--font-size-medium;
}
}
6. 按钮样式
- 将
checkbox渲染成按钮样式,也是对css的操作。 button属性设置给checkbox-group,由父级接管该功能。
// checkbox.vue 省略部分代码
<template>
<label
class="my-checkbox"
:class="{
'my-checkbox-checked': checked,
'my-checkbox-disabled': forbidden,
[`my-checkbox-${size}-border`]: border,
[`my-checkbox-${size}-button`]: button,
'my-checkbox-checked-button': checked && button,
}"
>
......
</label>
</template>
<script>
export default {
......
data() {
return {
......
button: false, // 是否渲染成按钮样式
}
},
......
}
</script>
// checkbox.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(checkbox) {
......
&-icon {
......
&-button {
display: none;
}
}
// 按钮样式相关样式
&-large-button {
float: left;
background-color: #fff;
height: 40px;
line-height: 38px;
padding: 0 15px 0 12px;
margin: 0;
border: 1px solid $--border-color;
}
&-medium-button {
float: left;
background-color: #fff;
height: 36px;
line-height: 34px;
padding: 0 15px 0 12px;
margin: 0;
border: 1px solid $--border-color;
}
&-small-button {
float: left;
background-color: #fff;
height: 32px;
line-height: 30px;
padding: 0 15px 0 12px;
margin: 0;
border: 1px solid $--border-color;
font-size: $--font-size-medium;
}
&-mini-button {
float: left;
background-color: #fff;
height: 28px;
line-height: 26px;
padding: 0 15px 0 12px;
margin: 0;
border: 1px solid $--border-color;
font-size: $--font-size-medium;
}
}
checkbox-group.vue 省略部分代码<template>
<div class="my-checkbox-group">
<slot></slot>
</div>
</template>
<script>
export default {
......
props: {
......
// 是否启用按钮样式
button: {
type: Boolean,
default: false,
},
},
watch: {
......
// 是否使用按钮样式
button: {
handler(newVal) {
this.syncOptionsButtonStyle(newVal)
},
immediate: true,
},
},
created() {
// 监听on-checkbox-add事件,将checkbox实例存到options上
this.$on('on-checkbox-add', (checkbox) => {
......
this.syncOptionsButtonStyle(this.button)
})
},
methods: {
......
/**
* @description 设置子选项的button属性,用以控制按钮样式
* @param {Boolean} value 是否设置
*/
syncOptionsButtonStyle(value) {
this.options.forEach((d) => {
d.button = value
})
},
},
}
</script>
// checkbox-group.scss
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(checkbox-group) {
overflow: hidden;
.my-checkbox-large-button,
.my-checkbox-medium-button,
.my-checkbox-small-button,
.my-checkbox-mini-button {
&:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
结语
相对于单选框而言,复选框大多数的功能与其都是高度一致的。除了可选项数量限制这个功能,这个功能其实是在做数学题,只要掌握了它的表现规律(当等于最小数量限制时,已选项不能取消;当等于最大数量时,未选项不可点击),实现起来就很简单啦。