使用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
- 第一种思路是在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}"
获取
传入组件
- 第二种思路是在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方法
- 第三种思路是直接在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>
);
}
};