说说如何基于 Vue.js 实现表格组件

我们基于 Vue.js 实现一个可根据某列进行排序的表格组件。

一个表格包含表头和数据两部分内容。因此,我们定义两个数组,columns 表示表头信息,在 <thread> 中渲染,并可在此指定某一列是否需要排序;data 表示数据。

html:

<div id="app" v-cloak>
    <v-table :data="data" :columns="columns"></v-table>
    <button @click="add">新增</button>
</div>
  • 把父组件中定义的 data 与 columns 传入 v-table 组件。

js:

Vue.component('vTable', {
    props: {
        //表头列名称
        columns: {
            type: Array,
            default: function () {
                return [];
            }
        },
        //数据
        data: {
            type: Array,
            default: function () {
                return [];
            }
        }
    },
    //为了不影响原始数据,这里定义了相应的需要操作的数据对象
    data: function () {
        return {
            currentColumns: [],
            currentData: []
        }
    },
    //render 实现方式
    render: function (createElement) {
        var that = this;

        /**
         * 创建列样式与表头
         */
        var ths = [];//<th> 标签数组
        var cols = [];//<cols> 标签数组
        this.currentColumns.forEach(function (col, index) {
            if (col.width) {//创建列样式
                cols.push(createElement('col', {
                    style: {
                        width: col.width
                    }
                }))
            }


            if (col.sortable) {
                ths.push(createElement('th', [
                    createElement('span', col.title),
                    //升序
                    createElement('a', {
                        class: {
                            on: col.sortType === 'asc'
                        },
                        on: {
                            click: function () {
                                that.sortByAsc(index)
                            }
                        }
                    }, '↑'),
                    //降序
                    createElement('a', {
                        class: {
                            on: col.sortType === 'desc'
                        },
                        on: {
                            click: function () {
                                that.sortByDesc(index);
                            }
                        }
                    }, '↓')
                ]));
            } else {
                ths.push(createElement('th', col.title));
            }
        });


        /**
         * 创建内容
         */
        var trs = [];//<tr> 标签数组
        this.currentData.forEach(function (row) {//遍历行
            var tds = [];//<td> 标签数组
            that.currentColumns.forEach(function (cell) {//遍历单元格
                tds.push(createElement('td', row[cell.key]));
            });
            trs.push(createElement('tr', tds));
        });

        return createElement('table', [
            createElement('colgroup', cols),
            createElement('thead', [
                createElement('tr', ths)
            ]),
            createElement('tbody', trs)
        ])
    },

    methods: {
        //初始化表头
        initColumns: function () {
            this.currentColumns = this.columns.map(function (col, index) {
                //新建字段,标识当前列排序类型;默认为“不排序”
                col.sortType = 'normal';
                //新建字段,标识当前列在数组中的索引
                col.index = index;
                return col;
            });
        },
        //初始化数据
        initData: function () {
            this.currentData = this.data.map(function (row, index) {
                //新建字段,标识当前行在数组中的索引
                row.index = index;
                return row;
            });
        },

        //排序
        order: function (index, type) {
            this.currentColumns.forEach(function (col) {
                col.sortType = 'normal';
            });

            //设置排序类型
            this.currentColumns[index].sortType = type;

            //设置排序函数
            var sortFunction;
            var key = this.currentColumns[index].key;
            switch (type) {
                default://默认为 asc 排序
                case 'asc':
                    sortFunction = function (a, b) {
                        return a[key] > b[key] ? 1 : -1;
                    };
                    break;
                case 'desc':
                    sortFunction = function (a, b) {
                        return a[key] < b[key] ? 1 : -1;
                    };
                    break;
            }
            this.currentData.sort(sortFunction);
        },

        //升序
        sortByAsc: function (index) {
            this.order(index, 'asc');
        },
        //降序
        sortByDesc: function (index) {
            this.order(index, 'desc');
        }
    },
    watch: {
        data: function () {
            this.initData();

            //找出排序字段
            var sortedColumn = this.currentColumns.filter(function (col) {
                return col.sortType !== 'normal';
            });

            if (sortedColumn.length > 0) {
                if (sortedColumn[0].sortType === 'asc') {
                    this.sortByAsc(sortedColumn[0].index);
                } else {
                    this.sortByDesc(sortedColumn[0].index);
                }
            }
        }
    },
    mounted() {
        this.initColumns();
        this.initData();
    }
});

