Element ui表格组件修改,实现可编辑单元格,自定义下拉筛选,数据化结构,快捷选择。

前言

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

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

效果

编辑-选择.gif

筛选.gif

主要内容

  • 数据化结构(减少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语法),但是能够更好的完成需求,也算将功补过吧,总的来说完成的还算比较满意。如果有什么建议,或则有什么更好的方式完成这些需求,欢迎交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • dg-table 项目介绍 项目github 基于ElementUI 2.9.2 及以上版本 开发的强大表头筛选...
    万物论阅读 11,714评论 7 30
  • 在后台管理系统、数据类产品等的设计中,表格作为一种常见的信息组织整理手段,甚至是web页面的基础设施之一,其重要性...
    停停走走UP阅读 5,593评论 3 46
  • 年轻游侠儿泪眼模糊,凄然一笑,站起身,拿木剑对准墙壁,狠狠折断。 ...
    何某凯阅读 5,351评论 0 2
  • 路,永无止境。。。 一提起路,会想到很多的名句。鲁迅的“希望是本无所谓有,无所谓无,这正如地上的路,其实地上本没有...
    挪威的森林126阅读 447评论 2 0
  • 毕淑敏曾说:你想美好吗?你就读书吧。不需要花费很多的金钱,但要花费很多的时间,不过,你真的可以修得美好的气质…… ...
    碎碎妖阅读 461评论 3 11