配置化调用Element Table组件

2,186 阅读6分钟

前言

在替换旧项目用div模拟的表格时使用的Element的table组件,然后熟练的写下(复制)如下代码:

// App.vue
<template>
    <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>
  </template>
  
  <script>
    export default {
      data() {
        return {
          tableData: [{
            id: 11,
            date: '2016-05-02',
            name: '李小明',
            address: '北京市东城区长安街 11 号'
          }, {
            id: 22,
            date: '2016-05-04',
            name: '李小明',
            address: '北京市东城区长安街 22 号'
          }]
        }
      }
    }
  </script>

业务中表格需要展示的字段经常会超过10个,这样子就不得不重复写(复制)el-table-column显得非常low(累),所以在这里对Element的Table组件做二次封装,想起之前使用iview时在表头配置中支持自定义每一列的单元格中的内容,所以就按这个思路来做。

组件结构

Vue提供$listeners$attrs这两个api非常适合用来二次封装组件,分别是获取父级组件绑定的事件 (不含 .native 修饰器的) 和属性(class 和 style 除外),这样子就不用一个个去加props和在methods定义方法emit事件了。

// index.vue
<template>
  <div>
    <el-table :data="data" v-bind="tableOptions" v-on="$listeners" ref="table">
      <!-- 注:这里使用v-bind动态绑定属性,不会忽略style和class -->
      <el-table-column
        v-for="(header, headerIndex) of tableHeader"
        :key="headerIndex"
        v-bind="header.props"
      >
        <template slot="header" slot-scope="scope">
          <b-render
            v-if="header.renderHeader"
            :render="header.renderHeader"
            :row-index="scope.$index"
            :column-data="scope.column"
            type="header"
          >
          </b-render>
          <span v-else>{{ scope.column.label }}</span>
        </template>
        <template slot-scope="scope">
          <b-render
            v-if="header.renderCell"
            :render="header.renderCell"
            :row-data="scope.row"
            :row-index="scope.$index"
          >
          </b-render>
          <span v-else>{{ scope.row[header.props.prop] }}</span>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
import { Table, TableColumn } from "element-ui";
import render from "./render.js";
export default {
  props: {
    data: {
      type: Array,
      default() {
        return [];
      }
    },
    tableOptions: {
      type: Object,
      default() {
        return {};
      }
    },
    tableHeader: {
      type: Array,
      default() {
        return [];
      }
    }
  },
  components: {
    ElTable: Table,
    ElTableColumn: TableColumn,
    bRender: render
  },
  methods: {
    getTable() {
      return this.$refs["table"];
    }
  }
};
</script>

使用函数式组件渲染表格列的render方法

关于函数式组件请参考 官方文档

// render.js
export default {
  functional: true,
  props: {
    // 这里的render函数是调用方在tableHeader里定义的
    render: {
      type: Function,
      defalut: null
    },
    rowData: {
      type: Object,
      defalut() {
        return {};
      }
    },
    rowIndex: {
      type: [String, Number],
      defalut: -1
    },
    columnData: {
      type: Object,
      default() {
        return {};
      }
    },
    type: {
      type: [String],
      default: "cell",
      validator(value) {
        return ["cell", "header"].includes(value);
      }
    }
  },
  render(createElement, context) {
    let {
      render,
      emptyNodes,
      type,
      rowData,
      columnData,
      rowIndex
    } = context.props;
    //  render函数的返回值必须是调用createElement返回的结果
    if (type === "cell") { // 渲染单元格内容
      return render(createElement, rowData, rowIndex);
    } else { // 渲染列头
      return render(createElement, columnData, rowIndex);
    }
  }
};

组件支持单选/多选

Element的表格组件是通过添加一列并且设置type为selection的用法实现多选,同理我们也可以适配一下

// App.vue
<base-table
  ref="table"
  :data="tableData"
  :table-header="tableHeader"
  :table-options="tableOptions"
>
  <template v-slot:column>
    <el-table-column type="selection"></el-table-column>
  </template>
</base-table>

// index.vue
// ...
<slot name="column"></slot>

虽然这样子也能实现效果,但是既然是配置化调用,那么也应该把功能写到组件中去

// index.vue
<template>
  // ...
  <!-- 单选/多选 -->
  <el-table-column
    v-if="hasDefinedSelection && isSelection"
    type="selection"
    width="55"
  ></el-table-column>
  <el-table-column
    v-else-if="hasDefinedSelection"
    label="#"
    align="center"
    width="55"
  >
    <template slot-scope="scope">
      <el-radio 
        :value="(currentSelectRow && currentSelectRow.id) || ''" 
        :label="scope.row.id"
        @input="handleSelectRow(scope.row)"
      ></el-radio>
    </template>
  </el-table-column>
  // ...
</template>

<script>
export default {
  props: {
    // ...
    // 父组件未传时默认值为undefined,表示不需要选择
    isSelection: {
      type: Boolean,
      default: undefined
    }
  },
  data() {
    return {
      currentSelectRow: undefined
    };
  },
  computed: {
    // 是否传入isSelection
    hasDefinedSelection() {
      return typeof this.isSelection !== "undefined";
    }
  },
  methods: {
    handleSelectRow(row) {
        if (this.hasDefinedSelection && this.isSelection === false) {
          this.currentSelectRow = row
          this.table.setCurrentRow(row)
        }
    }
  }
};
</script>

