使用函数式组件来完成按钮超出后自动折叠

| 前端 | 函数式组件 / 按钮组 | 3.6k | 6 分钟

假如我们拥有这么一个需求:当一组按钮超出三个时只展示两个按钮,其他的收在下拉菜单之中展示成 ... 或者 更多操作,点击下拉菜单再展示收起的按钮

糟糕的方案

通常我们的按钮组是受条件动态控制的,不同的条件下会展示不同的按钮。

<button v-if="can1">按钮1</button>
<button v-if="can2">按钮2</button>
<button v-if="can3">按钮3</button>
<button v-if="can4">按钮4</button>
<button v-if="can5">按钮5</button>

因此我们直接下面这样写不太合适:

<button v-if="can1">按钮1</button>
<button v-if="can2">按钮2</button>
<button v-if="can3">按钮3</button>
<el-dropdown>
  ...
  <el-dropdown-menu slot="dropdown">
    <el-dropdown-item> v-if="can4">按钮4</el-dropdown-item>
    <el-dropdown-item> v-if="can5">按钮5</el-dropdown-item>
  </el-dropdown-menu>
</el-dropdown>

一种 可怕 的实现是尝试通过大量的 if 逻辑来尝试枚举出所有可能场景

<button v-if="can1">按钮1</button>
<button v-if="can2">按钮2</button>
<button v-if="can3">按钮3</button>
<button v-if="can4 && (!can1 || !can2 || !can3)">按钮4</button>
<button v-if="can5 && ((!can1 && (!can2 || !can3 || !can4)) || ...)">按钮5</button>
<el-dropdown v-if="....">
  ...
  <el-dropdown-menu slot="dropdown">
    <el-dropdown-item v-if="can4">按钮4</el-dropdown-item>
    <el-dropdown-item v-if="can5">按钮5</el-dropdown-item>
  </el-dropdown-menu>
</el-dropdown>

我们也许可能会尝试通过封装 一个组件 和通过 两个计算属性 来控制按钮组的陈列:

<template>
  <button v-for="item in outBtns" @click="item.click" :key="item.id">{{item.text}}</button>
  <el-dropdown v-if="sideBtns.length">
    ...
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item v-for="item in sideBtns" @click.stop="item.click" :key="item.id">
        {{item.text}}
      </el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>    
<template>
// 为了减少对象的创建 btns 可以通过 import 导入 
const btns = [
  { text: '按钮1', click: (row) => {}, id: 1, checkShow: row => '...布尔表达式' },
  { text: '按钮2', click: (row) => {}, id: 2, checkShow: row => '...布尔表达式' },
  { text: '按钮3', click: (row) => {}, id: 3, checkShow: row => '...布尔表达式' },
  { text: '按钮4', click: (row) => {}, id: 4, checkShow: row => '...布尔表达式' },
  { text: '按钮5', click: (row) => {}, id: 5, checkShow: row => '...布尔表达式' }
]
export default {
  props: [ 'row'],
  computed: {
    showBtns () {
      return btns.filter(btn => btn.checkShow(this.row))
    },
    outBtns () {
      if (this.showBtns.length <= 3) {
        return this.showBtns
      }
      return this.showBtns.slice(0, 2)
    },
    sideBtns () {
      if (this.showBtns.length <= 3) {
        return []
      }
      return this.showBtns.slice(2)
    }
  }
}

用这种方式来实现无法做到自动兼容新增的按钮,需要手动维护 btns 的数据,不太友好。

最佳方案

我们可以使用 render函数控制slot渲染函数式组件 来实现这个需求,它即考虑了性能也保证了自动兼容后面的按钮添加,还可以感受到 render函数 的强大。

export default {
  functional: true,
  render(h, vm) {
    // 过滤掉 v-if 为 false的节点
    const nodes = vm.children.filter(e => e.tag)
    if (nodes.length <= 3) {
      return h('div', nodes)
    } else {
      const outBtns = [nodes[0], nodes[1]]
      const sideBtns = nodes.slice(2)
      return (
        <div>
          { h('div', outBtns) }
          <el-dropdown>
            <span>
              更多操作<i class="el-icon-arrow-down"></i>
            </span>
            <el-dropdown-menu slot="dropdown">
              { sideBtns.map(item => <el-dropdown-item>{ h('span', [item]) }</el-dropdown-item>) }
            </el-dropdown-menu>
          </el-dropdown>
        </div>
      )
    }
  }
}

使用方式如下:

<!-- btn-auto-side是我们封装的函数式组件 -->
<btn-auto-side>
  <button v-if="can1">按钮1</button>
  <button v-if="can2">按钮2</button>
  <button v-if="can3">按钮3</button>
  <button v-if="can4">按钮4</button>
  <button v-if="can5">按钮5</button>
</btn-auto-side>

这种方案非常友好的实现了这个需求:按钮超出后自动折叠