背景
需要按照UI稿开发一个可以手动选择和取消选择子项的功能,然后将用户选择的数据搜集到一个数组中。思考了一下,so easy!马上开始码代码。。。
<template>
<div class="checkbox-btn" @click="hanldeChange" :class="{ 'checkbox-btn-active': isChecked }">
<slot></slot>
<!-- 我的icon图标 -->
<h-icon class="iconfont icon-checkmark checkbox-btn-icon"></h-icon>
</div>
</template>
<script>
export default {
name: "checkboxBtn",
props: {
value: [String, Number],
checkboxGroup: Array
},
computed: {
isChecked() {
return this.checkboxGroup.indexOf(this.value) > -1;
},
},
methods: {
hanldeChange() {
// 告诉parent组件我改变了
this.$emit("change", this.value, this.isChecked)
}
}
}
</script>
<style lang="scss" scoped>
.checkbox-btn {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 84px;
height: 28px;
font-size: 12px;
color: #555B66;
cursor: pointer;
border-radius: 2px;
border: 1px solid #DADFE6;
&:hover {
border-color:#3a8ee6;
}
&-icon {
display: none;
}
&-active {
&:after {
position: absolute;
right: 0;
bottom: 0;
content: "";
width: 0;
height: 0;
border-top: 16px solid transparent;
border-right: 16px solid #3484FF;
}
.checkbox-btn-icon {
display: block;
position: absolute;
right: -1px;
bottom: -10px;
font-size: 12px;
transform: scale(0.7);
color: white;
z-index: 1;
}
}
}
</style>
<template>
<div class="wrapper">
<checkbox-btn
v-for="item in taskStatusList"
:value="item.value"
@change="handleChange"
:key="item.value"
:checkboxGroup="statusForm.statusList"
>
{{ item.label }}
</checkbox-btn>
</div>>
</template>
<script>
import CheckboxBtn from "@/components/checkboxBtn.vue";
export default {
data() {
return {
statusForm: {
statusList: []
}
}
},
components: {
CheckboxBtn
},
methods: {
handleChange(value, isChecked) {
let list = this.statusForm.statusList;
if (isChecked) {
list.splice(list.indexOf(value), 1);
} else {
list.push(value)
}
},
}
}
</script>
代码实现了,可是一点都不优雅,每个子组件都需要传入list参数,都需要去监听change事件。每次引入组件外层都需要写逻辑来处理list数组的增删,对于组件使用者来说,我不关心这些,我只关心最后用户选择生成的数组。
是否有优化的方法?思考良久,这组件功能怎么这么眼熟啊。一番搜索,是的,[Element checkbox-group] (https://element.eleme.cn/#/zh-CN/component/checkbox)组件功能不就是一样样的吗,看来得“借鉴”一下,人家的组件是这样引用的:
<template>
<el-checkbox-group v-model="checkboxGroup1">
<el-checkbox-button v-for="city in cities" :label="city" :key="city">{{city}}</el-checkbox-button>
</el-checkbox-group>
</template>
Element在外面包裹了一层group组件,我是不是也可以这样呢,肯定是可以的。问题的关键在于怎么更新父组件的数据,子组件emit吗,那外层组件不是又要监听这个事件,有没有更easy的方法。我们先来揭开v-model这个指令的语法糖:
Vue官网: 一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件。
思考良久,是不是触发group组件的input事件就行了呢。赶紧再去“借鉴一下”,于是我们的改良版组件就出来了:
<template>
<div class="checkbox-btn-group">
<slot></slot>
</div>
</template>
<script>
// group组件
export default {
name: 'checkboxBtnGroup',
componentName: 'CheckboxBtnGroup',
props: {
value: {},
}
}
</script>
<style lang="scss" scoped>
.checkbox-btn-group {
display: flex;
flex-direction: row;
}
</style>
<template>
<div class="checkbox-btn" @click="hanldeChange" :class="{ 'checkbox-btn-active': isChecked }">
<slot></slot>
<h-icon class="iconfont icon-checkmark checkbox-btn-icon"></h-icon>
</div>
</template>
<script>
// checkbox子组件
import Emitter from '@/utils/emitter';
export default {
name: "checkboxBtn",
props: {
value: [String, Number]
},
mixins: [Emitter],
computed: {
isChecked() {
if (Array.isArray(this._checkboxGroup.value)) {
return this._checkboxGroup.value.indexOf(this.value) > -1;
} else {
return false
}
},
_checkboxGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'CheckboxBtnGroup') {
parent = parent.$parent;
} else {
return parent;
}
}
return false;
},
},
methods: {
hanldeChange() {
this.$nextTick(() => {
let result = this._checkboxGroup.value
if (this._checkboxGroup) {
if (this.isChecked) {
result.splice(result.indexOf(this.value), 1);
} else {
result.push(this.value)
}
this.dispatch('CheckboxBtnGroup', 'input', [result]);
}
});
}
}
}
</script>
<style lang="scss" scoped>
.checkbox-btn {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 84px;
height: 28px;
font-size: 12px;
color: #555B66;
cursor: pointer;
border-radius: 2px;
border: 1px solid #DADFE6;
&:hover {
border-color:#3a8ee6;
}
&-icon {
display: none;
}
&-active {
&:after {
position: absolute;
right: 0;
bottom: 0;
content: "";
width: 0;
height: 0;
border-top: 16px solid transparent;
border-right: 16px solid #3484FF;
}
.checkbox-btn-icon {
display: block;
position: absolute;
right: -1px;
bottom: -10px;
font-size: 12px;
transform: scale(0.7);
color: white;
z-index: 1;
}
}
}
</style>
具体在页面中怎么引入我们开发好的组件呢,看下面,是不是优雅了很多。
<template>
<checkbox-btn-group v-model="statusForm.statusList">
<checkbox-btn v-for="item in taskStatusList" :value="item.value" :key="item.value">{{ item.label }}</checkbox-btn>
</checkbox-btn-group>
</template>
我们发现在上面多了一个dispatch方法,这又是何方神圣:
export default {
methods: {
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
}
}
};
这就是Element传说中自己实现的dispatch方法,具体的作用就是不断向上查找,直到找到跟变量componentName名字相同的parent组件,然后触发事件变量eventName事件。我们这里触发的就是input事件,通过v-model的语法糖,我们更新了group组件上绑定的数组,至此整个完整的组件就完成了。
总结
我们通过dispatch方法,在子组件封装了外层数组更新的逻辑,利用v-model的语法糖,更新了group组件的数据。在组件引入时,最外面所需的只要传入我们的数组即可。
彩蛋
由于文章篇幅的限制,大家自己去看看element checkbox-button 和 checkbox-group的源码,看看大家发现了啥(大家观察它的model),欢迎大家到评论中来讨论。
欢迎关注
码字实属不易,希望大家能关注一波公众号,一起学习,一起Easy。