Table: add expand row feature.

This commit is contained in:
furybean 2016-11-12 23:08:17 +08:00 committed by 杨奕
parent c968525da9
commit b569b314b8
8 changed files with 430 additions and 18 deletions

View File

@ -1354,6 +1354,94 @@ Customize table column so it can be integrated with other components.
```
:::
### Expandable row
When the row content is too long and you do not want to display the horizontal scroll bar, you can use the expandable row feature.
:::demo Activate expandable row by adding type="expand" and `inline-template` attributeThe template for `el-table-column` will be rendered as the contents of the expanded row, you can access the same attributes as the` inline-template`。
```html
<template>
<el-table
:data="tableData3"
style="width: 100%">
<el-table-column type="expand" inline-template>
<div>
<p>State: {{ row.state }}</p>
<p>City: {{ row.city }}</p>
<p>Address: {{ row.address }}</p>
<p>Zip: {{ row.zip }}</p>
</div>
</el-table-column>
<el-table-column
label="Date"
prop="date">
</el-table-column>
<el-table-column
label="Name"
prop="name">
</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>
```
:::
### Table Attributes
| Attribute | Description | Type | Accepted Values | Default |
|---------- |-------------- |---------- |-------------------------------- |-------- |
@ -1369,7 +1457,9 @@ Customize table column so it can be integrated with other components.
| row-style | function that returns custom style for a row, or a string assigning custom style for every row | Function(row, index)/Object | — | — |
| row-key | key of row data, used for optimizing rendering. Required if `reserve-selection` is on | Function(row)/String | — | — |
| context | context of Table, e.g. `_self` refers to the current context, `$parent` parent context, `$root` root context, can be overridden by `context` in `el-table-column` | Object | - | current context where Table lies |
| empty-text | Displayed text when data is empty. You can customize this area with `slot="empty"` | String | | - | No Data |
| empty-text | Displayed text when data is empty. You can customize this area with `slot="empty"` | String | - | No Data |
| default-expand-all | whether expand all rows by default, only works when the table has a column type="expand" | Boolean | - | false |
| expand-row-keys | set expanded rows by this prop, prop's value is the keys of expand rows, you should set row-key before using this prop | Array | - | |
### Table Events
| Event Name | Description | Parameters |
@ -1387,6 +1477,7 @@ Customize table column so it can be integrated with other components.
| sort-change | triggers when Table's sorting changes | { column, prop, order } |
| filter-change | column's key. If you need to use the filter-change event, this attribute is mandatory to identify which column is being filtered | filters |
| current-change | triggers when current row changes | currentRow, oldCurrentRow |
| expand | triggers when user expands or collapses a row | row, expanded |
### Table Methods
| Method | Description | Parameter |
@ -1397,7 +1488,7 @@ Customize table column so it can be integrated with other components.
### Table-column Attributes
| Attribute | Description | Type | Accepted Values | Default |
|---------- |-------------- |---------- |-------------------------------- |-------- |
| type | type of the column. If set to `selection`, the column will display checkbox. If set to `index`, the column will display index of the row (staring from 1) | string | selection/index | — |
| type | type of the column. If set to `selection`, the column will display checkbox. If set to `index`, the column will display index of the row (staring from 1). If set to `expand`, the column will display expand icon. | string | selection/index/expand | — |
| label | column label | string | — | — |
| column-key | column's key. If you need to use the filter-change event, you need this attribute to identify which column is being filtered | string | string | - | - |
| prop | field name. You can also use its alias: `property` | string | — | — |

View File

@ -60,6 +60,7 @@
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-02',
@ -67,6 +68,7 @@
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-04',
@ -81,6 +83,7 @@
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-08',
@ -88,6 +91,7 @@
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-06',
@ -95,6 +99,7 @@
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-07',
@ -102,6 +107,7 @@
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}],
tableData4: [{
@ -1363,6 +1369,74 @@
```
:::
### 展开行
当行内容过多并且不想显示横向滚动条时,可以使用 Table 展开行功能。
:::demo 通过设置 type="expand" 和 `inline-template` 属性可以开启展开行功能,`el-table-column` 的模板会被渲染成为展开行的内容,展开行可访问的属性与使用 `inline-template` 的时候相同。
```html
<template>
<el-table
:data="tableData3"
style="width: 100%">
<el-table-column type="expand" inline-template>
<div>
<p>省: {{ row.province }}</p>
<p>市: {{ row.city }}</p>
<p>住址: {{ row.detailAddress }}</p>
<p>邮编: {{ row.zip }}</p>
</div>
</el-table-column>
<el-table-column
label="日期"
prop="date">
</el-table-column>
<el-table-column
label="姓名"
prop="name">
</el-table-column>
</el-table>
</template>
<script>
export default {
data() {
return {
tableData3: [{
date: '2016-05-02',
name: '王小虎',
province: '上海',
city: '普陀区',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-04',
name: '王小虎',
province: '上海',
city: '普陀区',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-01',
name: '王小虎',
province: '上海',
city: '普陀区',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-03',
name: '王小虎',
province: '上海',
city: '普陀区',
detailAddress: '金沙江路 1518 弄',
zip: 200333
}]
}
}
}
</script>
```
:::
### Table Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------------- |---------- |-------------------------------- |-------- |
@ -1377,7 +1451,9 @@
| row-style | 行的 style 的回调方法,也可以使用一个固定的 Object 为所有行设置一样的 Style。 | Function(row, index)/Object | — | — |
| row-key | 行数据的 Key用来优化 Table 的渲染;在使用 reserve-selection 功能的情况下,该属性是必填的 | Function(row)/String | — | — |
| context | 设置上下文环境,例如设置当前上下文就是 `_self`,父级就是 `$parent`,根组件 `$root`。优先读取 column 的 context 属性。 | Object | - | Table 所处上下文 |
| empty-text | 空数据时显示的文本内容,也可以通过 `slot="empty"` 设置 | String | | - | 暂无数据 |
| empty-text | 空数据时显示的文本内容,也可以通过 `slot="empty"` 设置 | String | - | 暂无数据 |
| default-expand-all | 是否默认展开所有行,当 Table 中存在 type="expand" 的 Column 的时候有效 | Boolean | - | false |
| expand-row-keys | 可以通过该属性设置 Table 目前的展开行,需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。| Array | - | |
### Table Events
| 事件名 | 说明 | 参数 |
@ -1395,6 +1471,7 @@
| sort-change | 当表格的排序条件发生变化的时候会触发该事件 | { column, prop, order } |
| filter-change | 当表格的筛选条件发生变化的时候会触发该事件,参数的值是一个对象,对象的 key 是 column 的 columnKey对应的 value 为用户选择的筛选条件的数组。 | filters |
| current-change | 当表格的当前行发生变化的时候会触发该事件,如果要高亮当前行,请打开表格的 highlight-current-row 属性 | currentRow, oldCurrentRow |
| expand | 当用户对某一行展开或者关闭的上会触发该事件 | row, expanded |
### Table Methods
| 方法名 | 说明 | 参数 |
@ -1405,7 +1482,7 @@
### Table-column Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------------- |---------- |-------------------------------- |-------- |
| type | 对应列的类型。如果设置了 `selection` 则显示多选框如果设置了 `index` 则显示该行的索引(从 1 开始计算) | string | selection/index | — |
| type | 对应列的类型。如果设置了 `selection` 则显示多选框如果设置了 `index` 则显示该行的索引(从 1 开始计算);如果设置了 expand 则显示为一个可展开的按钮 | string | selection/index/expand | — |
| column-key | column 的 key如果需要使用 filter-change 事件,则需要此属性标识是哪个 column 的筛选条件 | string | - | - |
| label | 显示的标题 | string | — | — |
| prop | 对应列内容的字段名,也可以使用 property 属性 | string | — | — |

