基于ElementUI二次封装Table实现动态生成Column

755 阅读3分钟

个人感觉写Vue比较烦人的是写模版代码,比如Table组件,有多少列就要写多少个Column,比较繁琐。而用JS代码则可以很灵活,庆幸的是Vue是支持渲染函数的,意味着可以用JS代码去实现组件。

首先明确一下我们的封装目标

  1. 对于一般的只用作显示的列,只要指定prop和label即可自动生成。
  2. 保留原来Column的slot能力,用户能自定义显示列。
  3. 可选的控制某列是否显示。

使用和效果

先来看看最后能用这个二次封装的组件干些什么

简单数据展示

如果是单纯的数据显示,那么只要这样写

<EluiDynTable :desc="tableDesc" :data="tableData" />

然后把需要哪些列以及数据给它即可,个人感觉是非常方便的

export default {
  name: "app",
  components: {
    EluiDynTable,
  },
  data() {
    return {
      tableDesc: [
        { prop: "name", label: "名字" },
        { prop: "city", label: "城市" },
        { prop: "born", label: "出生时间", formatter: "ts" },
      ],
      tableData: [
        {
          name: "Alice",
          city: "Shanghai",
          born: 946656000000,
        },
        {
          name: "Bob",
          city: "Hongkong",
          born: 946699994000,
        },
      ],
    };
  },
};

效果如下

自定义列

如果要自定义列,比如要在最右测添加功能操作按钮,则可以这样

<EluiDynTable :desc="tableDesc" :data="tableData">
  <EluiDynColumn prop="operation">
    <span slot="header">
      自定义
    </span>
    <div slot-scope="{ row, $index }">
      <el-button type="danger" size="mini" @click="handleClick(row, $index)"
        >删除</el-button
      >
    </div>
  </EluiDynColumn>
</EluiDynTable>
export default {
  name: "app",
  components: {
    EluiDynTable,
    EluiDynColumn,
  },
  data() {
    return {
      tableDesc: [
        { prop: "name", label: "名字" },
        { prop: "city", label: "城市" },
        { prop: "born", label: "出生时间", formatter: "ts" },
        { prop: "operation", label: "操作", fixed: "right" },
      ],
      tableData: [
        {
          name: "Alice",
          city: "Shanghai",
          born: 946656000000,
        },
        {
          name: "Bob",
          city: "Hongkong",
          born: 946699994000,
        },
      ],
    };
  },
  methods: {
    handleClick(row, index) {
      /* eslint-disable no-console */
      console.log(`deleting ${row.name} at ${index}`);
    },
  },
};

效果图

控制列显示

调用组件的toggle函数,函数签名如下

toggle(prop, hidden)

prop用来指定哪一列。如果不指定hidden值(boolean),则是在显示和不显示之间来回切换,如果指定则用指定值。

实现

我们首先要给各种数据类型准备一些常用的格式化器,我开箱内置里一些,具体代码查看这里:

function formatNumber(prop) {
}

function formatArray(array, extra) {
}

function formatTimestamp(ts) {
}

function formatSecond(second) {
}

function formatterByType(prop) {
}

export function format({ formatter, prop, scope, extra }) {
}

如果内置的格式化器不能满足要求,也是可以自定义的,给formatter传递custom,然后在extra里指定一个formatter,它应该是一个函数类型,具体的函数签名是这样的

extra.formatter(prop, scope)

然后我们对ElColumn进行简单的封装,default slot会默认调用格式化器对数据进行格式化展示,而header slot则会显示label值。注意把原来的slot再暴露出去,这样在使用的时候,就可以自定义了。

<template>
  <el-table-column v-bind="$attrs">
    <template slot="header" slot-scope="h">
      <slot name="header" v-bind="h">{{ h.column.label }}</slot>
    </template>
    <template slot-scope="scope">
      <slot v-bind="scope">
        <span>{{ formatRow(scope.row[prop], scope) }}</span>
      </slot>
    </template>
  </el-table-column>
</template>
<script>
import { format } from "../utils/format";

export default {
  name: "EluiDynColumn",
  props: ["prop", "formatter", "extra"],
  methods: {
    formatRow(prop, scope) {
      return format({
        formatter: this.formatter,
        prop,
        scope,
        extra: this.extra,
      });
    },
  },
};
</script>

最后使用渲染函数,实现一个Table组件

const EluiDynTable = {
  name: "EluiDynTable",

  props: {
    data: {
      type: Array,
      default: () => [],
    },
    desc: {
      type: Array,
      default: () => [],
    },
  },
  methods: {
    toggle(prop, hidden) {
      const d = this.desc.find((e) => e.prop === prop);
      if (d) {
        if (hidden !== undefined) {
          d.hidden = !!hidden;
        } else {
          d.hidden = !d.hidden;
        }
        this.$forceUpdate();
      }
    },
  },
  render: function (h) {
    const isDynColumn = (c) =>
      c.componentOptions.Ctor.extendOptions.name === "EluiDynColumn";
    const dynColumns = (this.$slots.default || []).filter(isDynColumn);
    const keyOf = (c) => c.componentOptions.propsData.prop;
    const columnGroups = group(dynColumns, keyOf);

    const children = [];
    for (let d of this.desc) {
      if (d.hidden) {
        continue;
      }
      let child = columnGroups[d.prop];
      if (child) {
        const propKeys = Object.keys(child.componentOptions.Ctor.options.props);
        for (let k in d) {
          if (propKeys.indexOf(k) >= 0) {
            child.componentOptions.propsData[k] = d[k];
          } else {
            child.data.attrs[k] = d[k];
          }
        }
      } else {
        const propKeys = Object.keys(EluiDynColumn.props);
        const props = {};
        const attrs = {};
        for (let k in d) {
          if (propKeys.indexOf(k) >= 0) {
            props[k] = d[k];
          } else {
            attrs[k] = d[k];
          }
        }
        child = h(EluiDynColumn, {
          props,
          attrs,
        });
      }
      children.push(child);
    }
    return h(Table, { props: this.$props, attrs: this.$attrs }, children);
  },
};

可以看到的代码细节是,我只会筛选出default slot里的EluiDynColumn组件,认为这是自定义的列,然后遍历表格描述desc,如果用户有自定义组件,则使用自定义组件,否则自动生成EluiDynColumn组件。给EluiDynColumn的propsattrs赋值,因为EluiDynColumn会把attrs通过v-bind绑定到ElColumn上,因此完美保留了原来ElColumn的特性。还有一个细节就是hidden属性,这是用来控制列显示的。render函数最后返回一个ElTable的VNode即可。

前端小白,如有问题请友善指出:)

完整的代码查看这里