var app = new Vue({
    el: '#app',
    data: {
        //title 、key 与 width 必填;sortable 选填
        columns: [
            {
                title: '名称',
                key: 'name',
                width:'60%'
            },
            {
                title: '数量',
                key: 'num',
                width:'20%',
                sortable: true
            },
            {
                title: '单价',
                key: 'unitPrice',
                width:'20%',
                sortable: true
            }
        ],
        data: [
            {
                name: '真果粒牛奶饮品',
                num: 2,
                unitPrice: 59.9
            },
            {
                name: '苏泊尔(SUPOR)电压力锅 ',
                num: 1,
                unitPrice: 378.0
            },
            {
                name: '乐事(Lay\'s)薯片',
                num: 3,
                unitPrice: 63.0
            }
        ]
    },
    methods:{
        add:function () {
            this.data.push( {
                name: '良品铺子 休闲零食大礼包',
                num: 5,
                unitPrice: 59.80
            });
        }
    }
});
  • 为了让排序后的 columns 与 data 不影响原始数据,我们在组件的 data 中定义了相应的当前数据对象。因此在 method 中使用传入的值,初始化这些数据对象,最后在 mounted() 调用这些初始化方法。
  • columns 中的每一项都是包含 title(列名)、key(对应 data 中的字段名)、width(宽度) 以及 sortable(是否可排序) 的对象。其中,只有 sortable 为可选项,如果设定为 true,则表示该列可点击排序。
  • map() 会对数组的每一项运行给定函数,返回每次函数调用的结果组成的数组。
  • 排序分为升序与降序,因为只能对某一列进行排序,所以是互斥操作。我们为每一列新增一个 sortType ,用于标识该列的排序类型,初始值为 normal,表示不排序。
  • 因为排序字段可能是任意列,所以我们为每一列新增一个 index,用于标识当前列在数组中的索引。
  • 在 Render 函数中,首先创建列样式与表头,接着创建内容。
  • Render 函数中的 createElement 可以简写为 h,这样代码会变得更简洁:
render: function (h) {
    var that = this;

    /**
     * 创建列样式与表头
     */
    var ths = [];//<th> 标签数组
    var cols = [];//<cols> 标签数组
    this.currentColumns.forEach(function (col, index) {
        if (col.width) {//创建列样式
            cols.push(h('col', {
                style: {
                    width: col.width
                }
            }))
        }


        if (col.sortable) {
            ths.push(h('th', [
                h('span', col.title),
                //升序
                h('a', {
                    class: {
                        on: col.sortType === 'asc'
                    },
                    on: {
                        click: function () {
                            that.sortByAsc(index)
                        }
                    }
                }, '↑'),
                //降序
                h('a', {
                    class: {
                        on: col.sortType === 'desc'
                    },
                    on: {
                        click: function () {
                            that.sortByDesc(index);
                        }
                    }
                }, '↓')
            ]));
        } else {
            ths.push(h('th', col.title));
        }
    });


    /**
     * 创建内容
     */
    var trs = [];//<tr> 标签数组
    this.currentData.forEach(function (row) {//遍历行
        var tds = [];//<td> 标签数组
        that.currentColumns.forEach(function (cell) {//遍历单元格
            tds.push(h('td', row[cell.key]));
        });
        trs.push(h('tr', tds));
    });

    return h('table', [
        h('colgroup', cols),
        h('thead', [
            h('tr', ths)
        ]),
        h('tbody', trs)
    ])
}
  • 创建内容时,我们首先遍历所有行,然后在循环内部遍历所有列,得出 <td><tr> 内容。
  • 创建表头时,对是否排序做了相应的处理,并绑定了相应的点击事件。
  • 点击事件定义在 methods 中,因为升序与降序逻辑大体相同,所以又封装了一层 order() 排序函数。
  • order() 排序函数内部使用了数组的 sort() 方法。sort() 方法会调用每个数组项的 toString() 方法,然后比较得到的字符串,即使数组中的每一项是数值,比较的也是字符串。这里传入了一个比较函数作为参数。为了兼容所有浏览器,在比较函数中,我们返回的是 1 或者 -1。
  • 排序之前,先把所有列的排序类型都设置为不排序,然后再更新当前列的排序状态。这就会对应到 render 函数里绑定 <a> 标签的 class 中的 on 样式,即当前列排序状态会被高亮显示。
  • 表格被初始化渲染之后,如果 data 发生变化,那么表格组件数据应该也要同步更新。因此,我们在 watch 中做了数据更新以及数据重排操作。

css:

[v-cloak] {
    display: none;
}

table {
    width: 100%;
    margin-bottom: 24px;
    /*合并边框模型*/
    border-collapse: collapse;
    border-spacing: 0;
    /*在空单元格周围绘制边框*/
    empty-cells: show;
    border: 1px solid #e9e9e9;
}

table th {
    font: bold 14px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
    background: #CAE8EA;
    color: #5c6b77;
    /*设置文本粗细*/
    font-weight: 600;
    /*段落中的文本不进行换行*/
    white-space: nowrap;
    border-top: 1px solid #C1DAD7;
}

table td, table th {
    padding: 8px 16px;
    text-align: left;
    border-right: 1px solid #C1DAD7;
    border-bottom: 1px solid #C1DAD7;
}

table th a {
    /*不独占一行的块级元素*/
    display: inline-block;
    margin: 0 4px;
    cursor: pointer;
}

table th a.on {
    color: #3399ff;
}

table th a:hover {
    color: #3399ff;
}

效果:


本文示例代码

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

推荐阅读更多精彩内容