ElementUI 的 Dropdown 组件键盘操控异常抛出

前段时间把项目的脚手架升级到 VueCLI 5 同步也升级了一些依赖项。所以这几天都是在自测,看看是不是有一些很明显的 BUG,以及解决一些依赖升级之后的 WARN

在测试一个下拉菜单(Dropdown)时,发现了一个问题:使用方向键操控会抛出一个异常:
Uncaught TypeError: ele is undefined

查看控制台输出,对应异常抛出的位置是 resetTabindex 函数,在 ele.setAttribute('tabindex', '0') 执行时 eleundefined,所以抛出了这个异常。

那么看函数调用链看 ele 是怎么来的。
👉 是 handleItemKeyDown 函数执行传入的 this.menuItems[nextIndex]

进入调试模式之后发现整个 menuItems 数组是空数组,并没有把我在模板中渲染的 el-dropdown-item 都包含进来。


捋了一下 el-dropdown 组件的业务逻辑,发现 el-dropdown-menu 组件会在完成挂载之后(mounted)立即执行 dropdown 组件的 initDomOperation 函数。

其他的任意时机都不会再次执行 initDomOperation 方法了。所以问题就是在 el-dropdown-menu 组件完成挂载时,我在模板中渲染的 el-dropdown-item 组件都没有被 querySelectorAll 方法查询到。也就是说并没有被正确渲染在DOM树上。

确实,业务中使用 v-for 循环的数据是通过接口返回的el-dropdown-menu 组件完成挂载时接口数据是不可能及时返回,所以 querySelectorAll 获取不到任何的 el-dropdown-item 元素。

那么解决方案就很简单了:

  1. 使用 v-if 包裹 el-dropdown 或者 el-dropdown-menu 组件
  2. 或者在接口返回数据后手动调用 el-dropdown 组件的 initDomOperation 方法

最后再补充一个 Dropdown 组件使用中遇到的其他问题:

💥 在 el-dropdown 组件中使用 el-input 作为 el-dropdown-item 的内容物时,点击 el-input 元素或者回车都会导致 el-dropdown-menu 收起。

这个需求是为了长下拉菜单中,可以通过键入 keyword 过滤下拉菜单,并且可以使用回车快速选中筛选的第一项。

这个问题很简单把 el-dropdown 组件的 hide-on-click 属性置为 false 即可。

但是这样做会导致一个其他的问题:点击正常的 el-dropdown-item 元素(内容物不是 el-input 的菜单项)时,el-dropdown-menu 也不会收起了。

场景1:如果你不需要在 el-input 元素中回车可以选择第一项菜单这样的需求,那么只需要在 el-input 使用 .stop 修饰符拦截事件冒泡。

注意: 不需要将 el-dropdown 组件的 hide-on-click 属性置为 false

<el-dropdown>
  <span class="el-dropdown-link">
    下拉菜单<i class="el-icon-arrow-down el-icon--right"></i>
  </span>
  <el-dropdown-menu slot="dropdown">
    <el-dropdown-item>
      <el-input v-model="keyword" @click.native.stop="() => ({})" />
    </el-dropdown-item>
    <el-dropdown-item>黄金糕</el-dropdown-item>
    <el-dropdown-item>狮子头</el-dropdown-item>
  </el-dropdown-menu>
</el-dropdown>

场景2:和我的需求一样,期望在 el-input 中回车仍可以完成选择菜单第一项,那么就需要手动控制 el-dropdown 组件的 visible 属性。

完整的示例代码 👇

<template>
  <el-dropdown ref="elDropdown" trigger="click" :hide-on-click="false" @command="handleCommand">
    <span class="el-dropdown-link">
      点击展开菜单<i class="el-icon-arrow-down el-icon--right"></i>
    </span>
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item>
        <el-input v-model="keyword" />
      </el-dropdown-item>
      <el-dropdown-item v-for="item in displayOptionList" :key="item" :command="item">
        {{ item }}
      </el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</template>
<script>
export default {
  name: "TEST",
  data() {
    return {
      keyword: '',
      optionList: ['黄金糕', '狮子头', '螺蛳粉', '双皮奶', '蚵仔煎']
    };
  },
  computed: {
    displayOptionList() {
      if(this.keyword === '') return this.optionList
      return this.optionList.filter(str => str.includes(this.keyword))
    }
  },
  methods: {
    handleCommand(key) {
      if (key) {
        this.$refs.elDropdown.visible = false
        this.$nextTick(() => {
          this.keyword = ''
        })
        alert(`您选择的是:${key}`)
      } else {
        if (this.keyword !== '') {
          const firstOption = this.displayOptionList[0]
          if(firstOption) this.handleCommand(firstOption)
        }
      }
    }
  }
}
</script>

记得将 el-dropdown 组件的 hide-on-click 属性置为 false。如果不设置为 false,依靠 @click.native.stop="() => ({})" 来取消 el-input 元素的 click 事件冒泡。那么在 el-input 中回车并不会触发 el-dropdown 组件的 command 事件。并且因为 el-input 没有正确抛出 keydown/keyup 相关的事件,想要单独处理就会非常麻烦。el-dropdown 会注册 keydown 事件监听,并直接拦截掉事件冒泡和默认行为。


相关资源