基于 Vue.js使用Render函数 实现可排序的表格组件

1,445 阅读5分钟

一个标准的表格由<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();
}

});