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

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

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

相关推荐
徐小黑ACG几秒前
个人blog系统 前后端分离 前端js后端go
开发语言·前端·javascript·vue.js·golang
拉不动的猪1 小时前
刷刷题39(同一组件中的不同的标签页如何实现通信)
前端·javascript·面试
拉不动的猪1 小时前
刷刷题37(vue3的优化点)
前端·javascript·面试
家里有只小肥猫2 小时前
关于新奇的css
前端·css
南雨北斗2 小时前
jquery ajax 返回TP6错误信息的调试方法
前端·后端
星星不打輰2 小时前
css的显示模式
前端·css
代码CC2 小时前
Vue.js+Element UI 登录界面开发详解【附源码】
前端·vue.js·ui·elementui
无名之逆2 小时前
Hyperlane:Rust 语言打造的 Web 后端框架新标杆
开发语言·前端·网络·网络协议·rust·github·ssl
冰夏之夜影2 小时前
【css酷炫效果】纯CSS实现悬浮弹性按钮
前端·css