View File

@ -38,7 +38,7 @@ export default {
<tbody>
{
this._l(this.data, (row, $index) =>
<tr
[<tr
style={ this.rowStyle ? this.getRowStyle(row, $index) : null }
key={ this.$parent.rowKey ? this.getKeyOfRow(row, $index) : $index }
on-dblclick={ ($event) => this.handleDoubleClick($event, row) }
@ -46,7 +46,7 @@ export default {
on-contextmenu={ ($event) => this.handleContextMenu($event, row) }
on-mouseenter={ _ => this.handleMouseEnter($index) }
on-mouseleave={ _ => this.handleMouseLeave() }
class={ this.getRowClass(row, $index) }>
class={ [this.getRowClass(row, $index)] }>
{
this._l(this.columns, (column, cellIndex) =>
<td
@ -62,7 +62,15 @@ export default {
{
!this.fixed && this.layout.scrollY && this.layout.gutterWidth ? <td class="gutter" /> : ''
}
</tr>
</tr>,
this.store.states.expandRows.indexOf(row) > -1
? (<tr>
<td colspan={ this.columns.length } class="el-table__expanded-cell">
{ this.$parent.renderExpanded ? this.$parent.renderExpanded.call(this._renderProxy, h, { row, $index, store: this.store, _self: this.$parent.$vnode.context }) : ''}
</td>
</tr>)
: ''
]
)
}
</tbody>
@ -178,7 +186,7 @@ export default {
if (cell) {
const column = getColumnByCell(table, cell);
const hoverState = table.hoverState = { cell, column, row };
const hoverState = table.hoverState = {cell, column, row};
table.$emit('cell-mouse-enter', hoverState.row, hoverState.column, hoverState.cell, event);
}
@ -228,6 +236,10 @@ export default {
this.store.commit('setCurrentRow', row);
table.$emit('row-click', row, event, column);
},
handleExpandClick(row) {
this.store.commit('toggleRowExpanded', row);
}
}
};

View File

@ -16,6 +16,12 @@ const defaults = {
order: '',
className: 'el-table-column--selection'
},
expand: {
width: 48,
minWidth: 48,
realWidth: 48,
order: ''
},
index: {
width: 48,
minWidth: 48,
@ -48,6 +54,21 @@ const forced = {
return <div>{ $index + 1 }</div>;
},
sortable: false
},
expand: {
renderHeader: function(h, {}) {
return '';
},
renderCell: function(h, { row, store }, proxy) {
const expanded = store.states.expandRows.indexOf(row) > -1;
return <div class={ 'el-table__expand-icon ' + (expanded ? 'el-table__expand-icon--expanded' : '') }
on-click={ () => proxy.handleExpandClick(row) }>
<i class='el-icon el-icon-arrow-right'></i>
</div>;
},
sortable: false,
resizable: false,
className: 'el-table__expand-column'
}
};
@ -225,9 +246,35 @@ export default {
objectAssign(column, forced[type] || {});
this.columnConfig = column;
let renderCell = column.renderCell;
let _self = this;
if (type === 'expand') {
owner.renderExpanded = function(h, data) {
if (_self.$vnode.data.inlineTemplate) {
data._self = _self.context || data._self;
if (Object.prototype.toString.call(data._self) === '[object Object]') {
for (let prop in data._self) {
if (!data.hasOwnProperty(prop)) {
data[prop] = data._self[prop];
}
}
}
data._staticTrees = _self._staticTrees;
data.$options.staticRenderFns = _self.$options.staticRenderFns;
return _self.customRender.call(data);
}
};
column.renderCell = function(h, data) {
return <div class="cell">{ renderCell(h, data, this._renderProxy) }</div>;
};
return;
}
column.renderCell = function(h, data) {
if (_self.$vnode.data.inlineTemplate) {
renderCell = function() {
@ -261,8 +308,6 @@ export default {
</el-tooltip>
: <div class="cell">{ renderCell(h, data) }</div>;
};
this.columnConfig = column;
},
destroyed() {

View File

@ -69,7 +69,9 @@ const TableStore = function(table, initialState = {}) {
selectable: null,
currentRow: null,
hoverRow: null,
filters: {}
filters: {},
expandRows: [],
defaultExpandAll: false
};
for (let prop in initialState) {
@ -85,6 +87,15 @@ TableStore.prototype.mutations = {
states._data = data;
states.data = sortData((data || []), states);
states.data.forEach((item) => {
if (!item.$extra) {
Object.defineProperty(item, '$extra', {
value: {},
enumerable: false
});
}
});
this.updateCurrentRow();
if (!states.reserveSelection) {
@ -114,6 +125,11 @@ TableStore.prototype.mutations = {
}
}
const defaultExpandAll = states.defaultExpandAll;
if (defaultExpandAll) {
this.states.expandRows = (states.data || []).slice(0);
}
Vue.nextTick(() => this.table.updateScrollY());
},
@ -218,6 +234,26 @@ TableStore.prototype.mutations = {
this.updateAllSelected();
},
toggleRowExpanded: function(states, row, expanded) {
const expandRows = states.expandRows;
if (typeof expanded !== 'undefined') {
const index = expandRows.indexOf(row);
if (expanded) {
if (index === -1) expandRows.push(row);
} else {
if (index !== -1) expandRows.splice(index, 1);
}
} else {
const index = expandRows.indexOf(row);
if (index === -1) {
expandRows.push(row);
} else {
expandRows.splice(index, 1);
}
}
this.table.$emit('expand', row, expandRows.indexOf(row) !== -1);
},
toggleAllSelection: debounce(10, function(states) {
const data = states.data || [];
const value = !states.isAllSelected;
@ -286,6 +322,22 @@ TableStore.prototype.clearSelection = function() {
}
};
TableStore.prototype.setExpandRowKeys = function(rowKeys) {
const expandRows = [];
const data = this.states.data;
const rowKey = this.states.rowKey;
if (!rowKey) throw new Error('[Table] prop row-key should not be empty.');
const keysMap = getKeysMap(data, rowKey);
rowKeys.forEach((key) => {
const info = keysMap[key];
if (info) {
expandRows.push(info.row);
}
});
this.states.expandRows = expandRows;
};
TableStore.prototype.toggleRowSelection = function(row, selected) {
const changed = toggleRowSelection(this.states, row, selected);
if (changed) {
@ -395,6 +447,8 @@ TableStore.prototype.commit = function(name, ...args) {
const mutations = this.mutations;
if (mutations[name]) {
mutations[name].apply(this, [this.states].concat(args));
} else {
throw new Error(`Action not found: ${name}`);
}
};

View File

@ -158,7 +158,11 @@
highlightCurrentRow: Boolean,
emptyText: String
emptyText: String,
expandRowKeys: Array,
defaultExpandAll: Boolean
},
components: {
@ -346,6 +350,10 @@
handler(val) {
this.store.commit('setData', val);
}
},
expandRowKeys(newVal) {
this.store.setExpandRowKeys(newVal);
}
},
@ -362,7 +370,8 @@
data() {
const store = new TableStore(this, {
rowKey: this.rowKey
rowKey: this.rowKey,
defaultExpandAll: this.defaultExpandAll
});
const layout = new TableLayout({
store,
@ -373,6 +382,7 @@
return {
store,
layout,
renderExpanded: null,
resizeProxyVisible: false
};
}

View File

@ -66,6 +66,44 @@
color: #5e6d82;
}
@e expand-column {
.cell {
padding: 0;
text-align: center;
}
}
@e expand-icon {
position: relative;
cursor: pointer;
color: #666;
font-size: 12px;
transition: transform 0.2s ease-in-out;
height: 40px;
@m expanded {
transform: rotate(90deg);
}
> .el-icon {
position: absolute;
left: 50%;
top: 50%;
margin-left: -5px;
margin-top: -5px;
}
}
@e expanded-cell {
padding: 20px 50px;
background-color: #f9fafc;
box-shadow: inset 0 2px 0 #f4f4f4;
&:hover {
background-color: #f9fafc !important;
}
}
@modifier fit {
border-right: 0;
border-bottom: 0;

View File

@ -8,11 +8,11 @@ const toArray = function(obj) {
const getTestData = function() {
return [
{ name: 'Toy Story', release: '1995-11-22', director: 'John Lasseter', runtime: 80 },
{ name: 'A Bug\'s Life', release: '1998-11-25', director: 'John Lasseter', runtime: 95 },
{ name: 'Toy Story 2', release: '1999-11-24', director: 'John Lasseter', runtime: 92 },
{ name: 'Monsters, Inc.', release: '2001-11-2', director: 'Peter Docter', runtime: 92 },
{ name: 'Finding Nemo', release: '2003-5-30', director: 'Andrew Stanton', runtime: 100 }
{ id: 1, name: 'Toy Story', release: '1995-11-22', director: 'John Lasseter', runtime: 80 },
{ id: 2, name: 'A Bug\'s Life', release: '1998-11-25', director: 'John Lasseter', runtime: 95 },
{ id: 3, name: 'Toy Story 2', release: '1999-11-24', director: 'John Lasseter', runtime: 92 },
{ id: 4, name: 'Monsters, Inc.', release: '2001-11-2', director: 'Peter Docter', runtime: 92 },
{ id: 5, name: 'Finding Nemo', release: '2003-5-30', director: 'Andrew Stanton', runtime: 100 }
];
};
@ -27,6 +27,7 @@ describe('Table', () => {
const vm = createVue({
template: `
<el-table :data="testData">
<el-table-column prop="id" />
<el-table-column prop="name" label="片名" />
<el-table-column prop="release" label="发行日期" />
<el-table-column prop="director" label="导演" />
@ -957,6 +958,90 @@ describe('Table', () => {
}, DELAY);
});
});
describe('= expand', () => {
const createInstance = function(extra) {
extra = extra || '';
return createVue({
template: `
<el-table row-key="id" :data="testData" @expand="handleExpand" ${extra}>
<el-table-column type="expand" inline-template>
<div>{{row.name}}</div>
</el-table-column>
<el-table-column prop="release" label="release" />
<el-table-column prop="director" label="director" />
<el-table-column prop="runtime" label="runtime" />
</el-table>
`,
created() {
this.testData = getTestData();
},
data() {
return { expandCount: 0, expandRowKeys: [] };
},
methods: {
handleExpand() {
this.expandCount++;
}
}
}, true);
};
it('works', done => {
const vm = createInstance();
setTimeout(_ => {
expect(vm.$el.querySelectorAll('td.el-table__expand-column').length).to.equal(5);
destroyVM(vm);
done();
}, DELAY);
});
it('should expand when click icon', done => {
const vm = createInstance();
setTimeout(_ => {
vm.$el.querySelector('td.el-table__expand-column .el-table__expand-icon').click();
setTimeout(_ => {
expect(vm.$el.querySelectorAll('.el-table__expanded-cell').length).to.equal(1);
expect(vm.expandCount).to.equal(1);
vm.$el.querySelector('td.el-table__expand-column .el-table__expand-icon').click();
setTimeout(_ => {
expect(vm.$el.querySelectorAll('.el-table__expanded-cell').length).to.equal(0);
expect(vm.expandCount).to.equal(2);
destroyVM(vm);
done();
}, DELAY);
}, DELAY);
}, DELAY);
});
it('should set expanded rows using expandRowKeys', done => {
const vm = createInstance(':expand-row-keys="expandRowKeys"');
setTimeout(_ => {
vm.expandRowKeys = [1, 3];
setTimeout(_ => {
expect(vm.$el.querySelectorAll('.el-table__expanded-cell').length).to.equal(2);
vm.expandRowKeys = [2];
setTimeout(_ => {
expect(vm.$el.querySelectorAll('.el-table__expanded-cell').length).to.equal(1);
destroyVM(vm);
done();
}, DELAY);
}, DELAY);
}, DELAY);
});
it('should default-expand-all when default-expand-all is true', done => {
const vm = createInstance('default-expand-all');
setTimeout(_ => {
expect(vm.$el.querySelectorAll('.el-table__expanded-cell').length).to.equal(5);
destroyVM(vm);
done();
}, DELAY);
});
});
});
describe('sortable', () => {