<style lang="scss" scoped>
.container /deep/ .el-radio__label {
  display: none;
}
</style>

然后在父组件绑上is-selection就可以实现单选和多选了

使用

到这里就完成了组件的基本功能开发,我们来调用一下看看效果

// App.vue
<template>
  <div id="app">
    <base-table
      ref="table"
      :data="tableData"
      :table-header="tableHeader"
      :table-options="tableOptions"
      is-selection
    >
    </base-table>
  </div>
</template>

<script>
import BaseTable from "@/components/BaseTable/index.vue";
import dayjs from "dayjs";
export default {
  name: "App",
  components: {
    BaseTable
  },
  data() {
    return {
      tableData: [
        {
          id: 11,
          date: "2016-05-02",
          name: "李小明",
          address: "北京市东城区长安街 11 号"
        },
        {
          id: 22,
          date: "2016-05-04",
          name: "李小明",
          address: "北京市东城区长安街 22 号"
        }
      ],
      tableOptions: {
        border: true,
        stripe: true,
        style: {
          border: '1px solid red'
        },
        // 这里用的是scss, 在style中要加 /deep/ 才生效
        class: 'table-container'
      },
      tableHeader: [
        {
          // el-table-column组件的props
          props: {
            prop: "date",
            label: "日期"
          },
          // 自定义表头函数,非必填
          renderHeader(createElement, columnData, rowIndex) {
            const { label } = columnData;
            return createElement("span", { style: { color: "red" } }, label);
          },
          // 自定义date列内容函数,非必填
          renderCell(createElement, rowData, rowIndex) {
            let { date } = rowData;
            return createElement("span", dayjs(date).format("YYYY年MM月DD日"));
          }
        },
        {
          props: {
            prop: "name",
            label: "姓名"
          }
        },
        {
          props: {
            prop: "address",
            label: "地址"
          }
        }
      ]
    };
  }
};
</script>

最终效果:

image

slot处理

在Element的表格组件中支持empty的slot,在这里也实现下

// App.vue
<base-table
  ref="table"
  :data="[]"
  :table-header="tableHeader"
  :table-options="tableOptions"
>
  <div slot="empty" style="color: green;">自定义的暂无数据</div>
</base-table>

// index.vue
<b-render 
  v-if="$slots.empty" 
  :empty-nodes="$slots.empty"
>
</b-render>

// render.js
props: {
    // ...
    emptyNodes: {
      type: Array,
      default() {
        return undefined;
      }
    }
}
render(createElement, context) {
    // ...
    if (emptyNodes) {
      return createElement("template", { slot: "empty" }, emptyNodes);
    }
}

使用jsx优化createElement

接下来我们让vue组件支持jsx写法,关于什么是createElement参考官方文档 渲染函数 & JSX,如果项目使用的是@vue/cli脚手架搭建的可以跳过安装步骤直接使用,否则会报错vue jsx Duplicate declaration "h"

  • 安装Babel插件
npm install \ 
  @vue/babel-preset-jsx \
  @vue/babel-helper-vue-jsx-merge-props \
  --save-dev
  • 修改.babelrc
{
  "presets": ["@vue/babel-preset-jsx"]
}

然后重新改造下renderCell方法

// 自定义date列内容函数,非必填
renderCell(h, rowData, rowIndex) {
    let { date } = rowData;
    return (
        <span>{dayjs(date).format("YYYY年MM月DD日")}</span>
    );
}

当然也支持与其他Element组件组合使用

// main.js
import { 
    Button, 
    Dropdown, 
    DropdownMenu, 
    DropdownItem
} from "element-ui";

Vue.use(Button)
	.use(Dropdown)
	.use(DropdownMenu)
	.use(DropdownItem);
  
// App.vue
tableHeader: {
    // ...
    {
      props: {
        label: "操作"
      },
      renderCell(h, rowData, rowIndex) {
        // 注意这里this的指向不是vue实例
        return (
          <div>
            <el-button type="primary" vOn:click={self.handleEdit}>
              编辑
            </el-button>
            <el-button type="danger">删除</el-button>
            <el-dropdown style="margin-left: 10px">
              <el-button type="primary">
                更多操作
                <i class="el-icon-arrow-down" />
              </el-button>
              <el-dropdown-menu slot="dropdown">
                <el-dropdown-item>操作1</el-dropdown-item>
                <el-dropdown-item>操作2</el-dropdown-item>
                <el-dropdown-item>操作3</el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </div>
        );
      }
    }
}

最终效果:

image

更多用法:github.com/vuejs/jsx

结论

组件代码不多,主要的核心思想是通过函数式组件辅助渲染父组件传递过来的自定义render,这里也只是完成了基础简易版,满足日常的使用,随着业务的深入也会逐步完善这个组件。

通过这种二次封装的方法最大程度上保留了原来Element表格组件的使用方法,然后可以基于此添加一些公司上的表格处理业务逻辑。