el-select+transition-group踩坑

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、组件库 实现原理)吧。

事出反常必有妖,在一次一次踩坑并爬坑中,我们的能力才能逐步提高。

欢迎关注我的前端自检清单,我和你一起成长

相关推荐
weixin_473894771 分钟前
前端服务器部署分类总结
前端·网络·性能优化
LuckyLay19 分钟前
React百日学习计划-Grok3
前端·学习·react.js
澄江静如练_23 分钟前
小程序 存存上下滑动的页面
前端·javascript·vue.js
源码方舟30 分钟前
基于SpringBoot+Vue的房屋租赁管理系统源码包(完整版)开发实战
vue.js·spring boot·后端
互联网搬砖老肖39 分钟前
Web 架构之会话保持深度解析
前端·架构
m0_5139625343 分钟前
vue-ganttastic甘特图label标签横向滚动固定方法
javascript·vue.js·甘特图
菜鸟una1 小时前
【taro3 + vue3 + webpack4】在微信小程序中的请求封装及使用
前端·vue.js·微信小程序·小程序·typescript·taro
Java&Develop1 小时前
怎么查看当前vue项目,要求的node.js版本
vue.js
hao_04131 小时前
elpis-core: 基于 Koa 实现 web 服务引擎架构设计解析
前端
松树戈2 小时前
openfeign与dubbo调用下载excel实践
vue.js·spring cloud·elementui·dubbo