优雅使用el-table组件

9,925 阅读3分钟

使用Vue业务开发中碰到最多的就是table,项目使用的是Vue+Element-ui, 正常写法如下,尝试过写过snippet每次快速复制粘贴,渐渐地有点写吐了,感谢网上的分享优雅的使用 element-ui 中的 table 组件,在此基础上进行了一些改进,如有写的不对的地方希望得到各位大佬指出,万分感谢。

<el-table
  :data="tableData"
  style="width: 100%">
  <el-table-column
    prop="date"
    label="日期"
    width="180">
  </el-table-column>
  <el-table-column
    prop="name"
    label="姓名"
    width="180">
  </el-table-column>
  <el-table-column
    prop="address"
    label="地址">
  </el-table-column>
</el-table>

之前使用过ant-desgin,根据此设计table的思路可以想象出传入两个基本属性,column以及data。 在页面中如果能这样写入<custom-table :column="column" data="data"/>这样就省去了好多重复的复制,代码也更加简洁明了了。el-table-column可以理解每项传入相应的prop,label以及el-table-column对应的属性通过遍历获得,使用jsx写法可以更加直观。

指令处理

此外对于table在未获取数据前应该处于loading状态,因此需要传入loading,对于v-loading使用jsx的语法

const directives = {
  directives: [{ name: 'loading', value: this.loading }]
};

事件处理

  • 此外可能会需要一些点击行信息的操作,传入一些事件方法,jsx中的语法
const listeners = {
  on: {
    ['row-click']: (row, column, event) =>
      this.$emit('row-click', row, column, event)
  }
};

额外props、attrs处理

对于额外的一些属性以及props, vue的组件实例中的$props$attrs给我们提供了很大的便利,可以通过vm.$props以及vm.$attrs传入

ps: $attrs 包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件。

单元格可操作处理

正常业务可能对于某些单元格需要传入输入框

传入slot

  1. 第一种思路是在column中传入slot属性{ slot: 'opt', label: '操作', align: 'center' }
<el-table-column
  prop="date"
  label="日期"
  width="180">
  <template #opt>
    <el-input />
  </template>
</el-table-column>

slot在更高版本的vue中主要使用v-slot,具体对于slot的一些理解可以参考这篇文章[Vue] slot详解,slot、slot-scope和v-slot,内部的主要实现其实将其编译成了

slot = {
  default: [VNode],
  opt: [VNode]
}

在组件下插入元素其实都加入了默认default的键值对中

假如使用template,内部会将其编译成

scopedSlots: _vm._u([
  {
    key: "header",
    fn: function() {
      return [_c("div", [_vm._v("具名插槽header")])]
    },
    proxy: true
  }
])

加入需要回传数据只需要在内部传入对应行的数据vm.$scopedSlots[slotName](props),这样在外层只需要在template中通过#[slotName]="{row}"获取

传入组件

  1. 第二种思路是在column中传入component属性
component: {
  name: 'email',
  render() {
    return <div>自定义组件</div>;
  }
}

vue组件其实就是一个包含name和render方法的对象,在创造虚拟节点的时候会进行相应的判断

if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor);
}

其实就是通过Vue.extend将sub指向指向super父类的原型,将父类的option合并到子类,将父类的方法加入到子类中

传入render方法

  1. 第三种思路是直接在column中传入render方法
render(h, { row }) {
  return <div>自定义组件</div>;
}

其实是对于第一种方法的改良,外部写的render方法默认不会进行转换,因为vue中是通过在render方法中第一个参数传入createElement方法进行转换成虚拟dom的,所以传入列中的render方法内传入createElement以及想要回调出去的当前行信息,这样就可以在外部数据里进行正常操作,而不用移步到template中进行修改,这种方法写着写着越来越和react相似了,所以可以完全去掉template,将文件转换成.js文件,通过将对象暴露出去。

这三种方式说白了其实将子组件放入父组件下,可以归纳为就是对于scopedSlot的操作,因此可以统一对其内部进行操作

const scopedSlots =
  column.slot || column.render || column.component
    ? {
        scopedSlots: {
          default(scope) {
            const exportVal = {
              column: scope.column,
              $index: scope.$index,
              row: scope.row
            };
            if (column.slot) {
              return self.$scopedSlots[column.slot](exportVal);
            } else if (column.render) {
              return column.render(h, exportVal);
            } else if (column.component) {
              return <column.component />;
            }
          }
        }
      }
    : {};

最终效果如下

之后在页面中只需要引入<custom-table />就可以

一些思考

对于一些合并多行单元格的方法也进行了相应的实现,对于多列单元格的合并处理起来感觉很麻烦,使用原生table进行了实现,在后续会进行分享。

最终代码

/* eslint-disable indent */
export default {
  name: 'customTable',
  props: {
    data: {
      type: Array,
      default: () => [
        {
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄',
          email: '2222'
        },
        {
          date: '2016-05-04',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1517 弄',
          email: 'xxxx'
        },
        {
          date: '2016-05-06',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1517 弄',
          email: 'dddd'
        }
      ]
    },
    columns: {
      type: Array,
      default: () => [
        { prop: 'date', label: '日期', align: 'center' },
        { prop: 'name', label: '姓名', align: 'center' },
        { prop: 'address', label: '地址', align: 'center' },
        {
          prop: 'email',
          label: '邮箱',
          align: 'center',
          component: {
            name: 'email',
            props: ['column'],
            render() {
              return <div>自定义组件</div>;
            }
          }
        }
        // // 模版中的元素需要对应的有 slot="opt" 属性
        // { slot: 'opt1', label: '输入', align: 'center' },
        // { slot: 'opt', label: '操作', align: 'center' }
      ]
    },
    loading: {
      type: Boolean,
      default: false
    },
    spanMethod: {
      type: Function,
      default: () => {}
    },
    showSummary: {
      type: Boolean,
      default: false
    },
    summaryMethod: {
      type: Function,
      default: () => {}
    }
  },
  methods: {
    getAttrs(h, column) {
      const self = this;
      const scopedSlots =
        column.slot || column.render || column.component
          ? {
              scopedSlots: {
                default(scope) {
                  const exportVal = {
                    column: scope.column,
                    $index: scope.$index,
                    row: scope.row
                  };
                  if (column.slot) {
                    return self.$scopedSlots[column.slot](exportVal);
                  } else if (column.render) {
                    return column.render(h, exportVal);
                  } else if (column.component) {
                    return h(column.component);
                  }
                }
              }
            }
          : {};

      return {
        ...scopedSlots,
        attrs: {
          ...column,
          'show-overflow-tooltip': true,
          align: column.align || 'center'
        }
      };
    }
  },
  render(h) {
    const directives = {
      directives: [{ name: 'loading', value: this.loading }]
    };
    const listeners = {
      on: {
        ['row-click']: (row, column, event) =>
          this.$emit('row-click', row, column, event),
        ['selection-change']: row => this.$emit('selection-change', row)
      }
    };

    return (
      <el-table
        {...{ attrs: this.$attrs }}
        {...{ props: this.$props }}
        {...directives}
        {...listeners}
        class={{ 'base-table': true, 'mb-2': true }}
        ref="table"
      >
        {this.columns.map(column => {
          return (
            <el-table-column key={column.prop} {...this.getAttrs(h, column)}>
              {column.children &&
                column.children.map(c => {
                  return (
                    <el-table-column key={c.prop} {...this.getAttrs(h, c)} />
                  );
                })}
            </el-table-column>
          );
        })}
      </el-table>
    );
  }
};