Table: add header group feature. (#1312)

pull/1314/head
FuryBean 2016-11-23 20:32:23 +08:00 committed by cinwell.li
parent c3aba68ecc
commit 2f3f5eabc1
7 changed files with 476 additions and 48 deletions

View File

@ -683,6 +683,115 @@ When you have huge chunks of data to put in a table, you can fix the header and
```
:::
### Grouping table head
When the data structure is complex, you can use group header to show the data hierarchy.
:::demo Only need to place el-table-column inside a el-table-column, you can achieve group header.
```html
<template>
<el-table
:data="tableData3"
border
style="width: 100%">
<el-table-column
prop="date"
label="Date"
width="150">
</el-table-column>
<el-table-column label="Delivery Info">
<el-table-column
prop="name"
label="Name"
width="120">
</el-table-column>
<el-table-column label="Address Info">
<el-table-column
prop="state"
label="State"
width="120">
</el-table-column>
<el-table-column
prop="city"
label="City"
width="120">
</el-table-column>
<el-table-column
prop="address"
label="Address"
width="300">
</el-table-column>
<el-table-column
prop="zip"
label="Zip"
width="120">
</el-table-column>
</el-table-column>
</el-table-column>
</el-table>
</template>
<script>
export default {
data() {
return {
tableData3: [{
date: '2016-05-03',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
}, {
date: '2016-05-02',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
}, {
date: '2016-05-04',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
}, {
date: '2016-05-01',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
}, {
date: '2016-05-08',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
}, {
date: '2016-05-06',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
}, {
date: '2016-05-07',
name: 'Tom',
state: 'California',
city: 'Los Angeles',
address: 'No. 189, Grove St, Los Angeles',
zip: 'CA 90036'
}]
}
}
}
</script>
```
:::
### Single select
Single row selection is supported.

View File

@ -689,6 +689,115 @@
```
:::
### 多级表头
数据结构比较复杂的时候,可使用多级表头来展现数据的层次关系。
:::demo 只需要在 el-table-column 里面嵌套 el-table-column就可以实现多级表头。
```html
<template>
<el-table
:data="tableData3"
border
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>
</template>
<script>
export default {
data() {
return {
tableData3: [{
date: '2016-05-03',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-02',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-04',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-01',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-08',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-06',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-07',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
}]
}
}
}
</script>
```
:::
### 单选
选择单行数据时使用色块表示。

View File

@ -126,11 +126,13 @@ export default {
}
},
render() {},
render() {
return (<div>{ this._t('default') }</div>);
},
data() {
return {
isChildColumn: false,
isSubColumn: false,
columns: []
};
},
@ -158,13 +160,15 @@ export default {
created() {
this.customRender = this.$options.render;
this.$options.render = (h) => h('div');
this.$options.render = (h) => {
return (<div>{ this._t('default') }</div>);
};
let columnId = this.columnId = (this.$parent.tableId || (this.$parent.columnId + '_')) + 'column_' + columnIdSeed++;
let parent = this.$parent;
let owner = this.owner;
this.isChildColumn = owner !== parent;
this.isSubColumn = owner !== parent;
let type = this.type;
@ -326,12 +330,12 @@ export default {
const parent = this.$parent;
let columnIndex;
if (!this.isChildColumn) {
if (!this.isSubColumn) {
columnIndex = [].indexOf.call(parent.$refs.hiddenColumns.children, this.$el);
} else {
columnIndex = [].indexOf.call(parent.$el.children, this.$el);
}
owner.store.commit('insertColumn', this.columnConfig, columnIndex);
owner.store.commit('insertColumn', this.columnConfig, columnIndex, this.isSubColumn ? parent.columnConfig : null);
}
};

View File

@ -3,10 +3,75 @@ import ElTag from 'element-ui/packages/tag';
import Vue from 'vue';
import FilterPanel from './filter-panel.vue';
const getAllColumns = (columns) => {
const result = [];
columns.forEach((column) => {
if (column.children) {
result.push(column);
result.push.apply(result, getAllColumns(column.children));
} else {
result.push(column);
}
});
return result;
};
const convertToRows = (originColumns) => {
let maxLevel = 1;
const traverse = (column, parent) => {
if (parent) {
column.level = parent.level + 1;
if (maxLevel < column.level) {
maxLevel = column.level;
}
}
if (column.children) {
let childrenMax = 1;
let colSpan = 0;
column.children.forEach((subColumn) => {
const temp = traverse(subColumn, column);
if (temp > childrenMax) {
childrenMax = temp;
}
colSpan += subColumn.colSpan;
});
column.colSpan = colSpan;
} else {
column.colSpan = 1;
}
};
originColumns.forEach((column) => {
column.level = 1;
traverse(column);
});
const rows = [];
for (let i = 0; i < maxLevel; i++) {
rows.push([]);
}
const allColumns = getAllColumns(originColumns);
allColumns.forEach((column) => {
if (!column.children) {
column.rowSpan = maxLevel - column.level + 1;
} else {
column.rowSpan = 1;
}
rows[column.level - 1].push(column);
});
return rows;
};
export default {
name: 'el-table-header',
render(h) {
const originColumns = this.store.states.originColumns;
const columnRows = convertToRows(originColumns, this.columns);
return (
<table
class="el-table__header"
@ -26,44 +91,50 @@ export default {
: ''
}
<thead>
<tr>
{
this._l(this.columns, (column, cellIndex) =>
<th
on-mousemove={ ($event) => this.handleMouseMove($event, column) }
on-mouseout={ this.handleMouseOut }
on-mousedown={ ($event) => this.handleMouseDown($event, column) }
on-click={ ($event) => this.handleClick($event, column) }
class={ [column.id, column.order, column.align, column.className || '', this.isCellHidden(cellIndex) ? 'is-hidden' : ''] }>
<div class={ ['cell', column.filteredValue && column.filteredValue.length > 0 ? 'highlight' : ''] }>
{
column.renderHeader
? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context })
: column.label
{
this._l(columnRows, (columns) =>
<tr>
{
this._l(columns, (column, cellIndex) =>
<th
colspan={ column.colSpan }
rowspan={ column.rowSpan }
on-mousemove={ ($event) => this.handleMouseMove($event, column) }
on-mouseout={ this.handleMouseOut }
on-mousedown={ ($event) => this.handleMouseDown($event, column) }
on-click={ ($event) => this.handleClick($event, column) }
class={ [column.id, column.order, column.align, column.className || '', this.isCellHidden(cellIndex) ? 'is-hidden' : '', !column.children ? 'is-leaf' : ''] }>
<div class={ ['cell', column.filteredValue && column.filteredValue.length > 0 ? 'highlight' : ''] }>
{
column.renderHeader
? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context })
: column.label
}
{
column.sortable
? <span class="caret-wrapper" on-click={ ($event) => this.handleHeaderClick($event, column) }>
<i class="sort-caret ascending"></i>
<i class="sort-caret descending"></i>
</span>
: ''
}
{
column.filterable
? <span class="el-table__column-filter-trigger" on-click={ ($event) => this.handleFilterClick($event, column) }><i class={ ['el-icon-arrow-down', column.filterOpened ? 'el-icon-arrow-up' : ''] }></i></span>
: ''
}
</div>
</th>
)
}
{
column.sortable
? <span class="caret-wrapper" on-click={ ($event) => this.handleHeaderClick($event, column) }>
<i class="sort-caret ascending"></i>
<i class="sort-caret descending"></i>
</span>
!this.fixed && this.layout.gutterWidth
? <th class="gutter" style={{ width: this.layout.scrollY ? this.layout.gutterWidth + 'px' : '0' }}></th>
: ''
}
{
column.filterable
? <span class="el-table__column-filter-trigger" on-click={ ($event) => this.handleFilterClick($event, column) }><i class={ ['el-icon-arrow-down', column.filterOpened ? 'el-icon-arrow-up' : ''] }></i></span>
: ''
}
</div>
</th>
)
}
{
!this.fixed && this.layout.gutterWidth
? <th class="gutter" style={{ width: this.layout.scrollY ? this.layout.gutterWidth + 'px' : '0' }}></th>
: ''
}
</tr>
</tr>
)
}
</thead>
</table>
);
@ -168,6 +239,7 @@ export default {
},
handleMouseDown(event, column) {
if (column.children && column.children.length > 0) return;
/* istanbul ignore if */
if (this.draggingColumn && this.border) {
this.dragging = true;
@ -234,6 +306,7 @@ export default {
},
handleMouseMove(event, column) {
if (column.children && column.children.length > 0) return;
let target = event.target;
while (target && target.tagName !== 'TH') {
target = target.parentNode;

View File

@ -52,6 +52,7 @@ const TableStore = function(table, initialState = {}) {
this.states = {
rowKey: null,
_columns: [],
originColumns: [],
columns: [],
fixedColumns: [],
rightFixedColumns: [],
@ -159,13 +160,19 @@ TableStore.prototype.mutations = {
Vue.nextTick(() => this.table.updateScrollY());
},
insertColumn(states, column, index) {
let _columns = states._columns;
if (typeof index !== 'undefined') {
_columns.splice(index, 0, column);
} else {
_columns.push(column);
insertColumn(states, column, index, parent) {
let array = states._columns;
if (parent) {
array = parent.children;
if (!array) array = parent.children = [];
}
if (typeof index !== 'undefined') {
array.splice(index, 0, column);
} else {
array.push(column);
}
if (column.type === 'selection') {
states.selectable = column.selectable;
states.reserveSelection = column.reserveSelection;
@ -236,6 +243,18 @@ TableStore.prototype.mutations = {
})
};
const doFlattenColumns = (columns) => {
const result = [];
columns.forEach((column) => {
if (column.children) {
result.push.apply(result, doFlattenColumns(column.children));
} else {
result.push(column);
}
});
return result;
};
TableStore.prototype.updateColumns = function() {
const states = this.states;
const _columns = states._columns || [];
@ -246,7 +265,8 @@ TableStore.prototype.updateColumns = function() {
_columns[0].fixed = true;
states.fixedColumns.unshift(_columns[0]);
}
states.columns = [].concat(states.fixedColumns).concat(_columns.filter((column) => !column.fixed)).concat(states.rightFixedColumns);
states.originColumns = [].concat(states.fixedColumns).concat(_columns.filter((column) => !column.fixed)).concat(states.rightFixedColumns);
states.columns = doFlattenColumns(states.originColumns);
states.isComplex = states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0;
};

View File

@ -86,7 +86,6 @@
text-overflow: ellipsis;
vertical-align: middle;
position: relative;
border-bottom: 1px solid var(--table-border-color);
@when center {
text-align: center;
@ -101,10 +100,18 @@
}
}
& th.is-leaf, td {
border-bottom: 1px solid var(--table-border-color);
}
@modifier border {
& th, td {
border-right: 1px solid var(--table-border-color);
}
& th {
border-bottom: 1px solid var(--table-border-color);
}
}
& th {

View File

@ -985,6 +985,112 @@ describe('Table', () => {
});
});
describe('multi level column', () => {
it('should works', done => {
const vm = createVue({
template: `
<el-table :data="testData">
<el-table-column prop="name" />
<el-table-column label="group">
<el-table-column prop="release"/>
<el-table-column prop="director"/>
</el-table-column>
<el-table-column prop="runtime"/>
</el-table>
`,
created() {
this.testData = null;
}
}, true);
setTimeout(_ => {
const trs = vm.$el.querySelectorAll('.el-table__header tr');
expect(trs.length).equal(2);
const firstRowHeader = trs[0].querySelectorAll('th .cell').length;
const secondRowHeader = trs[1].querySelectorAll('th .cell').length;
expect(firstRowHeader).to.equal(3);
expect(secondRowHeader).to.equal(2);
expect(trs[0].querySelector('th:first-child').getAttribute('rowspan')).to.equal('2');
expect(trs[0].querySelector('th:nth-child(2)').getAttribute('colspan')).to.equal('2');
destroyVM(vm);
done();
}, DELAY);
});
it('should works', done => {
const vm = createVue({
template: `
<el-table :data="testData">
<el-table-column prop="name" />
<el-table-column label="group">
<el-table-column label="group's group">
<el-table-column prop="release" />
<el-table-column prop="runtime"/>
</el-table-column>
<el-table-column prop="director" />
</el-table-column>
<el-table-column prop="runtime"/>
</el-table>
`,
created() {
this.testData = null;
}
}, true);
setTimeout(_ => {
const trs = vm.$el.querySelectorAll('.el-table__header tr');
expect(trs.length).equal(3);
const firstRowHeader = trs[0].querySelectorAll('th .cell').length;
const secondRowHeader = trs[1].querySelectorAll('th .cell').length;
const thirdRowHeader = trs[2].querySelectorAll('th .cell').length;
expect(firstRowHeader).to.equal(3);
expect(secondRowHeader).to.equal(2);
expect(thirdRowHeader).to.equal(2);
expect(trs[0].querySelector('th:first-child').getAttribute('rowspan')).to.equal('3');
expect(trs[0].querySelector('th:nth-child(2)').getAttribute('colspan')).to.equal('3');
expect(trs[1].querySelector('th:first-child').getAttribute('colspan')).to.equal('2');
expect(trs[1].querySelector('th:nth-child(2)').getAttribute('rowspan')).to.equal('2');
destroyVM(vm);
done();
}, DELAY);
});
it('should work in one column', done => {
const vm = createVue({
template: `
<el-table :data="testData">
<el-table-column label="group">
<el-table-column prop="release"/>
</el-table-column>
</el-table>
`,
created() {
this.testData = null;
}
}, true);
setTimeout(_ => {
const trs = vm.$el.querySelectorAll('.el-table__header tr');
expect(trs.length).equal(2);
const firstRowLength = trs[0].querySelectorAll('th .cell').length;
const secondRowLength = trs[1].querySelectorAll('th .cell').length;
expect(firstRowLength).to.equal(1);
expect(secondRowLength).to.equal(1);
expect(trs[0].querySelector('th:first-child').getAttribute('rowspan')).to.equal('1');
expect(trs[0].querySelector('th:first-child').getAttribute('colspan')).to.equal('1');
destroyVM(vm);
done();
}, DELAY);
});
});
describe('methods', () => {
const createTable = function(prop = '', opts) {
return createVue({