el-select多选入坑背景
需求描述:el-select多选 已选中的values 只展示前3个,多的+n展示,你会怎么实现呢?
我的解决思路如下:
1、首先我们看一下element文档el-select用法
collapse-tags属性可以将超出一个的折叠,然而只能超出一个
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
collapse-tags | 多选时是否将选中值按文字的形式展示 | boolean | --- | false |
2、看看el-select源码,有没有隐藏的api
el-select template部分分为三部分
- 多选相关
- input标签
- 悬浮展示option下拉列表,使用了 vue-popper
这里只展示部分,完整的自己去 github element源码下packages/select 下查看完整的
我们着重看一下多选相关 的代码,熟悉el-select的同学知道多选选项是el-tag的形式展示,这部分在div class="el-select__tags"
标签下
html
<div class="el-select__tags" v-if="multiple" ref="tags">
// ---折叠展示
<span v-if="collapseTags && selected.length">
<el-tag @close="deleteTag($event, selected[0])" ...>
<span class="el-select__tags-text">{{ selected[0].currentLabel }}</span>
</el-tag>
<el-tag v-if="selected.length > 1" ...>
<span class="el-select__tags-text">+ {{ selected.length - 1 }}</span>
</el-tag>
</span>
// ---非折叠
<transition-group @after-leave="resetInputHeight" v-if="!collapseTags">
<el-tag v-for="item in selected" @close="deleteTag($event, item)" ...>
<span class="el-select__tags-text">{{ item.currentLabel }}</span>
</el-tag>
</transition-group>
// ---filterable的输入框
<input class="el-select__input" v-if="filterable" ... ref="input" />
</div>
这里代码也分为2块
- 多选分
collapseTags=true or false
:当collapseTags=true
,只展示一个el-tag;当collapseTags=false
,采用v-for展示多个el-tag标签。 - input标签:只有
filterable=true
的时候展示,我们知道设置filterable时候,输入框是可以在el-tag后面输入关键字的,这时候我们输入的位置就是这个input区域
可以看出 超出一个就折叠固定写在代码里面了,也没有提供slot插槽来自定义,看起来没有可用的api可以直接用了
3、上网搜索一下别人的解法
总结:没找到想要的答案
4、自己动手吧
思路:通过js操作dom,将多余的el-tag display:none
html
// ---非折叠
<transition-group @after-leave="resetInputHeight" v-if="!collapseTags">
<el-tag v-for="item in selected" @close="deleteTag($event, item)" ...>
<span class="el-select__tags-text">{{ item.currentLabel }}</span>
</el-tag>
</transition-group>
代码如下
js
regionChange(values) {
this.$nextTick(() => {
const regionElement = this.$refs.regionRef.$el;
const tagContainer = regionElement.querySelector(".el-select__tags");
const existingExtraTag = tagContainer.querySelector(".tag-number");
if (existingExtraTag) {
existingExtraTag.remove();
}
// 将超出的el-tag隐藏
const tagsEle = tagContainer.querySelectorAll(".el-tag");
const maxlen = 3;
tagsEle.forEach((tag, index) => {
tag.style.display = index < maxlen ? "" : "none";
});
// 增加折叠+n展示
if (values.length > maxlen) {
const newTag = document.createElement("span");
newTag.className = "tag-number";
newTag.setAttribute(
"style",
"border:1px solid #ddd;padding:4px;border-radius:50%;"
);
newTag.textContent = `+${values.length - maxlen}`;
tagContainer.querySelector("span").appendChild(newTag);
}
});
},
【坑点1】:values长度小于el-tag元素的长度
然而奇怪的事情发生了
当我们删除tag时,现在values.length=4
怎么只展示2个tag了???
现在进入到今天的主题了,我们带着这个bug去埋坑
通过上面代码debugger,我发现奇怪的现象,当我增加时 tagsEle.length = values.length
,然而当我删除时tagsEle.length = values.length +1
,怎么会这样?
不信邪的我自己写一个demo,我发现无论增删 liCount.length = this.items.length
,那么问题出在 transition-group
上了
transition-group源码
先回忆一下transition-group
用法
列表过渡:那么怎么同时渲染整个列表,比如使用 v-for
?在这种场景中,使用 <transition-group>
组件列表过渡
简单来说,就是在添加、删除元素能够平滑过渡、以及其他动效
html
<div id="list-complete-demo" class="demo">
<button v-on:click="shuffle">Shuffle</button>
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list-complete" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-complete-item"> {{ item }} </span>
</transition-group>
</div>
.list-complete-item {
transition: all 1s;
display: inline-block;
margin-right: 10px;
}
.list-complete-enter, .list-complete-leave-to
opacity: 0;
transform: translateY(30px);
}
.list-complete-leave-active {
position: absolute;
}
如何实现 折叠过渡动画
这里我们想思考一下,用原生js实现一个列表,新增删除实现过渡效果
js
addItem() {
if (this.newItem.trim()) {
const newItem = {
id: this.nextId++,
text: this.newItem.trim(),
};
this.items.push(newItem);
this.newItem = "";
this.$nextTick(() => {
const list = this.$refs.list;
const lastItem = list.lastElementChild;
if (lastItem) {
lastItem.classList.add("fade-in"); // ---添加class="fade-in"
}
});
}
},
removeItem(index) {
const item = this.$refs.list.children[index];
if (item) {
item.classList.add("fade-out"); // ---添加class="fade-out"
setTimeout(() => {
this.items.splice(index, 1);
}, 300); // Match the animation duration
}
},
.list-item.fade-in {
animation: fadeIn 0.3s ease;
}
.list-item.fade-out {
animation: fadeOut 0.3s ease;
}
新增删除的逻辑如上,大概就是我们需要手动给dom增加一个class,需要注意的是在删除的时候,为了看到动画效果,我们需要增加一个setTimeout延时删除dom,这也就是为什么上面我们删除的时候,tagsEle.length = values.length +1
,而在新增的时候,两者是相等的。后面我们深入transition-group
源码去证实
transition 源码分析
可以看出在执行leave的时候,也是有setTimeout explicitLeaveDuration预期的过渡时间
js
function performLeave () {
if (!vnode.data.show) {
(el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key: any)] = vnode
}
beforeLeave && beforeLeave(el)
if (expectsCSS) {
addTransitionClass(el, leaveClass)
addTransitionClass(el, leaveActiveClass)
nextFrame(() => {
removeTransitionClass(el, leaveClass)
if (!cb.cancelled) {
addTransitionClass(el, leaveToClass)
if (!userWantsControl) {
if (isValidDuration(explicitLeaveDuration)) {
setTimeout(cb, explicitLeaveDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
}
})
}
leave && leave(el, cb)
if (!expectsCSS && !userWantsControl) {
cb()
}
}
【坑点2】: what? tagsEle.length +1 = values.length
???
当我改了上面的进行测试,诡异的事情又又出现了,前面我们分析 tagsEle.length > values.length
tagsEle长度总是大于 values的长度,然而在一次测试时,我发现在 出现了tagsEle.length +1 = values.length
,百折不挠的我肯定要搞清楚原因的
经过debugger我发现,values存在为空的情况,大概就是历史数据出问题了,我用的el-element版本是 《2.13.1》,大概原因就是 transition-group 里面的元素都需要一个key属性,当key属性为空是不渲染的,如下的代码只渲染 666
html
<transition-group>
<p key="666">666</p>
<p>777</p>
</transition-group>
【坑点3】:el-popper又抽风了?
当el-select数量太多,为了方便用户看到所有选项,最好弄个悬浮展示所有选项,效果如下:
这时候出现了,
el-select filterable
无法输入搜索了
html
<el-form-item label="活动区域">
<el-popover ref="popover" placement="top-start" width="500" trigger="focus" :content="form.region.join(';')">
</el-popover>
<el-select
ref="regionRef"
v-popover:popover
@change="regionChange"
v-model="form.region"
multiple
filterable
placeholder="请选择"
>
<el-option v-for="item in options" :key="item.label" :label="item.label" :value="item.label"> </el-option>
</el-select>
</el-form-item>
这时候我们在 el-select 外层包裹一层div,将v-popover:popover属性放到div上面,便可以解决这个问题
要想知道为什么,就需要查看 github element源码下packages/popover源码
js
mounted() {
let reference = (this.referenceElm = this.reference || this.$refs.reference);
const popper = this.popper || this.$refs.popper;
...
// 可访问性
if (reference) {
addClass(reference, "el-popover__reference");
...
if (this.trigger !== "click") {
on(reference, "focusin", () => {
this.handleFocus();
const instance = reference.__vue__;
if (instance && typeof instance.focus === "function") {
instance.focus();
}
});
on(popper, "focusin", this.handleFocus);
on(reference, "focusout", this.handleBlur);
on(popper, "focusout", this.handleBlur);
}
on(reference, "keydown", this.handleKeydown);
on(reference, "click", this.handleClick);
}
...
}
下面的reference就是我们的el-select组件,可以看出当trigger !== "click"
时,会给reference增加focus事件,然后执行第14行的 instance.focus
去聚焦,然而我们看el-select文档,的确有focus方法,但是为什么没生效呢?
我们回到el-select源码,可以看出上面的 instance.focus
是让红色框区域的input聚焦,而在多选时,想要输入是需要蓝色区域的input聚焦。
大家也可以做一个el-select的多选demo,可以看出下面的方法无法使之focus
js
this.$refs.regionRef.focus()
总结
我自己曾经也拿着element-ui源码调试,除非自己动手从0到1实现一次,里面的细节真的很难深入了解。 但是在业余时间去做这件事,真的需要很强的毅力,我是没有坚持下来。
在如今 react、vue、组件库
升级换代,对我们的要求如下:
- 基础:掌握react、vue最新的语法用法
- 进阶:掌握
react、vue、组件库
实现原理 - 深入:自己动手调试、实现
我觉得要想不被AI替代,至少要做到进阶(掌握react、vue、组件库
实现原理)吧。
事出反常必有妖,在一次一次踩坑并爬坑中,我们的能力才能逐步提高。
欢迎关注我的前端自检清单,我和你一起成长