一个标准的表格由<table><thead><tbody><tr><th><td>等元素组成
表格组件的内容(表头和行数据)由两个props构成: columns和data, 两者都是数组, columns用来描述每列的信息,并渲染在表头<thead>内,可以指定某一列是否需要排序;data是每一行的数据,由columns决定每一行里各列的顺序
按照惯例,先初始化各个文件
<head>
<meta charset="utf-8">
<title>可排序的表格组件</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="app" v-cloak>
<v-table :data="data" :columns="columns"></v-table>
<button>添加数据</button>
</div>
<script src="https://unpkg.com/vue/dist/vue.min.js">
<script src="table.js"></script>
<script src="index.js"></script>
</body>
columns的每一项是一个对象,其中title和key字段是必填的,用来标识这列的表头标题,key是对应data中列内容的字段名。 sortable是选填字段,如果值为true,说明该列需要排序。在index.js中构造数据,比如:
index.js:
var app = new Vue({
el: '#app',
data: {
columns: [
{
title: '姓名',
key: 'name'
},
{
title: '年龄',
key: 'age',
sortable: true
},
{
title: '出生日期',
key: 'birthday',
sortable: true
},
{
title: '地址',
key: 'address'
}
],
data: [
{
name: '王小明',
age: 18,
birthday: '1992-02-20',
address: '北京'
},
{
name: '王小刚',
age: 18,
birthday: '1992-02-20',
address: '上海'
},
{
name: '王小红',
age: 18,
birthday: '1992-02-20',
address: '南京'
}
]
},
methods: {
handleAddData: function () {
// body...
this.data.push({
name: '刘晓天',
age: 19,
birthday:'1992-04-12',
address: '北京市'
});
}
}
});
在index.html里,把数据传递给组件v-table:
<v-table :data="data" :columns="columns"></v-table>
表格的最外层是table元素,里面包含了thead 表头和 tbody表格主体, thead是一行多列(一个tr,多个th),tbody是多行多列(多个tr,多个td)。 先由外至内构建出大致的DOM结构
table.js:
Vue.component('vTable', {
props: {
columns: {
type: Array,
default: function () {
return [];
}
},
data: {
type: Array,
default: function () {
return [];
}
}
},
为了让排序后的columns不影响原始数据,给v-table组件的data选项添加两个对应的数据,组件对所有对操作将在这两个数据上完成,不对原始数据做任何处理:
data () {
return {
currentColumns: [],
currentData: []
}
},
这里的h 就是createElement,只是换了个名称,表格主体 trs 是一个二维数组,数据由currentColumns 和 currentData 组成: 先遍历所有的行,然后在每行内再遍历各列,最终结合出主体内容节点trs.
// 表头的节点ths复杂一点,因为有排序的功能:
render: function(h) {
var _this = this;
var ths = [];
this.currentColumns.forEach(function(col, index) {
// 如果col.sorttable 定义了,或值为true,除了渲染title,还要加两个<a>元素来实现升序或降序的操作。
if (col.sortable) {
ths.push(h(th, [
h('span', col.title),
h('a', {
class: {
on: col._sortType === 'asc'
},
on: {
click: function () {
_this.handleSortByAsc(index)
}
}
},'↑'),
h('a', {
class: {
on: col._sortType === 'desc'
},
on: {
click: function () {
_this.handleSortByAsc(index)
}
}
},'↓')
]));
} else {
// 如果col.sorttable 没有定义,或值为false,就直接把col.title渲染出来。
ths.push(h('th', col.title));
}
});
var trs = [];
this.currentData.forEach(function(row) {
var tds = [];
_this.currentColumns.forEach(function(cell) {
tds.push(h('td', row[cell.key]));
});
trs.push(h('tr', tds));
});
return h('table', [
h('thead', [
h('tr', ths)
]),
h('tbody', trs)
])
},
// v-table组件目前的prop:columns 和 data 的数据已经从父级传递过来了,不过前面介绍过,v-table不直接使用它们,
// 而是使用data选项的 currentColumns 和 currentData。 所以在v-table初始化时,需要把columns 和 data 赋值给 currentColumns he currentData。
// 在v-table的methods选项里定义两个方法用来赋值,并在mounted 钩子内调用:
// map() 是JS数组的一个方法,根据传入的函数重新构造一个新数组。排序分升序(asc)和降序(desc),而且同时只能对一列数据进行排序,与其他列互斥,
// 为了标识当前列的排序状态,在map列添加数据时,默认给每列都添加一个_sortType的字段,而且赋值为normal,表示默认排序(也就是不排序)。
// 在排序后,currentData 每项的顺序可能都会发生变化,所以给currentColumns 和 currnetData 的每个数据都添加_index字段,代表当前数据在原始数据中的索引。
methods: {
makeColumns: function () {
this.currentColumns = this.columns.map(function (col, index) {
// 添加一个字段标识当前列排序的状态,后续使用
col._sortType = 'normal';
// 添加一个字段标识当前列在数组中的索引,后续使用
col._index = index;
return col;
})
},
makeData: function () {
this.currentData = this.data.map(function(row, index) {
// 添加一个字段标识当前行在数组中的索引,后续使用
row._index = index;
return row;
});
},
// 两个方法基本类似,一个是升序操作,一个升序操作,目的都是改变currentColumns 数组每项的顺序。
// 排序使用列JS数组的sort() 方法,这里之所以返回1 和 -1,而不直接返回a[key] < a[key] ,也就是true或false,
// 是因为在部分浏览器(比如Safari)对sort的处理不同,而1 和 -1 可以做到兼容。排序前,先将所有列的排序状态都重置为normal,
// 然后设置当前列的排序状态(asc或desc),对应到render里<a>元素到class名称on,后面会通过CSS来高亮显示出当前的排序状态。
handleSortByAsc: function (index) {
var key = this.currentColumns[index].key;
this.currentColumns.forEach(function (col) {
col._sortType = 'normal'
});
this.currentColumns[index]._sortType = 'asc';
this.currentData.sort(function (a, b) {
return a[key] > b [key] ? 1 : -1;
});
},
handleSortByDesc: function (index) {
var key = this.currentColumns[index].key;
this.currentColumns.forEach(function (col) {
col._sortType = 'normal';
});
this.currentColumns[index]._sortType = 'desc';
this.currentData.sort(function (a, b) {
return a[key] < b[key] ? 1 : -1;
})
}
},
// 当渲染完表格后,父级修改列data数据,比如增加或删除,v-table的currentData也应该更新,如果某列已经存在排序状态,更新后应该直接处理一次排序。
// 通过遍历currentColumns来找出是否按某一列进行过排序,如果有,就按照当前排序状态对更新后的数据做一次排序操作
watch: {
data: function () {
this.makeData();
var sortColumn = this.currentColumns.filter(function (col) {
return col._sortType !== 'normal';
});
if (sortColumn.length > 0 ) {
if (sortColumn[0]._sortType === 'asc') {
this.handleSortByAsc(sortColumn[0]._index);
} else {
this.handleSortByDesc(sortColumn[0]._index);
}
}
}
},
mounted() {
// v-table 初始化时调用
this.makeColumns();
this.makeData();
}
});