前言
在使用ElementUI过程中,表格的使用占了很大一部分,但是使用起来总感觉不方便,而且部分想要的功能也没有。这促使我在ElementUI 表格组件上再进行一次封装。在这基础上添加所需功能,并且要使用更方便,但是不能破坏原有功能。
此项目已经上传GitHub,欢迎交流。希望能给个star鼓励一下,谢谢-。
在线Demo
效果
主要内容
- 数据化结构(减少html代码)
- 可编辑单元格(同时满足不同编辑形式)
- 自定义显示单元格内容 (el-table-column作用域插槽的数据结构版)
- 自定义下拉筛选(获取远程下拉筛选条件,自定义下拉筛选形式)
- 选择数据简单化(单击选择/取消,快捷多选,连续多选)
- 保证原有功能
因为代码太多所以没有全部放,只抽出部分讲解,感兴趣的小伙伴可以到GitHub下载一份源码。
数据化结构
将原有的html结构变为数据结构,只用写一个数组传入就可生成想要的结构
- 原来
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="150"></el-table-column>
<!-- 多级表头 -->
<el-table-column label="配送信息">
<el-table-column prop="name" label="姓名" width="120"></el-table-column>
<el-table-column label="地址">
<el-table-column prop="province" label="省份" width="120"></el-table-column>
<el-table-column prop="city" label="市区" width="120"></el-table-column>
<el-table-column prop="address" label="地址" width="300"></el-table-column>
<el-table-column prop="zip" label="邮编" width="120"></el-table-column>
</el-table-column>
</el-table-column>
</el-table>
</el-table>
- 现在
//html e-table为封装好的组件名
<e-table :data="tableDate" :columns="columns"></e-table>
//数据
columns: [
{
prop: "date",
label: "日期",
width: 150
},
{
label: "配送信息",
childrens: [
{
label: "地址",
childrens: [
{
label: "省份",
prop: "province",
with:120
},
{
label: "市区",
prop: "city"
}
...
]
}
]
}
]
这样数据化之后可以更方便操作,结构也更清晰。每一个el-table-column
对应columns
数组中的一个元素,可以嵌套实现多级表头。元素里面可以包含所有el-table-column
上的属性。
实现方式
通过render
函数循环嵌套生成,也可以写.vue
的单文件用html结构生成,但是没有render
函数生成可操控性高,render
函数可以更加精确的操控元素,也更加贴近编译器。还没用过的同学赶快学习一下把 render
// render 函数部分代码
render(h){
let _this = this,
columnRender = function(col,h){
if(!hasOwn(col, 'childrens')){
.... // 主要内容 单独讲
}else{
if (Array.isArray(col.childrens) && col.childrens.length > 0) {
return h('el-table-column', {
attrs: {
label: col.label || col.prop
}
}, [...col.childrens.map(function (column) { //递归
return columnRender(column, h)
})])
}
return null;
}
}
return h('el-table', {
ref: 'elTable',
props: {
...this.$attrs, //传递所有绑定的属性
// 添加功能需要用到属性,覆盖,并将这些属性作为props接受,然后再在内部添加,保证功能不缺失
rowStyle: this.rowStyle_,
cellClassName: this.cellClassName_,
rowClassName: this.rowClassName_,
},
on: {
...this.$listeners, // 传递监听的所有事件
...this.eListeners // 添加功能所需,同理 在内部再添加该事件,保证不缺失功能
},
scopedSlots: { //保留其具名插槽
empty: function () {
return _this.$slots.empty
},
append: function () {
return _this.$slots.append
}
},
}, [ this.columns.map(function (col) { // 渲染转递过来的columns数组
return columnRender(col, h) //循环递归column 单独抽离为一个函数(定义在上面)
}),
this.$slots.default // 保留默认的html写法
])
}
可编辑单元格
网上很多的ElementUI表格可编辑表格教程都是一整行的切换编辑状态,这不是我想要的。应该是每个单元格都可以控制编辑状态
//添加编辑功能 所添加的属性
columns:[
{
prop:'name',
label:'姓名',
edit:true, //该列开启可编辑模式
editType:'selection', //选择编辑形式,默认default,即input框 可以不写该属性
editControl:function(value,row,column){return true} ,//更精确控制该列每个单元格是否可编辑
editComponent:customCompontent,//可以传入自定义编辑组件
editAttrs:{size:'mini',...},// 可以传入属性
editListeners: { focus:function(){},...} //可接受事件
}
]
实现方式
// 主要代码
if (!hasOwn(col, 'childrens')) {
...
return h('el-table-column',{
props: {
...col //赋予属性
},
scopedSlots:{
default:function (props) { //改写默认内容
let {
row,
column
} = props;
let funControl = isFunction(col.editControl) ? col.editControl.call(null, row[col.prop], row, column) : true, //控制每个单元格的可编辑性
isCan = (_this.tableConfig.cellEdit !== false && col.edit && funControl) ? true : false;
isCan ? _this.setEditMap({ //生成可编辑表格的具体位置集合 点击单元格时匹配,显示编辑框
x: row.rowIndex,
y: column.property
}) : null;
if (isCan && _this.editX === row.rowIndex && _this.editY === column.property){//点击单元格满足条件显示编辑框
let options = { // .... props 和 listeners 的绑定
attrs: {
...col.editAttrs
},
props: { //给编辑组件传递数据
value_: row[col.prop],
column: column,
columnObj: col,
row: row
},
on: {
...col.editListeners,
setValue: (v) => { //改变单元格原数据,自定义编辑组件也应提供这个事件来改变原数据
row[col.prop] = v
},
change: (v) => { //覆盖change事件,转移到table主体事件
_this.$emit('cell-value-change', v, row, column, col)
}
},
if (col.editComponent && typeof col.editComponent === 'object') { //使用自定义编辑组件
return h(col.editComponent, options)
} else { //使用内部自带编辑组件
return h(editComponents[col.editType || 'default'], options)
}
} else {
//... 默认内容
}
}
}
})
}
自定义单元格显示
在使用html结构时可以使用作用域插槽进行自行自定义显示,而现在数据化结构后只能使用使用数据化方式,最为合理且唯一的应该就是调用render
函数渲染自定义内容了。(如果写成字符串解析成html,有很多问题,无法使用组件,无法绑定事件...)
columns:[
{
prop:'render',
label:'自定义显示',
renderCell:function(h,{value,row,column}){ //自定义渲染内容
return h('el-button',
{
attrs:{size:'mini'},
on:{
click:e=>{...},
...
}
...
},'自定义')
} //自定义渲染内容
formatter:function(value){return `<b>${value}</b>`}//格式化内容,可写入html字符串
}
]
实现方式
//主要代码 接可编辑代码
if (isCan && _this.editX === row.rowIndex && _this.editY === column.property){
....
}else{
if (col.renderCell && typeof col.renderCell === 'function') { //显示自定义内容
return col.renderCell.call(_this._renderProxy, h, { //_renderPeroxy Vue实例内置属性 实现render功能
value: row[col.prop],
row: row,
column: column,
})
} else { //显示默认内容
return h('span', {
domProps: { //将内容作为html解析
innerHTML: (col.formatter && typeof col.formatter === 'function') ?
col.formatter(row[col.prop]) : row[col.prop]
}
})
}
}
自定义下拉筛选
ElementUI 表格自带自由两种下拉筛选形式的选择而且需要先给定值,不能满足下拉筛选数据异步从服务器拿数据的需求。所以为了保留原来自带的下拉功能,就要将这两种方式区别开来。添加defaultHeader
属性,为true则使用默认列表头,否则使用自定义表头。
//<e-table :columns="columns" :getFilters="getFilters"></e-table>
getFilters:function(){ //总的获取下拉数据异步函数 需返回Promise.resolve(data)
return new Promse((resolve,reject)=>{
req('/api/getFilters').then(res=>{
resolve(res.data)
})
})
},
columns:[
{
prop:'filter',
label:'自定义下拉筛选',
defaultHeader:false, // 默认为false 为true时自定义筛选无用,显示为默认列表头 添加filters数组属性则使用默认表格下拉筛选
filter:true, //开启过滤
filterType:'selection', //内置下拉筛选类型 默认selection 多选
filterComponent:customCp //自定义下拉筛选组件
getFilters:function(){ //可控制获取每个列的下拉筛选数据
return new Promise(....)
},
filterAttrs:{ //传递属性
size:'mini',
...
},
filterListeners:{ //绑定事件
change:function(){}
...
},
}
]
实现方式
因为下拉内容为独立出来的部分,所以使用el-popover
组件组为载体,显示下拉筛选内容,将popover单独封装为组件当点击下拉筛选按钮时再实例化显示它。
// 点击按钮时主要代码
async headFilterBtnClick(columnObj, column, event) {
let colKey = column.columnKey || column.property || column.id;
if (this.filterLoads.some(fd => fd === colKey)) return; //已在loading状态点击无效
const target = event.target;
let cell = target.tagName === 'I' ? target : target.parentNode,
filterPanel = this.filterPanels[colKey],
filtersData = [];
cell = cell.querySelector('.e-filter-tag') || cell;
if (filterPanel && this.headFCNs.some(f => f === colKey)) { // 已经存在过滤面板且已打开面板
filterPanel.doClose()
return
}
this.filterLoads.push(colKey) //显示loading
try { //await异步获取过滤数据时 捕获异常
if (columnObj.getFilters && typeof columnObj.getFilters === 'function') {
filtersData = (await columnObj.getFilters(columnObj, column)) || [] //每一列可单独异步获取下拉数据
} else if (this.getFilters) {
filtersData = (await this.getFilters(columnObj, column)) || [] //下拉数据获取
}
} catch (error) {
this.filterLoads.splice(this.filterLoads.findIndex(fd => fd === colKey), 1)
throw new Error(error)
return
}
if (filterPanel) { //存在但当前未打开
this.filters = filtersData;
filterPanel.filtedList = this.filtedList;
filterPanel.filters = filtersData;
this.filterLoads.splice(this.filterLoads.findIndex(fd => fd === colKey), 1);
filterPanel.doShow();
return
}
if (!filterPanel) { //不存在过滤面板
filterPanel = new Vue(FilterPanel) //实例化popover组件
this.filterPanels[colKey] = filterPanel //将每个下拉的popover保存到table内部方便操作
filterPanel.reference = cell //popover显示依赖
filterPanel.columnId = colKey //传递数据
filterPanel.column = column
filterPanel.columnObj = columnObj
filterPanel.table = this._self
filterPanel.filters = filtersData,
filterPanel.filtedList = this.filtedList,
filterPanel.$mount(document.createElement('div')); //挂载
this.filterLoads.splice(this.filterLoads.findIndex(fd => fd === colKey), 1)
filterPanel.doShow() //显示组件
}
},
- filterPanel组件组要代码
render(h) {
let _this = this
return h('el-popover', {
props: {
reference: this.reference,
},
on: {
hide: this.hide,
show: this.show,
},
ref: "filterPane",
scopedSlots: {
default: function (props) {
let options = {
attrs: {
..._this.columnObj.filterAttrs //传递属性
},
props: {
filters: _this.filters,
filtedList: _this.filtedList,
column: _this.column,
columnObj: _this.columnObj,
showPopper: _this.showPopper
},
on: {
..._this.columnObj.filterListeners, //传递事件
filterChange: _this.filterChange //数据改变时触发
}
};
if (_this.columnObj.filterComponent && typeof _this.columnObj.filterComponent === 'object') { //自定义下拉筛选组件
return h(_this.columnObj.filterComponent, options)
}
//内置下拉筛选组件
return h(filterComponents[_this.columnObj.filterType || 'selection'], options)
}
}
})
//主要事件,将数据传给table
filterChange(value, columnObj, column) {
this.table.filterChange(value, columnObj, column)
this.doClose() //关闭面板
},
选择数据简单化
这个内容在之前已经单独讲过,Element UI 表格点击选中行/取消选中 快捷多选 以及快捷连续多选,高亮选中行
结语
修改这些功能主要靠render
函数的特性,虽然结构看起来不宜读(可采用jsx语法),但是能够更好的完成需求,也算将功补过吧,总的来说完成的还算比较满意。如果有什么建议,或则有什么更好的方式完成这些需求,欢迎交流。