前言

在使用ElementUI过程中,表格的使用占了很大一部分,但是使用起来总感觉不方便,而且部分想要的功能也没有。这促使我在ElementUI 表格组件上再进行一次封装。在这基础上添加所需功能,并且要使用更方便,但是不能破坏原有功能。

此项目已经上传GitHub,欢迎交流,希望能给个star鼓励一下,谢谢^-^。 在线Demo

效果

编辑-选择
编辑-选择
筛选
筛选

主要内容

因为代码太多所以没有全部放,只抽出部分讲解,感兴趣的小伙伴可以到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
//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"
              }
              ...
            ]
          }
        ]
      }
    ]
jsx

这样数据化之后可以更方便操作,结构也更清晰。每一个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写法
        ]) 
}
javascript

可编辑单元格

网上很多的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(){},...} //可接受事件 
     }
]
javascript
实现方式
// 主要代码
 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 {
                                //... 默认内容
                            }
                   }
        }
      })  
 }
javascript

自定义单元格显示

在使用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字符串
    }
]
javascript
实现方式
//主要代码 接可编辑代码
 if (isCan && _this.editX === row.rowIndex && _this.editY === column.property){
     ....
 }else{
     if (col.renderCell && typeof col.renderCell === 'function') { //显示自定义内容
         return col.renderCell.call(null, h, { // 实现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]
                                        }
                                    })
                                }                           
     }
javascript

自定义下拉筛选

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(){}
            ...
        },
    }
]
javascript
实现方式

因为下拉内容为独立出来的部分,所以使用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() //显示组件
            }

        },
javascript
 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() //关闭面板
}
javascript

选择数据简单化

这个内容在之前已经单独讲过,Element UI 表格点击选中行/取消选中 快捷多选 以及快捷连续多选,高亮选中行;

结语

修改这些功能主要靠render函数的特性,虽然结构看起来不宜读(可采用jsx语法),但是能够更好的完成需求,也算将功补过吧,总的来说完成的还算比较满意。如果有什么建议,或则有什么更好的方式完成这些需求,欢迎交流。