Просмотр исходного кода

Carbon: Table add features and fix bugs (#7682)

* Table: Fix error in calculate `totalFlexWidth`

* Table: Fix error in caculate `scrollX`

* Table: Watch `max-height` change

* Table: Change `toggleRowExpanded`

* Table: Support `obj[key]`

* Table: Update docs for `toggleRowExpanded`

* Table: Add style and class control for every row and cell

* Table: Add `migrating` mixin for `expand`

* Table: Update test spec

* Table: Add `index` in `table-column` for custom index
Cyril Su 7 лет назад
Родитель
Сommit
25e8503d65

+ 64 - 9
examples/docs/en-US/table.md

@@ -256,10 +256,10 @@
         return row.tag === value;
       },
 
-      tableRowClassName(row, index) {
-        if (index === 1) {
+      tableRowClassName({row, rowIndex}) {
+        if (rowIndex === 1) {
           return 'warning-row';
-        } else if (index === 3) {
+        } else if (rowIndex === 3) {
           return 'success-row';
         }
         return '';
@@ -293,6 +293,10 @@
             };
           }
         }
+      },
+
+      indexMethod(index) {
+        return index * 2;
       }
     },
 
@@ -542,10 +546,10 @@ You can highlight your table content to distinguish between "success, informatio
 <script>
   export default {
     methods: {
-      tableRowClassName(row, index) {
-        if (index === 1) {
+      tableRowClassName({row, rowIndex}) {
+        if (rowIndex === 1) {
           return 'warning-row';
-        } else if (index === 3) {
+        } else if (rowIndex === 3) {
           return 'success-row';
         }
         return '';
@@ -1845,6 +1849,49 @@ Configuring rowspan and colspan allows you to merge cells
 ```
 :::
 
+### 自定义索引
+
+自定义 `type=index` 列的行号。
+:::demo 通过给 `type=index` 的列传入 `index` 属性,可以自定义索引。该属性传入数字时,将作为索引的起始值。也可以传入一个方法,它提供当前行的行号(从 `0` 开始)作为参数,返回值将作为索引展示。
+
+```html
+<template>
+  <el-table
+    :data="tableData"
+    style="width: 100%">
+    <el-table-column
+      type="index"
+      :index="indexMethod">
+    </el-table-column>
+    <el-table-column
+      prop="date"
+      label="日期"
+      width="180">
+    </el-table-column>
+    <el-table-column
+      prop="name"
+      label="姓名"
+      width="180">
+    </el-table-column>
+    <el-table-column
+      prop="address"
+      label="地址">
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+  export default {
+    methods: {
+      indexMethod(index) {
+        return index * 2;
+      }
+    }
+  };
+</script>
+```
+:::
+
 ### Table Attributes
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
@@ -1858,8 +1905,14 @@ Configuring rowspan and colspan allows you to merge cells
 | show-header | whether Table header is visible | boolean | — | true |
 | highlight-current-row | whether current row is highlighted | boolean | — | false |
 | current-row-key | key of current row, a set only prop | string,number | — | — |
-| row-class-name | function that returns custom class names for a row, or a string assigning class names for every row | Function(row, index)/String | — | — |
-| row-style | function that returns custom style for a row,  or a string assigning custom style for every row | Function(row, index)/Object | — | — |
+| row-class-name | function that returns custom class names for a row, or a string assigning class names for every row | Function({row, rowIndex})/String | — | — |
+| row-style | function that returns custom style for a row,  or a string assigning custom style for every row | Function({row, rowIndex})/Object | — | — |
+| cell-class-name | function that returns custom class names for a cell, or a string assigning class names for every cell | Function({row, rowIndex})/String | — | — |
+| cell-style | function that returns custom style for a cell,  or a string assigning custom style for every cell | Function({row, rowIndex})/Object | — | — |
+| header-row-class-name | function that returns custom class names for a row in table header, or a string assigning class names for every row in table header | Function({row, rowIndex})/String | — | — |
+| header-row-style | function that returns custom style for a row in table header,  or a string assigning custom style for every row in table header | Function({row, rowIndex})/Object | — | — |
+| header-cell-class-name | function that returns custom class names for a cell in table header, or a string assigning class names for every cell in table header | Function({row, rowIndex})/String | — | — |
+| header-cell-style | function that returns custom style for a cell in table header,  or a string assigning custom style for every cell in table header | Function({row, rowIndex})/Object | — | — |
 | row-key | key of row data, used for optimizing rendering. Required if `reserve-selection` is on. When its type is String, multi-level access is supported, e.g. `user.info.id`, but `user.info[0].id` is not supported, in which case `Function` should be used. | Function(row)/String | — | — |
 | 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 |
@@ -1889,13 +1942,14 @@ Configuring rowspan and colspan allows you to merge cells
 | 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 |
 | header-dragend | triggers when finish dragging header | newWidth, oldWidth, column, event |
-| expand | triggers when user expands or collapses a row | row, expanded |
+| expand-change | triggers when user expands or collapses a row | row, expandedRows |
 
 ### Table Methods
 | Method | Description | Parameters |
 |------|--------|-------|
 | clearSelection | used in multiple selection Table, clear selection, might be useful when `reserve-selection` is on | selection |
 | toggleRowSelection | used in multiple selection Table, toggle if a certain row is selected. With the second parameter, you can directly set if this row is selected | row, selected |
+| toggleRowExpanded | used in expand Table, toggle if a certain row is expanded. With the second parameter, you can directly set if this row is expanded | row, expanded |
 | setCurrentRow | used in single selection Table, set a certain row selected. If called without any parameter, it will clear selection. | row |
 | clearSort | clear sorting, restore data to the original order | — |
 | clearFilter | clear filter | — |
@@ -1909,6 +1963,7 @@ Configuring rowspan and colspan allows you to merge cells
 | 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). If set to `expand`, the column will display expand icon.  | string | selection/index/expand | — |
+| index | 如果设置了 `type=index`,可以通过传递 `index` 属性来自定义索引 | string, Function(index) | - | - |
 | 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 | — | — |

+ 65 - 10
examples/docs/zh-CN/table.md

@@ -298,10 +298,10 @@
         return row.tag === value;
       },
 
-      tableRowClassName(row, index) {
-        if (index === 1) {
+      tableRowClassName({row, rowndex}) {
+        if (rowndex === 1) {
           return 'warning-row';
-        } else if (index === 3) {
+        } else if (rowndex === 3) {
           return 'success-row';
         }
         return '';
@@ -335,6 +335,10 @@
             };
           }
         }
+      },
+
+      indexMethod(index) {
+        return index * 2;
       }
     },
 
@@ -582,10 +586,10 @@
 <script>
   export default {
     methods: {
-      tableRowClassName(row, index) {
-        if (index === 1) {
+      tableRowClassName({row, rowIndex}) {
+        if (rowIndex === 1) {
           return 'warning-row';
-        } else if (index === 3) {
+        } else if (rowIndex === 3) {
           return 'success-row';
         }
         return '';
@@ -1908,6 +1912,49 @@
 ```
 :::
 
+### 自定义索引
+
+自定义 `type=index` 列的行号。
+:::demo 通过给 `type=index` 的列传入 `index` 属性,可以自定义索引。该属性传入数字时,将作为索引的起始值。也可以传入一个方法,它提供当前行的行号(从 `0` 开始)作为参数,返回值将作为索引展示。
+
+```html
+<template>
+  <el-table
+    :data="tableData"
+    style="width: 100%">
+    <el-table-column
+      type="index"
+      :index="indexMethod">
+    </el-table-column>
+    <el-table-column
+      prop="date"
+      label="日期"
+      width="180">
+    </el-table-column>
+    <el-table-column
+      prop="name"
+      label="姓名"
+      width="180">
+    </el-table-column>
+    <el-table-column
+      prop="address"
+      label="地址">
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+  export default {
+    methods: {
+      indexMethod(index) {
+        return index * 2;
+      }
+    }
+  };
+</script>
+```
+:::
+
 ### Table Attributes
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
@@ -1921,8 +1968,14 @@
 | show-header | 是否显示表头 | boolean | — | true |
 | highlight-current-row | 是否要高亮当前行 | boolean | — | false |
 | current-row-key | 当前行的 key,只写属性 | String,Number | — | — |
-| row-class-name | 行的 className 的回调方法,也可以使用字符串为所有行设置一个固定的 className。 | Function(row, index)/String | — | — |
-| row-style | 行的 style 的回调方法,也可以使用一个固定的 Object 为所有行设置一样的 Style。 | Function(row, index)/Object | — | — |
+| row-class-name | 行的 className 的回调方法,也可以使用字符串为所有行设置一个固定的 className。 | Function({row, rowIndex})/String | — | — |
+| row-style | 行的 style 的回调方法,也可以使用一个固定的 Object 为所有行设置一样的 Style。 | Function({row, rowIndex})/Object | — | — |
+| cell-class-name | 单元格的 className 的回调方法,也可以使用字符串为所有单元格设置一个固定的 className。 | Function({row, column, rowIndex, columnIndex})/String | — | — |
+| cell-style | 单元格的 style 的回调方法,也可以使用一个固定的 Object 为所有单元格设置一样的 Style。 | Function({row, rowIndex, rowIndex, columnIndex})/Object | — | — |
+| header-row-class-name | 表头行的 className 的回调方法,也可以使用字符串为所有表头行设置一个固定的 className。 | Function({row, rowIndex})/String | — | — |
+| header-row-style | 表头行的 style 的回调方法,也可以使用一个固定的 Object 为所有表头行设置一样的 Style。 | Function({row, rowIndex})/Object | — | — |
+| header-cell-class-name | 表头单元格的 className 的回调方法,也可以使用字符串为所有表头单元格设置一个固定的 className。 | Function({row, column, rowIndex, columnIndex})/String | — | — |
+| header-cell-style | 表头单元格的 style 的回调方法,也可以使用一个固定的 Object 为所有表头单元格设置一样的 Style。 | Function({row, rowIndex, rowIndex, columnIndex})/Object | — | — |
 | row-key | 行数据的 Key,用来优化 Table 的渲染;在使用 reserve-selection 功能的情况下,该属性是必填的。类型为 String 时,支持多层访问:`user.info.id`,但不支持 `user.info[0].id`,此种情况请使用 `Function`。 | Function(row)/String | — | — |
 | empty-text | 空数据时显示的文本内容,也可以通过 `slot="empty"` 设置 | String | — | 暂无数据 |
 | default-expand-all | 是否默认展开所有行,当 Table 中存在 type="expand" 的 Column 的时候有效 | Boolean | — | false |
@@ -1952,13 +2005,14 @@
 | filter-change | 当表格的筛选条件发生变化的时候会触发该事件,参数的值是一个对象,对象的 key 是 column 的 columnKey,对应的 value 为用户选择的筛选条件的数组。 | filters |
 | current-change | 当表格的当前行发生变化的时候会触发该事件,如果要高亮当前行,请打开表格的 highlight-current-row 属性 | currentRow, oldCurrentRow |
 | header-dragend | 当拖动表头改变了列的宽度的时候会触发该事件 | newWidth, oldWidth, column, event |
-| expand | 当用户对某一行展开或者关闭的上会触发该事件 | row, expanded |
+| expand-change | 当用户对某一行展开或者关闭的时候会触发该事件 | row, expanded |
 
 ### Table Methods
 | 方法名 | 说明 | 参数 |
 | ---- | ---- | ---- |
 | clearSelection | 用于多选表格,清空用户的选择,当使用 reserve-selection 功能的时候,可能会需要使用此方法 | selection |
 | toggleRowSelection | 用于多选表格,切换某一行的选中状态,如果使用了第二个参数,则是设置这一行选中与否(selected 为 true 则选中) | row, selected |
+| toggleRowExpanded | 用于可展开表格,切换某一行的展开状态,如果使用了第二个参数,则是设置这一行展开与否(expanded 为 true 则展开) | row, expanded |
 | setCurrentRow | 用于单选表格,设定某一行为选中行,如果调用时不加参数,则会取消目前高亮行的选中状态。 | row |
 | clearSort | 用于清空排序条件,数据会恢复成未排序的状态 | — |
 | clearFilter | 用于清空过滤条件,数据会恢复成未过滤的状态 | — |
@@ -1971,7 +2025,8 @@
 ### Table-column Attributes
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
-| type | 对应列的类型。如果设置了 `selection` 则显示多选框;如果设置了 `index` 则显示该行的索引(从 1 开始计算);如果设置了 expand 则显示为一个可展开的按钮 | string | selection/index/expand | — |
+| type | 对应列的类型。如果设置了 `selection` 则显示多选框;如果设置了 `index` 则显示该行的索引(从 1 开始计算);如果设置了 `expand` 则显示为一个可展开的按钮 | string | selection/index/expand | — |
+| index | 如果设置了 `type=index`,可以通过传递 `index` 属性来自定义索引 | string, Function(index) | - | - |
 | column-key | column 的 key,如果需要使用 filter-change 事件,则需要此属性标识是哪个 column 的筛选条件 | string | — | — |
 | label | 显示的标题 | string | — | — |
 | prop | 对应列内容的字段名,也可以使用 property 属性 | string | — | — |

+ 3 - 27
packages/form/src/form-item.vue

@@ -32,31 +32,7 @@
 <script>
   import AsyncValidator from 'async-validator';
   import emitter from 'element-ui/src/mixins/emitter';
-
-  function noop() {}
-
-  function getPropByPath(obj, path) {
-    let tempObj = obj;
-    path = path.replace(/\[(\w+)\]/g, '.$1');
-    path = path.replace(/^\./, '');
-
-    let keyArr = path.split('.');
-    let i = 0;
-
-    for (let len = keyArr.length; i < len - 1; ++i) {
-      let key = keyArr[i];
-      if (key in tempObj) {
-        tempObj = tempObj[key];
-      } else {
-        throw new Error('please transfer a valid prop path to form item!');
-      }
-    }
-    return {
-      o: tempObj,
-      k: keyArr[i],
-      v: tempObj[keyArr[i]]
-    };
-  }
+  import { noop, getPropByPath } from 'element-ui/src/utils/util';
 
   export default {
     name: 'ElFormItem',
@@ -148,7 +124,7 @@
             path = path.replace(/:/, '.');
           }
 
-          return getPropByPath(model, path).v;
+          return getPropByPath(model, path, true).v;
         }
       },
       isRequired() {
@@ -226,7 +202,7 @@
           path = path.replace(/:/, '.');
         }
 
-        let prop = getPropByPath(model, path);
+        let prop = getPropByPath(model, path, true);
 
         if (Array.isArray(value)) {
           this.validateDisabled = true;

+ 53 - 24
packages/table/src/table-body.js

@@ -63,14 +63,8 @@ export default {
                       if (rowspan === 1 && colspan === 1) {
                         return (
                           <td
-                            class={
-                              [
-                                column.id,
-                                column.align,
-                                column.className || '',
-                                columnsHidden[cellIndex] ? 'is-hidden' : ''
-                              ]
-                            }
+                            style={ this.getCellStyle($index, cellIndex, row, column) }
+                            class={ this.getCellClass($index, cellIndex, row, column) }
                             on-mouseenter={ ($event) => this.handleCellMouseEnter($event, row) }
                             on-mouseleave={ this.handleCellMouseLeave }>
                             {
@@ -92,14 +86,8 @@ export default {
                       } else {
                         return (
                           <td
-                            class={
-                              [
-                                column.id,
-                                column.align,
-                                column.className || '',
-                                columnsHidden[cellIndex] ? 'is-hidden' : ''
-                              ]
-                            }
+                            style={ this.getCellStyle($index, cellIndex, row, column) }
+                            class={ this.getCellClass($index, cellIndex, row, column) }
                             rowspan={ rowspan }
                             colspan={ colspan }
                             on-mouseenter={ ($event) => this.handleCellMouseEnter($event, row) }
@@ -270,25 +258,31 @@ export default {
       };
     },
 
-    getRowStyle(row, index) {
-      const rowStyle = this.rowStyle;
+    getRowStyle(row, rowIndex) {
+      const rowStyle = this.table.rowStyle;
       if (typeof rowStyle === 'function') {
-        return rowStyle.call(null, row, index);
+        return rowStyle.call(null, {
+          row,
+          rowIndex
+        });
       }
       return rowStyle;
     },
 
-    getRowClass(row, index) {
+    getRowClass(row, rowIndex) {
       const classes = ['el-table__row'];
 
-      if (this.stripe && index % 2 === 1) {
+      if (this.stripe && rowIndex % 2 === 1) {
         classes.push('el-table__row--striped');
       }
-      const rowClassName = this.rowClassName;
+      const rowClassName = this.table.rowClassName;
       if (typeof rowClassName === 'string') {
         classes.push(rowClassName);
       } else if (typeof rowClassName === 'function') {
-        classes.push(rowClassName.call(null, row, index) || '');
+        classes.push(rowClassName.call(null, {
+          row,
+          rowIndex
+        }));
       }
 
       if (this.store.states.expandRows.indexOf(row) > -1) {
@@ -298,6 +292,41 @@ export default {
       return classes.join(' ');
     },
 
+    getCellStyle(rowIndex, columnIndex, row, column) {
+      const cellStyle = this.table.cellStyle;
+      if (typeof cellStyle === 'function') {
+        return cellStyle.call(null, {
+          rowIndex,
+          columnIndex,
+          row,
+          column
+        });
+      }
+      return cellStyle;
+    },
+
+    getCellClass(rowIndex, columnIndex, row, column) {
+      const classes = [column.id, column.align, column.className];
+
+      if (this.isColumnHidden(columnIndex)) {
+        classes.push('is-hidden');
+      }
+
+      const cellClassName = this.table.cellClassName;
+      if (typeof cellClassName === 'string') {
+        classes.push(cellClassName);
+      } else if (typeof cellClassName === 'function') {
+        classes.push(cellClassName.call(null, {
+          rowIndex,
+          columnIndex,
+          row,
+          column
+        }));
+      }
+
+      return classes.join(' ');
+    },
+
     handleCellMouseEnter(event, row) {
       const table = this.table;
       const cell = getCell(event);
@@ -371,7 +400,7 @@ export default {
     },
 
     handleExpandClick(row) {
-      this.store.commit('toggleRowExpanded', row);
+      this.store.toggleRowExpanded(row);
     }
   }
 };

+ 23 - 8
packages/table/src/table-column.js

@@ -1,7 +1,7 @@
 import ElCheckbox from 'element-ui/packages/checkbox';
 import ElTag from 'element-ui/packages/tag';
 import objectAssign from 'element-ui/src/utils/merge';
-import { getValueByPath } from 'element-ui/src/utils/util';
+import { getPropByPath } from 'element-ui/src/utils/util';
 
 let columnIdSeed = 1;
 
@@ -50,8 +50,17 @@ const forced = {
     renderHeader: function(h, { column }) {
       return column.label || '#';
     },
-    renderCell: function(h, { $index }) {
-      return <div>{ $index + 1 }</div>;
+    renderCell: function(h, { $index, column }) {
+      let i = $index + 1;
+      const index = column.index;
+
+      if (typeof index === 'number') {
+        i = $index + index;
+      } else if (typeof index === 'function') {
+        i = index($index);
+      }
+
+      return <div>{ i }</div>;
     },
     sortable: false
   },
@@ -97,9 +106,7 @@ const getDefaultColumn = function(type, options) {
 
 const DEFAULT_RENDER_CELL = function(h, { row, column }) {
   const property = column.property;
-  const value = property && property.indexOf('.') === -1
-    ? row[property]
-    : getValueByPath(row, property);
+  const value = property && getPropByPath(row, property).v;
   if (column && column.formatter) {
     return column.formatter(row, column, value);
   }
@@ -148,7 +155,8 @@ export default {
     filterMultiple: {
       type: Boolean,
       default: true
-    }
+    },
+    index: [Number, Function]
   },
 
   data() {
@@ -238,7 +246,8 @@ export default {
       filterMultiple: this.filterMultiple,
       filterOpened: false,
       filteredValue: this.filteredValue || [],
-      filterPlacement: this.filterPlacement || ''
+      filterPlacement: this.filterPlacement || '',
+      index: this.index
     });
 
     objectAssign(column, forced[type] || {});
@@ -354,6 +363,12 @@ export default {
       if (this.columnConfig) {
         this.columnConfig.sortable = newVal;
       }
+    },
+
+    index(newVal) {
+      if (this.columnConfig) {
+        this.columnConfig.index = newVal;
+      }
     }
   },
 

+ 74 - 2
packages/table/src/table-header.js

@@ -95,7 +95,10 @@ export default {
         <thead class={ [{ 'is-group': isGroup, 'has-gutter': this.hasGutter }] }>
           {
             this._l(columnRows, (columns, rowIndex) =>
-              <tr>
+              <tr
+                style={ this.getHeaderRowStyle(rowIndex) }
+                class={ this.getHeaderRowClass(rowIndex) }
+              >
               {
                 this._l(columns, (column, cellIndex) =>
                   <th
@@ -105,7 +108,8 @@ export default {
                     on-mouseout={ this.handleMouseOut }
                     on-mousedown={ ($event) => this.handleMouseDown($event, column) }
                     on-click={ ($event) => this.handleHeaderClick($event, column) }
-                    class={ [column.id, column.order, column.headerAlign, column.className || '', rowIndex === 0 && this.isCellHidden(cellIndex, columns) ? 'is-hidden' : '', !column.children ? 'is-leaf' : '', column.labelClassName, column.sortable ? 'is-sortable' : ''] }>
+                    style={ this.getHeaderCellStyle(rowIndex, cellIndex, columns, column) }
+                    class={ this.getHeaderCellClass(rowIndex, cellIndex, columns, column) }>
                     <div class={ ['cell', column.filteredValue && column.filteredValue.length > 0 ? 'highlight' : '', column.labelClassName] }>
                     {
                       column.renderHeader
@@ -172,6 +176,10 @@ export default {
   },
 
   computed: {
+    table() {
+      return this.$parent;
+    },
+
     isAllSelected() {
       return this.store.states.isAllSelected;
     },
@@ -256,6 +264,70 @@ export default {
       }
     },
 
+    getHeaderRowStyle(rowIndex) {
+      const headerRowStyle = this.table.headerRowStyle;
+      if (typeof headerRowStyle === 'function') {
+        return headerRowStyle.call(null, { rowIndex });
+      }
+      return headerRowStyle;
+    },
+
+    getHeaderRowClass(rowIndex) {
+      const classes = [];
+
+      const headerRowClassName = this.table.headerRowClassName;
+      if (typeof headerRowClassName === 'string') {
+        classes.push(headerRowClassName);
+      } else if (typeof headerRowClassName === 'function') {
+        classes.push(headerRowClassName.call(null, { rowIndex }));
+      }
+
+      return classes.join(' ');
+    },
+
+    getHeaderCellStyle(rowIndex, columnIndex, row, column) {
+      const headerCellStyle = this.table.headerCellStyle;
+      if (typeof headerCellStyle === 'function') {
+        return headerCellStyle.call(null, {
+          rowIndex,
+          columnIndex,
+          row,
+          column
+        });
+      }
+      return headerCellStyle;
+    },
+
+    getHeaderCellClass(rowIndex, columnIndex, row, column) {
+      const classes = [column.id, column.order, column.headerAlign, column.className, column.labelClassName];
+
+      if (rowIndex === 0 && this.isCellHidden(columnIndex, row)) {
+        classes.push('is-hidden');
+      }
+
+      if (!column.children) {
+        classes.push('is-leaf');
+      }
+
+      if (column.sortable) {
+        classes.push('is-sortable');
+      }
+
+      const headerCellClassName = this.table.headerCellClassName;
+      if (typeof headerCellClassName === 'string') {
+        classes.push(headerCellClassName);
+      } else if (typeof headerCellClassName === 'function') {
+        classes.push(headerCellClassName.call(null, {
+          rowIndex,
+          columnIndex,
+          row,
+          column
+        }));
+      }
+
+      return classes.join(' ');
+    },
+
     toggleAllSelection() {
       this.store.commit('toggleAllSelection');
     },

+ 4 - 2
packages/table/src/table-layout.js

@@ -118,10 +118,12 @@ class TableLayout {
         bodyMinWidth += column.width || column.minWidth || 80;
       });
 
-      if (bodyMinWidth < bodyWidth - this.gutterWidth) { // DON'T HAVE SCROLL BAR
+      const scrollYWidth = this.scrollY ? this.gutterWidth : 0;
+
+      if (bodyMinWidth <= bodyWidth - scrollYWidth) { // DON'T HAVE SCROLL BAR
         this.scrollX = false;
 
-        const totalFlexWidth = bodyWidth - this.gutterWidth - bodyMinWidth;
+        const totalFlexWidth = bodyWidth - scrollYWidth - bodyMinWidth;
 
         if (flexColumns.length === 1) {
           flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth;

+ 37 - 20
packages/table/src/table-store.js

@@ -44,6 +44,36 @@ const toggleRowSelection = function(states, row, selected) {
   return changed;
 };
 
+const toggleRowExpanded = function(states, row, expanded) {
+  let changed = false;
+  const expandRows = states.expandRows;
+  if (typeof expanded !== 'undefined') {
+    const index = expandRows.indexOf(row);
+    if (expanded) {
+      if (index === -1) {
+        expandRows.push(row);
+        changed = true;
+      }
+    } else {
+      if (index !== -1) {
+        expandRows.splice(index, 1);
+        changed = true;
+      }
+    }
+  } else {
+    const index = expandRows.indexOf(row);
+    if (index === -1) {
+      expandRows.push(row);
+      changed = true;
+    } else {
+      expandRows.splice(index, 1);
+      changed = true;
+    }
+  }
+
+  return changed;
+};
+
 const TableStore = function(table, initialState = {}) {
   if (!table) {
     throw new Error('Table is required.');
@@ -259,26 +289,6 @@ 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;
@@ -381,6 +391,13 @@ TableStore.prototype.toggleRowSelection = function(row, selected) {
   }
 };
 
+TableStore.prototype.toggleRowExpanded = function(row, expanded) {
+  const changed = toggleRowExpanded(this.states, row, expanded);
+  if (changed) {
+    this.table.$emit('expand-change', row, this.states.expandRows);
+  }
+};
+
 TableStore.prototype.cleanSelection = function() {
   const selection = this.states.selection || [];
   const data = this.states.data;

+ 31 - 2
packages/table/src/table.vue

@@ -156,6 +156,7 @@
   import debounce from 'throttle-debounce/debounce';
   import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
   import Locale from 'element-ui/src/mixins/locale';
+  import Migrating from 'element-ui/src/mixins/migrating';
   import TableStore from './table-store';
   import TableLayout from './table-layout';
   import TableBody from './table-body';
@@ -168,7 +169,7 @@
   export default {
     name: 'ElTable',
 
-    mixins: [Locale],
+    mixins: [Locale, Migrating],
 
     props: {
       data: {
@@ -214,6 +215,18 @@
 
       rowStyle: [Object, Function],
 
+      cellClassName: [String, Function],
+
+      cellStyle: [Object, Function],
+
+      headerRowClassName: [String, Function],
+
+      headerRowStyle: [Object, Function],
+
+      headerCellClassName: [String, Function],
+
+      headerCellStyle: [Object, Function],
+
       highlightCurrentRow: Boolean,
 
       currentRowKey: [String, Number],
@@ -239,6 +252,14 @@
     },
 
     methods: {
+      getMigratingConfig() {
+        return {
+          events: {
+            expand: 'expand is renamed to `expand-change`'
+          }
+        };
+      },
+
       setCurrentRow(row) {
         this.store.commit('setCurrentRow', row);
       },
@@ -248,6 +269,10 @@
         this.store.updateAllSelected();
       },
 
+      toggleRowExpanded(row, expanded) {
+        this.store.toggleRowExpanded(row, expanded);
+      },
+
       clearSelection() {
         this.store.clearSelection();
       },
@@ -317,8 +342,8 @@
 
       doLayout() {
         this.store.updateColumns();
-        this.layout.update();
         this.updateScrollY();
+        this.layout.update();
         this.$nextTick(() => {
           if (this.height) {
             this.layout.setHeight(this.height);
@@ -444,6 +469,10 @@
         this.layout.setHeight(value);
       },
 
+      maxHeight(value) {
+        this.layout.setMaxHeight(value);
+      },
+
       currentRowKey(newVal) {
         this.store.setCurrentRowKey(newVal);
       },

+ 28 - 0
src/utils/util.js

@@ -1,4 +1,7 @@
 const hasOwnProperty = Object.prototype.hasOwnProperty;
+
+export function noop() {};
+
 export function hasOwn(obj, key) {
   return hasOwnProperty.call(obj, key);
 };
@@ -38,6 +41,31 @@ export const getValueByPath = function(object, prop) {
   return result;
 };
 
+export function getPropByPath(obj, path, strict) {
+  let tempObj = obj;
+  path = path.replace(/\[(\w+)\]/g, '.$1');
+  path = path.replace(/^\./, '');
+
+  let keyArr = path.split('.');
+  let i = 0;
+  for (let len = keyArr.length; i < len - 1; ++i) {
+    let key = keyArr[i];
+    if (key in tempObj) {
+      tempObj = tempObj[key];
+    } else {
+      if (strict) {
+        throw new Error('please transfer a valid prop path to form item!');
+      }
+      break;
+    }
+  }
+  return {
+    o: tempObj,
+    k: keyArr[i],
+    v: tempObj[keyArr[i]]
+  };
+};
+
 export const generateId = function() {
   return Math.floor(Math.random() * 10000);
 };

+ 7 - 7
test/unit/specs/table.spec.js

@@ -138,10 +138,10 @@ describe('Table', () => {
     it('tableRowClassName', done => {
       const vm = createTable(':row-class-name="tableRowClassName"', {
         methods: {
-          tableRowClassName(row, index) {
-            if (index === 1) {
+          tableRowClassName({row, rowIndex}) {
+            if (rowIndex === 1) {
               return 'info-row';
-            } else if (index === 3) {
+            } else if (rowIndex === 3) {
               return 'positive-row';
             }
 
@@ -171,8 +171,8 @@ describe('Table', () => {
     it('tableRowStyle[Function]', done => {
       const vm = createTable(':row-style="tableRowStyle"', {
         methods: {
-          tableRowStyle(row, index) {
-            if (index === 1) {
+          tableRowStyle({row, rowIndex}) {
+            if (rowIndex === 1) {
               return { height: '60px' };
             }
 
@@ -897,12 +897,12 @@ describe('Table', () => {
           extra = extra || '';
           return createVue({
             template: `
-            <el-table row-key="id" :data="testData" @expand="handleExpand" ${extra}>
+            <el-table row-key="id" :data="testData" @expand-change="handleExpand" ${extra}>
               <el-table-column type="expand">
                 <template slot-scope="props">
                   <div>{{props.row.name}}</div>
                 </template>
-              </el-table-column>
+            </el-table-column>
               <el-table-column prop="release" label="release" />
               <el-table-column prop="director" label="director" />
               <el-table-column prop="runtime" label="runtime" />