前天晚上又投产了一版功能,刚投产完产品经理发现了一些ui交互的问题,便提出来看是否能优化。我看了下问题,确实有点影响用户的使用,就是多选Select的清空按钮当鼠标停留在Select上时应该显示出来,而现在只有鼠标停留在边框时和右边一小块区域显示,划入Select里反而不显示,如果划入得快,就很难发现有个清空按钮。先来看看默认行为的效果:
所以产品提出来希望只要鼠标在Select里,就能看到清空按钮。 另一个问题,是希望能改掉计数的方式,希望右边的tag的数量能改成选择数据的总数,目前是选择两个,显示的是+1,期望的是显示的是+2,即数据的总数。看看默认行为的效果:
先附上需求完成的最终效果(多选清空+计数修改):
第一个问题还是可以优化一下的,毕竟清空按钮一闪而过不太方便。另一个修改计数的问题感觉可以算是比较小众的场景了,但是既然产品提了这个问题我们就思考下能不能改。 我们先来分析下第一个问题,其实清空功能能正常使用,就是清空按钮一闪而过使用不方便,我其实也是按照文档来使用的属性,为什么会有这个问题,不管怎样再去看看文档吧,结果就发现了大问题。。
文档里写的清清楚楚,清空功能仅适用于单选(难道是多选不配清空吗。。)
既然如此,只能自己封装个自定义下拉组件来实现多选清空功能了。首先我们确认一下需求:
- 实现多选下拉框清空功能
- 实现自定义组件的v-model
- 封装自定义组件
- 实现多选下拉框的自定义计数
实现多选下拉框清空功能
- 在el-select外层加个父盒子
- 在el-select下方加个清空按钮,通过css控制按钮默认隐藏,hover时显示就可以了
<div class="custom-select-wrapper">
<el-select
v-model="selectValue"
multiple
collapse-tags
placeholder="请选择"
>
<el-option label="黄金糕" value="黄金糕"></el-option>
<el-option label="双皮奶" value="双皮奶"></el-option>
<el-option label="蚵仔煎" value="蚵仔煎"></el-option>
<el-option label="龙须面" value="龙须面"></el-option>
<el-option label="北京烤鸭" value="北京烤鸭"></el-option>
</el-select>
<!-- 清空按钮 -->
<i class="el-icon-circle-close"></i>
</div>
<style lang="less" scoped>
.custom-select-wrapper {
position: relative;
display: inline-block;
.el-icon-circle-close {
position: absolute;
top: 50%;
right: 8px;
margin-top: -8px;
color: #C0C4CC;
display: none;
cursor: pointer;
}
&:hover {
.el-icon-circle-close {
display: block;
}
}
}
</style>
效果:
可以看到清空按钮展示没问题了,我们再控制下hover时让下箭头隐藏就可以了。
&:hover {
.el-icon-circle-close {
display: block;
}
/deep/ .el-input__suffix {
display: none;
}
}
效果:
这时我们的ui效果就做好了,接着就要给清空按钮绑定click事件,点击时把多选下拉框的值置为空数组就可以了。由于我们是在封装一个自定义组件,将来使用组件时我们会给组件传递一个value值,所以清空时应该是清空这个传进来的value,为了更便于使用,我们再让自定义组件支持v-model,所以等完成了v-model后我们再回来写清空的逻辑。
实现自定义组件的v-model
- 让自定义组件支持v-model,则自定义组件将会接收一个value prop,我们要把value绑定到el-select,这时el-select的值就为父组件传的值。
- 同时在父组件将会监听input事件,由于我们是下拉框,所以应该是监听change事件,因此我们要改变v-model的默认行为,达到监听change事件的效果
- 因为父组件会监听change事件,所以子组件需要emit change事件,我们就监听el-select的change,再把最新值emit出去就可以了,父组件会接收到这个最新值并赋给v-model绑定的值
父组件:
<custom-select
v-model="selectValue"
></custom-select>
selectValue: ['黄金糕', '蚵仔煎']
子组件:
<el-select
:value="value"
@change="handleSelectChange"
>
<el-option label="黄金糕" value="黄金糕"></el-option>
<el-option>...</el-option>
</el-select>
props: {
value: {
type: Array,
default: () => ([])
}
},
model: {
event: 'change'
},
methods: {
handleSelectChange(val) {
this.$emit('change', val)
}
}
效果:
这样自定义组件就支持了v-model,我们接收开发清空功能。清空功能就是点击清空按钮时,把下拉框的值置为空数组,下拉框的值现在是由父组件传入的,要改变传入的prop,我们就$emit一个空数组就可以了
<!-- 清空按钮 -->
<i class="el-icon-circle-close" @click="handleClearClick"></i>
handleClearClick() {
this.$emit('change', [])
}
效果:
这样清空功能就做完了,这里我们其实只开发了多选下拉框的清空功能,其他原有我功能我们还是要复用的,所以我们可以对组件进行封装,使得组件其他原来的功能可以复用,只不过去覆盖掉原来的清空功能。
封装自定义组件
- 自定义组件会接收originAttr prop,代表el-select原有的功能
- 还会接收clearable prop,因为是单独开发的功能就单独使用prop,这样可以和originAttr区分开,使用组件时就可以知道哪些功能是自己开发的。
- 如果originAttr也有clearable,我们直接选择忽略掉
- 如果是单选,我们还是使用原来组件自带的clearable功能,如果是多选,才使用我们自己开发的清空功能
第一步和第三步可以结合起来写,接收originAttr,并过滤掉clearable,把过滤后的结果给到el-select
<el-select
:value="value"
@change="handleSelectChange"
v-bind="attr"
>
<el-option label="黄金糕" value="黄金糕"></el-option>
<el-option>...</el-option>
</el-select>
props: {
originAttr: {
type: Object,
default: () => ({})
},
},
computed: {
attr() {
const keyList = Object.keys(this.originAttr)
const attrList = keyList.filter(item => item !== 'clearable')
const attr = {}
attrList.forEach(item => {
attr[item] = this.originAttr[item]
})
return attr
}
},
接着开发第二步和第四步,如果是单选框,我们则直接给计算属性attr加上clearable,代表还是用原来的清空功能。如果是多选框,则增加一个是否为多选框的计算属性,当这个计算属性为true时,才会有我们开发的清空功能,也就是才会显示清空按钮,并且要给父盒子增加类名,才能使对应的下箭头隐藏。
父组件:
<custom-select
v-model="selectValue"
:originAttr="{
multiple: true,
'collapse-tags': true,
placeholder: '请选择',
clearable: true,
}"
clearable
></custom-select>
子组件:
<div :class="['custom-select-wrapper', isMultipleSelect ? 'custom-multiple-select-wrapper' : '']">
...
<!-- 清空按钮 -->
<i
...
v-if="isMultipleSelect"
></i>
</div>
attr() {
...
...
// 如果有clearable属性,并且是单选框,直接加在属性上
if (this.clearable && this.originAttr.multiple !== true) {
attr.clearable = true
}
return attr
},
isMultipleSelect() {
return this.originAttr.multiple === true
}
// 把之前跟清空有关的样式都挂在custom-multiple-select-wrapper这个类名下
.custom-select-wrapper.custom-multiple-select-wrapper {
...
}
测试多选:
测试单选:
把originAttr的multiple属性去掉或者改为false,并把下拉框的值改为字符串,发现如下问题:
这里我们要把value的prop类型改为String或Array,因为单选框的值不是数组,根据其他需求场景可能还要加上Number,这里就只定义为String后Array类型了。
效果:
下拉框的选项应该也有外部传入,再改写下组件
<custom-select
...
>
<el-option label="黄金糕" value="黄金糕"></el-option>
<el-option label="双皮奶" value="双皮奶"></el-option>
<el-option label="蚵仔煎" value="蚵仔煎"></el-option>
<el-option label="龙须面" value="龙须面"></el-option>
<el-option label="北京烤鸭" value="北京烤鸭"></el-option>
</custom-select>
子组件:
<el-select
...
>
<slot></slot>
</el-select>
这样我们的组件就封装完了,在支持原有el-select的功能下,多增加了自己开发的多选下拉框清空功能。
实现多选下拉框的自定义计数
这个需求暂时只想到去修改dom节点内容这个方法。思路就是每次下拉框值改变时,我们查找到页面上+1 tag的节点,再去修改这个dom节点的innerText,但是只在选择了两个值以上时才去做这件事情,因为一个值并不会有+1这个节点。
- 加上custom-select类名
- 下拉框值改变时,获取到+1这个dom节点,并把节点的innerText改为值的长度即val.length
<el-select
...
class="custom-select"
@change="handleSelectChange"
>
</el-select>
handleSelectChange(val) {
this.$emit('change', val)
this.$nextTick(() => {
const domNodeList = document.querySelectorAll('.custom-select .el-tag .el-select__tags-text')
if (domNodeList.length > 1) {
const domNode = document.querySelector('.custom-select .el-tag:last-child .el-select__tags-text')
domNode.innerText = `+ ${val.length}`
}
})
},
效果
这样就完成了后面tag显示的数量是当前选择的数据总数了。 最后贴上所有代码:
父组件:
<custom-select
v-model="selectValue"
:originAttr="{
multiple: true,
'collapse-tags': true,
placeholder: '请选择',
clearable: true,
}"
clearable
>
<el-option label="黄金糕" value="黄金糕"></el-option>
<el-option label="双皮奶" value="双皮奶"></el-option>
<el-option label="蚵仔煎" value="蚵仔煎"></el-option>
<el-option label="龙须面" value="龙须面"></el-option>
<el-option label="北京烤鸭" value="北京烤鸭"></el-option>
</custom-select>
data() {
return {
selectValue: ['蚵仔煎'],
}
},
子组件:
<template>
<div :class="['custom-select-wrapper', isMultipleSelect ? 'custom-multiple-select-wrapper' : '']">
<el-select
:value="value"
@change="handleSelectChange"
class="custom-select"
v-bind="attr"
>
<slot></slot>
</el-select>
<!-- 清空按钮 -->
<i
class="el-icon-circle-close"
@click="handleClearClick"
v-if="isMultipleSelect"
></i>
</div>
</template>
<script>
export default {
props: {
clearable: Boolean,
originAttr: {
type: Object,
default: () => ({})
},
value: {
type: [String, Array],
default: () => {
return ''
}
}
},
computed: {
attr() {
const keyList = Object.keys(this.originAttr)
// 直接忽略originAttr里的clearable属性
const attrList = keyList.filter(item => item !== 'clearable')
const attr = {}
attrList.forEach(item => {
attr[item] = this.originAttr[item]
})
// 如果有clearable属性,并且是单选框,直接加在属性上
if (this.clearable && this.originAttr.multiple !== true) {
attr.clearable = true
}
return attr
},
isMultipleSelect() {
return this.originAttr.multiple === true
}
},
model: {
event: 'change'
},
methods: {
handleSelectChange(val) {
this.$emit('change', val)
this.$nextTick(() => {
const domNodeList = document.querySelectorAll('.custom-select .el-tag .el-select__tags-text')
if (domNodeList.length > 1) {
const domNode = document.querySelector('.custom-select .el-tag:last-child .el-select__tags-text')
domNode.innerText = `+ ${val.length}`
}
})
},
handleClearClick() {
this.$emit('change', [])
}
}
}
</script>
<style lang="less" scoped>
.custom-select-wrapper.custom-multiple-select-wrapper {
position: relative;
display: inline-block;
.el-icon-circle-close {
position: absolute;
top: 50%;
right: 8px;
margin-top: -8px;
color: #C0C4CC;
display: none;
cursor: pointer;
}
&:hover {
.el-icon-circle-close {
display: block;
}
/deep/ .el-input__suffix {
display: none;
}
}
}
</style>