浏览代码

Improve Table:
1. Add TableStore & TableLayout.
2. Remove customCriteria & customBackgroundColors.
3. Remove fixedColumnCount.
4. Add fixed prop for TableColumn.
5. Add selectable prop for TableColumn[type="selection"].

furybean 8 年之前
父节点
当前提交
b36da2f982

+ 5 - 0
CHANGELOG.md

@@ -15,10 +15,15 @@
 - 修复 Loading 关闭后有几率滚动失效的问题
 - 修复 远程搜索的 Select 不能正确渲染默认初始值的问题
 - 修复 Switch 的 width 属性无效的问题
+- 优化 Table 性能,优化 Table 代码结构
+- Table 增加属性 rowClassName
+- TableColumn 增加 fixed 属性,可选值:true, false, left, right
+- TableColumn[type="selection"] 增加 selectable 属性
 
 #### 非兼容性更新
 
 - 全屏 Loading 现在默认不再锁定屏幕滚动。如果需要的话,可添加 `lock` 修饰符
+- Table 删除属性 fixedColumnCount, customCriteria, customBackgroundColors
 
 ### 1.0.0-rc.7
 

+ 86 - 46
examples/docs/zh-cn/table.md

@@ -117,6 +117,15 @@
 
       formatter(row, column) {
         return row.address;
+      },
+
+      tableRowClassName(row, index) {
+        if (index === 1) {
+          return 'info-row';
+        } else if (index === 3) {
+          return 'positive-row';
+        }
+        return '';
       }
     },
 
@@ -132,6 +141,16 @@
   };
 </script>
 
+<style>
+  .el-table .info-row {
+    background: #c9e5f5;
+  }
+
+  .el-table .positive-row {
+    background: #e2f0e4;
+  }
+</style>
+
 ## Table 表格
 
 用于展示多条结构类似的数据,可对数据进行排序、筛选、对比或其他自定义操作。
@@ -140,24 +159,24 @@
 
 基础的表格展示用法。
 
-:::demo 当`el-table`元素中注入`data`对象数组后,在`el-table-column`中用`property`属性来对应对象中的键名即可填入数据,用`label`属性来定义表格的列名。可以使用`width`属性来定义列宽。
+:::demo 当`el-table`元素中注入`data`对象数组后,在`el-table-column`中用`prop`属性来对应对象中的键名即可填入数据,用`label`属性来定义表格的列名。可以使用`width`属性来定义列宽。
 ```html
   <template>
     <el-table
       :data="tableData"
       style="width: 100%">
       <el-table-column
-        property="date"
+        prop="date"
         label="日期"
         width="180">
       </el-table-column>
       <el-table-column
-        property="name"
+        prop="name"
         label="姓名"
         width="180">
       </el-table-column>
       <el-table-column
-        property="address"
+        prop="address"
         label="地址">
       </el-table-column>
     </el-table>
@@ -203,17 +222,17 @@
     stripe
     style="width: 100%">
     <el-table-column
-      property="date"
+      prop="date"
       label="日期"
       width="180">
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="180">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址">
     </el-table-column>
   </el-table>
@@ -257,17 +276,17 @@
     border
     style="width: 100%">
     <el-table-column
-      property="date"
+      prop="date"
       label="日期"
       width="180">
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="180">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址">
     </el-table-column>
   </el-table>
@@ -305,33 +324,52 @@
 
 可将表格内容 highlight 显示,方便区分「成功、信息、警告、危险」等内容。
 
-:::demo 为行添加自定义背景色,表明该行处于某种状态。若某一行拥有`custom-criteria`数组中的某个字段且值为`true`,则为该行添加`custom-background-colors`数组中对应的背景色
+:::demo 可以通过指定 Table 组件的 rowClassName 属性来为 Table 中的某一行添加 class,表明该行处于某种状态
 ```html
 <template>
   <el-table
     :data="tableData2"
     style="width: 100%"
-    :custom-criteria="['$info', '$positive']"
-    :custom-background-colors="['#C9E5F5', '#E2F0E4']">
+    :row-class-name="tableRowClassName">
     <el-table-column
-      property="date"
+      prop="date"
       label="日期"
       width="180">
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="180">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址">
     </el-table-column>
   </el-table>
 </template>
 
+<style>
+  .el-table .info-row {
+    background: #c9e5f5;
+  }
+
+  .el-table .positive-row {
+    background: #e2f0e4;
+  }
+</style>
+
 <script>
   export default {
+    methods: {
+      tableRowClassName(row, index) {
+        if (index === 1) {
+          return 'info-row';
+        } else if (index === 3) {
+          return 'positive-row';
+        }
+        return '';
+      }
+    },
     data() {
       return {
         tableData2: [{
@@ -373,17 +411,17 @@
     border
     style="width: 100%">
     <el-table-column
-      property="date"
+      prop="date"
       label="日期"
       width="180">
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="180">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址">
     </el-table-column>
   </el-table>
@@ -433,41 +471,41 @@
 
 横向内容过多时,可选择固定首列。
 
-:::demo 固定列需要使用`fixed-column-count`属性,它接受一个`Number`,表示左起要固定的列数
+:::demo 固定列需要使用`fixed`属性,它接受 Boolean 值或者`left` `right`,表示左边固定还是右边固定
 ```html
 <template>
   <el-table
     :data="tableData"
-    :fixed-column-count="1"
     border
     style="width: 100%">
     <el-table-column
-      property="date"
+      fixed
+      prop="date"
       label="日期"
       width="150">
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="120">
     </el-table-column>
     <el-table-column
-      property="province"
+      prop="province"
       label="省份"
       width="120">
     </el-table-column>
     <el-table-column
-      property="city"
+      prop="city"
       label="市区"
       width="120">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址"
       width="300">
     </el-table-column>
     <el-table-column
-      property="zip"
+      prop="zip"
       label="邮编"
       width="120">
     </el-table-column>
@@ -523,37 +561,37 @@
 <template>
   <el-table
     :data="tableData3"
-    :fixed-column-count="1"
     border
     style="width: 100%"
     height="250">
     <el-table-column
-      property="date"
+      fixed
+      prop="date"
       label="日期"
       width="150">
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="120">
     </el-table-column>
     <el-table-column
-      property="province"
+      prop="province"
       label="省份"
       width="120">
     </el-table-column>
     <el-table-column
-      property="city"
+      prop="city"
       label="市区"
       width="120">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址"
       width="300">
     </el-table-column>
     <el-table-column
-      property="zip"
+      prop="zip"
       label="邮编"
       width="120">
     </el-table-column>
@@ -639,17 +677,17 @@
       width="50">
     </el-table-column>
     <el-table-column
-      property="date"
+      prop="date"
       label="日期"
       width="120">
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="120">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址">
     </el-table-column>
   </el-table>
@@ -694,7 +732,7 @@
 
 选择多行数据时使用 Checkbox。
 
-:::demo 除了`selection-mode`的设置外,多选与单选并没有太大差别,只是传入`selectionchange`事件中的参数从对象变成了对象数组。此外,需要提供一个列来显示多选框: 手动添加一个`el-table-column`,设`type`属性为`selection`即可。在本例中,为了方便说明其他属性,我们还使用了`inline-template`和`show-tooltip-when-overflow`:设置了`inline-template`属性后,可以通过调用`row`对象中的值取代`property`属性的设置;默认情况下若内容过多会折行显示,若需要单行显示可以使用`show-tooltip-when-overflow`属性,它接受一个`Boolean`,为`true`时多余的内容会在 hover 时以 tooltip 的形式显示出来。
+:::demo 除了`selection-mode`的设置外,多选与单选并没有太大差别,只是传入`selectionchange`事件中的参数从对象变成了对象数组。此外,需要提供一个列来显示多选框: 手动添加一个`el-table-column`,设`type`属性为`selection`即可。在本例中,为了方便说明其他属性,我们还使用了`inline-template`和`show-tooltip-when-overflow`:设置了`inline-template`属性后,可以通过调用`row`对象中的值取代`prop`属性的设置;默认情况下若内容过多会折行显示,若需要单行显示可以使用`show-tooltip-when-overflow`属性,它接受一个`Boolean`,为`true`时多余的内容会在 hover 时以 tooltip 的形式显示出来。
 ```html
 <template>
   <el-table
@@ -713,12 +751,12 @@
       <div>{{ row.date }}</div>
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="120">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址"
       show-tooltip-when-overflow>
     </el-table-column>
@@ -784,18 +822,18 @@
     border
     style="width: 100%">
     <el-table-column
-      property="date"
+      prop="date"
       label="日期"
       sortable
       width="180">
     </el-table-column>
     <el-table-column
-      property="name"
+      prop="name"
       label="姓名"
       width="180">
     </el-table-column>
     <el-table-column
-      property="address"
+      prop="address"
       label="地址"
       :formatter="formatter">
     </el-table-column>
@@ -843,9 +881,9 @@
 | stripe | 是否为斑马纹 table | boolean | — | false |
 | border | 是否带有纵向边框 | boolean | — | false |
 | fit | 列的宽度是否自撑开 | boolean | — | true |
+| rowClassName | 行的 className 的回调,会传入 row, index。 | Function | - | - |
 | selection-mode | 列表项选择模式 | string | single/multiple/none | none |
 | allow-no-selection | 单选模式是否允许选项为空 | boolean | — | false |
-| fixed-column-count | 固定列的个数 | number | — | 0 |
 
 ### Table Events
 | 事件名 | 说明 | 参数 |
@@ -859,12 +897,14 @@
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
 | label | 显示的标题 | string | — | — |
-| property | 对应列内容的字段名 | string | — | — |
+| prop | 对应列内容的字段名,也可以使用 property 属性 | string | — | — |
 | width | 对应列的宽度 | string | — | — |
+| fixed | 列是否固定在左侧或者右侧 | string, boolean | true, left, right | - |
 | sortable | 对应列是否可以排序 | boolean | — | false |
 | resizable | 对应列是否可以通过拖动改变宽度(如果需要,需在 el-table 上设置 border 属性为真) | boolean | — | false |
 | type | 对应列的类型。如果设置了 `selection` 则显示多选框,如果设置了 `index` 则显示该行的索引(从 1 开始计算) | string | selection/index | — |
 | formatter | 用来格式化内容,在 formatter 执行的时候,会传入 row 和 column | function | — | — |
 | show-tooltip-when-overflow | 当过长被隐藏时显示 tooltip | Boolean | — | false |
-| inline-template | 指定该属性后可以自定义 column 模板,参考多选的时间列,通过 row 获取行信息,JSX 里通过 _self 获取当前上下文。此时不需要配置 property 属性  | — | — |
+| inline-template | 指定该属性后可以自定义 column 模板,参考多选的时间列,通过 row 获取行信息,JSX 里通过 _self 获取当前上下文。此时不需要配置 prop 属性  | — | — |
 | align | 对齐方式 | String | left, center, right | left |
+| selectable | 仅对 type=selection 的列有效,类型为 Function,Function 的返回值用来决定这一行的 CheckBox 是否可以勾选 | Function | - |

+ 1 - 1
packages/table/package.json

@@ -11,7 +11,7 @@
   "repository": "https://github.com/element-component/element/tree/master/packages/dialog",
   "author": "elemefe",
   "license": "MIT",
-  "devDependencies": {
+  "dependencies": {
     "throttle-debounce": "^1.0.1"
   }
 }

+ 82 - 65
packages/table/src/table-body.js

@@ -1,6 +1,8 @@
-const getColumnById = function(grid, columnId) {
+import { getValueByPath, getCell } from './util';
+
+const getColumnById = function(table, columnId) {
   let column = null;
-  grid.columns.forEach(function(item) {
+  table.columns.forEach(function(item) {
     if (item.id === columnId) {
       column = item;
     }
@@ -8,26 +10,24 @@ const getColumnById = function(grid, columnId) {
   return column;
 };
 
-const getColumnByCell = function(grid, cell) {
-  const matches = (cell.className || '').match(/grid_[^\s]+/gm);
+const getColumnByCell = function(table, cell) {
+  const matches = (cell.className || '').match(/table_[^\s]+/gm);
   if (matches) {
-    return getColumnById(grid, matches[0]);
+    return getColumnById(table, matches[0]);
   }
   return null;
 };
 
-import { getValueByPath, getCell, orderBy, getChild } from './util';
-
 export default {
   props: {
-    columns: {},
-    data: {},
-    fixed: {},
-    selection: {
-      default() {
-        return [];
-      }
-    }
+    store: {
+      required: true
+    },
+    layout: {
+      required: true
+    },
+    rowClassName: [String, Function],
+    fixed: String
   },
 
   render(h) {
@@ -37,17 +37,20 @@ export default {
         cellspacing="0"
         cellpadding="0"
         border="0">
+        {
+          this._l(this.columns, column =>
+            <colgroup
+              name={ column.id }
+              width={ column.realWidth || column.width }
+            />)
+        }
         <tbody>
           {
             this._l(this.data, (row, $index) =>
               <tr
                 on-click={ ($event) => this.handleClick($event, row) }
                 on-mouseenter={ _ => this.handleMouseEnter($index) }
-                style={ this.getCustomStyle(row) }
-                class={{
-                  'current-row': row === this.$parent.selected,
-                  'hover': this.$parent.$parent.hoverRowIndex === $index
-                }}>
+                class={ this.getRowClass(row, $index) }>
                 {
                   this._l(this.columns, (column) =>
                     <td
@@ -57,11 +60,14 @@ export default {
                       on-mouseleave={ this.handleCellMouseLeave }>
                       {
                         column.template
-                          ? column.template.call(this._renderProxy, h, { row, column, $index, _self: this.$parent.$vnode.context })
-                          : <div class="cell">{ this.$getPropertyText(row, column.property, column.id) }</div>
+                          ? column.template.call(this._renderProxy, h, { row, column, $index, store: this.store, _self: this.$parent.$vnode.context })
+                          : <div class="cell">{ this.getCellContent(row, column.property, column.id) }</div>
                       }
                     </td>
-                  ).concat(this.fixed ? <td class="gutter" /> : '')
+                  )
+                }
+                {
+                  !this.fixed && this.layout.scrollY && this.layout.gutterWidth ? <td class="gutter" /> : ''
                 }
               </tr>
             )
@@ -71,93 +77,104 @@ export default {
     );
   },
 
+  computed: {
+    data() {
+      return this.store.states.data;
+    },
+    hoverRowIndex() {
+      return this.store.states.hoverRow;
+    },
+    currentRow() {
+      return this.store.states.currentRow;
+    },
+    columns() {
+      if (this.fixed === true || this.fixed === 'left') {
+        return this.store.states.fixedColumns;
+      } else if (this.fixed === 'right') {
+        return this.store.states.rightFixedColumns;
+      }
+      return this.store.states.columns;
+    }
+  },
+
   data() {
     return {
-      criteria: this.$parent.customCriteria,
-      colors: this.$parent.customBackgroundColors,
       tooltipDisabled: true
     };
   },
 
-  filters: {
-    orderBy
-  },
-
   methods: {
-    getColumnWhiteSpaceStyle(column) {
-      return column.showTooltipWhenOverflow ? { 'white-space': 'nowrap' } : {};
-    },
+    getRowClass(row, index) {
+      const classes = [];
+      if (row === this.currentRow) {
+        classes.push('current-row');
+      }
+      if (this.hoverRowIndex === index) {
+        classes.push('hover-row');
+      }
 
-    checkProperty(row) {
-      if (this.criteria && this.criteria.length > 0) {
-        for (let i = 0, len = this.criteria.length; i < len; i++) {
-          if (row[this.criteria[i]] === true) {
-            return i;
-          }
-        }
+      const rowClassName = this.rowClassName;
+      if (typeof rowClassName === 'string') {
+        classes.push(rowClassName);
+      } else if (typeof rowClassName === 'function') {
+        classes.push(rowClassName.apply(null, [row, index]) || '');
       }
-      return -1;
+
+      return classes.join(' ');
     },
 
-    getCustomStyle(row) {
-      if (!this.criteria || !this.colors || this.criteria.length !== this.colors.length) {
-        return {};
-      }
-      let criterionIndex = this.checkProperty(row);
-      return criterionIndex > -1 ? { 'background-color': this.colors[criterionIndex] } : {};
+    getColumnWhiteSpaceStyle(column) {
+      return column.showTooltipWhenOverflow ? { 'white-space': 'nowrap' } : {};
     },
 
     handleCellMouseEnter(event, row) {
-      let grid = this.$parent;
+      const table = this.$parent;
       const cell = getCell(event);
 
       if (cell) {
-        const column = getColumnByCell(grid, cell);
-        const hoverState = grid.hoverState = { cell: cell, column: column, row: row };
-        grid.$emit('cellmouseenter', hoverState.row, hoverState.column, hoverState.cell, event);
+        const column = getColumnByCell(table, cell);
+        const hoverState = table.hoverState = { cell, column, row };
+        table.$emit('cellmouseenter', hoverState.row, hoverState.column, hoverState.cell, event);
       }
 
       // 判断是否text-overflow, 如果是就显示tooltip
-      const cellChild = getChild(event);
+      const cellChild = event.target.querySelector('.cell');
 
       this.tooltipDisabled = cellChild.scrollWidth <= cellChild.offsetWidth;
     },
 
     handleCellMouseLeave(event) {
-      let grid = this.$parent;
       const cell = getCell(event);
 
       if (cell) {
-        const oldHoverState = grid.hoverState;
-        grid.$emit('cellmouseleave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
+        const table = this.$parent;
+        const oldHoverState = table.hoverState;
+        table.$emit('cellmouseleave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event);
       }
     },
 
     handleMouseEnter(index) {
-      this.$parent.hoverRowIndex = index;
+      this.store.commit('setHoverRow', index);
     },
 
     handleClick(event, row) {
-      let grid = this.$parent;
+      const table = this.$parent;
       const cell = getCell(event);
 
       if (cell) {
-        const column = getColumnByCell(grid, cell);
+        const column = getColumnByCell(table, cell);
         if (column) {
-          grid.$emit('cellclick', row, column, cell, event);
+          table.$emit('cellclick', row, column, cell, event);
         }
       }
 
-      if (grid.selectionMode === 'single') {
-        grid.selected = row;
-      }
+      this.store.commit('setSelectedRow', row);
 
-      grid.$emit('rowclick', row, event);
+      table.$emit('rowclick', row, event);
     },
 
-    $getPropertyText(row, property, columnId) {
-      let grid = this.$parent;
-      const column = getColumnById(grid, columnId);
+    getCellContent(row, property, columnId) {
+      const column = getColumnById(this.$parent, columnId);
       if (column && column.formatter) {
         return column.formatter(row, column);
       }

+ 65 - 58
packages/table/src/table-column.js

@@ -28,20 +28,39 @@ const defaults = {
 
 const forced = {
   selection: {
-    headerTemplate: function(h) { return <div><el-checkbox nativeOn-click={ this.toggleAllSelection } domProps-value={ this.allSelected } on-input={ ($event) => this.$emit('allselectedchange', $event) } /></div>; },
-    template: function(h, { row }) { return <el-checkbox domProps-value={ row.$selected } on-input={ ($event) => {row.$selected = $event;} } />; },
+    headerTemplate: function(h) {
+      return <div><el-checkbox
+        nativeOn-click={ this.toggleAllSelection }
+        domProps-value={ this.isAllSelected }
+        on-input={ (value) => { this.$emit('allselectedchange', value); } } />
+      </div>;
+    },
+    template: function(h, { row, column, store, $index }) {
+      return <el-checkbox
+        domProps-value={ row.$selected }
+        disabled={ column.selectable ? !column.selectable.call(null, row, $index) : false }
+        on-input={ (value) => { row.$selected = value; store.commit('rowSelectedChanged', row); } } />;
+    },
     sortable: false,
     resizable: false
   },
   index: {
     // headerTemplate: function(h) { return <div>#</div>; },
-    headerTemplate: function(h, label) { return <div>{ label || '#' }</div>; },
-    template: function(h, { row, $index }) { return <div>{ $index + 1 }</div>; },
+    headerTemplate: function(h, label) {
+      return <div>{ label || '#' }</div>;
+    },
+    template: function(h, { row, $index }) {
+      return <div>{ $index + 1 }</div>;
+    },
     sortable: false
   },
   filter: {
-    headerTemplate: function(h) { return <div>#</div>; },
-    template: function(h, { row, column }) { return <el-tag type="primary" style="height: 16px; line-height: 16px; min-width: 40px; text-align: center">{ row[column.property] }</el-tag>; },
+    headerTemplate: function(h) {
+      return <div>#</div>;
+    },
+    template: function(h, { row, column }) {
+      return <el-tag type="primary" style="height: 16px; line-height: 16px; min-width: 40px; text-align: center">{ row[column.property] }</el-tag>;
+    },
     resizable: false
   }
 };
@@ -60,6 +79,12 @@ const getDefaultColumn = function(type, options) {
     }
   }
 
+  if (!column.minWidth) {
+    column.minWidth = 80;
+  }
+
+  column.realWidth = column.width || column.minWidth;
+
   return column;
 };
 
@@ -73,6 +98,7 @@ export default {
     },
     label: String,
     property: String,
+    prop: String,
     width: {},
     minWidth: {},
     template: String,
@@ -89,7 +115,9 @@ export default {
       type: Boolean,
       default: false
     },
-    formatter: Function
+    fixed: [Boolean, String],
+    formatter: Function,
+    selectable: Function
   },
 
   render() {},
@@ -112,16 +140,25 @@ export default {
     ElTag
   },
 
+  computed: {
+    owner() {
+      let parent = this.$parent;
+      while (parent && !parent.tableId) {
+        parent = parent.$parent;
+      }
+      return parent;
+    }
+  },
+
   created() {
     this.customRender = this.$options.render;
     this.$options.render = (h) => h('div');
 
-    let columnId = this.columnId = (this.$parent.gridId || (this.$parent.columnId + '_')) + 'column_' + columnIdSeed++;
+    let columnId = this.columnId = (this.$parent.tableId || (this.$parent.columnId + '_')) + 'column_' + columnIdSeed++;
 
     let parent = this.$parent;
-    if (!parent.gridId) {
-      this.isChildColumn = true;
-    }
+    let owner = this.owner;
+    this.isChildColumn = owner !== parent;
 
     let type = this.type;
 
@@ -139,17 +176,15 @@ export default {
       if (isNaN(minWidth)) {
         minWidth = 80;
       }
-    } else {
-      minWidth = 80;
     }
 
     let isColumnGroup = false;
     let template;
 
-    let property = this.property;
+    let property = this.prop || this.property;
     if (property) {
       template = function(h, { row }, parent) {
-        return <span>{ parent.$getPropertyText(row, property, columnId) }</span>;
+        return <span>{ parent.getCellContent(row, property, columnId) }</span>;
       };
     }
 
@@ -163,11 +198,12 @@ export default {
       width,
       isColumnGroup,
       align: this.align ? 'is-' + this.align : null,
-      realWidth: width || minWidth,
       sortable: this.sortable,
       resizable: this.resizable,
       showTooltipWhenOverflow: this.showTooltipWhenOverflow,
-      formatter: this.formatter
+      formatter: this.formatter,
+      selectable: this.selectable,
+      fixed: this.fixed
     });
 
     objectAssign(column, forced[type] || {});
@@ -186,7 +222,7 @@ export default {
 
           return _self.customRender.call(data);
         };
-      };
+      }
 
       return _self.showTooltipWhenOverflow
         ? <el-tooltip
@@ -203,31 +239,8 @@ export default {
   },
 
   destroyed() {
-    if (!this.$parent) {
-      return;
-    }
-    let columns = this.$parent.columns;
-    if (columns) {
-      let columnId = this.columnId;
-      for (let i = 0, j = columns.length; i < j; i++) {
-        let column = columns[i];
-
-        if (column.id === columnId) {
-          columns.splice(i, 1);
-          break;
-        }
-      }
-    }
-
-    if (this.isChildColumn) {
-      if (this.$parent.$parent.$ready) {
-        this.$parent.$parent.debouncedReRender();
-      }
-    } else {
-      if (this.$parent.$ready) {
-        this.$parent.debouncedReRender();
-      }
-    }
+    if (!this.$parent) return;
+    this.owner.store.commit('removeColumn', this.columnConfig);
   },
 
   watch: {
@@ -237,6 +250,12 @@ export default {
       }
     },
 
+    prop(newVal) {
+      if (this.columnConfig) {
+        this.columnConfig.property = newVal;
+      }
+    },
+
     property(newVal) {
       if (this.columnConfig) {
         this.columnConfig.property = newVal;
@@ -245,8 +264,8 @@ export default {
   },
 
   mounted() {
-    let parent = this.$parent;
-    let columnConfig = this.columnConfig;
+    const owner = this.owner;
+    const parent = this.$parent;
     let columnIndex;
 
     if (!this.isChildColumn) {
@@ -255,18 +274,6 @@ export default {
       columnIndex = [].indexOf.call(parent.$el.children, this.$el);
     }
 
-    parent.columns.splice(columnIndex, 0, columnConfig);
-
-    if (this.isChildColumn) {
-      parent.columnConfig.columns = parent.columns;
-
-      if (parent.$parent.$ready) {
-        parent.$parent.debouncedReRender();
-      }
-    } else {
-      if (parent.$ready) {
-        parent.debouncedReRender();
-      }
-    }
+    owner.store.commit('insertColumn', this.columnConfig, columnIndex);
   }
 };

+ 89 - 79
packages/table/src/table-header.js

@@ -16,47 +16,58 @@ export default {
             <colgroup
               name={ column.id }
               width={ column.realWidth || column.width }
-            />).concat(
-              <thead>
-                <tr>
+            />)
+        }
+        {
+          !this.fixed && this.layout.gutterWidth
+            ? <colgroup name="gutter" width={ this.layout.scrollY ? this.layout.gutterWidth : '' }></colgroup>
+            : ''
+        }
+        <thead>
+          <tr>
+            {
+              this._l(this.columns, column =>
+                <th
+                  on-mousemove={ ($event) => this.handleMouseMove($event, column) }
+                  on-mouseout={ this.handleMouseOut }
+                  on-mousedown={ ($event) => this.handleMouseDown($event, column) }
+                  on-click={ ($event) => this.handleHeaderClick($event, column) }
+                  class={ [column.id, column.direction, column.align] }>
                   {
-                    this._l(this.columns, column =>
-                      <th
-                        on-mousemove={ ($event) => this.handleMouseMove($event, column) }
-                        on-mouseout={ this.handleMouseOut }
-                        on-mousedown={ ($event) => this.handleMouseDown($event, column) }
-                        on-click={ ($event) => this.handleHeaderClick($event, column) }
-                        class={ [column.id, column.direction, column.align] }>
-                        {
-                          [
-                            column.headerTemplate
-                              ? column.headerTemplate.call(this._renderProxy, h, column.label)
-                              : <div>{ column.label }</div>,
-                            column.sortable
-                              ? <div class="caret-wrapper">
-                                  <i class="sort-caret ascending"></i>
-                                  <i class="sort-caret descending"></i>
-                                </div>
-                              : ''
-                          ]
-                        }
-                      </th>
-                    ).concat(this.$parent.showVScrollBar && this.$parent.currentGutterWidth ? <th class="gutter"
-                              style={{ width: this.$parent.currentGutterWidth + 'px' }}></th> : '')
+                    [
+                      column.headerTemplate
+                        ? column.headerTemplate.call(this._renderProxy, h, column.label)
+                        : <div>{ column.label }</div>,
+                      column.sortable
+                        ? <div class="caret-wrapper">
+                            <i class="sort-caret ascending"></i>
+                            <i class="sort-caret descending"></i>
+                          </div>
+                        : ''
+                    ]
                   }
-                </tr>
-              </thead>
-            )
-        }
+                </th>
+              )
+            }
+            {
+              !this.fixed && this.layout.gutterWidth
+                ? <th class="gutter" style={{ width: this.layout.scrollY ? this.layout.gutterWidth + 'px' : '0' }}></th>
+                : ''
+            }
+          </tr>
+        </thead>
       </table>
     );
   },
 
   props: {
     columns: {},
-    fixed: Boolean,
-    allSelected: {
-      default: Boolean
+    fixed: String,
+    store: {
+      required: true
+    },
+    layout: {
+      required: true
     },
     border: Boolean
   },
@@ -66,9 +77,24 @@ export default {
     ElTag
   },
 
+  computed: {
+    isAllSelected() {
+      return this.store.states.isAllSelected;
+    },
+
+    columns() {
+      if (this.fixed === true || this.fixed === 'left') {
+        return this.store.states.fixedColumns;
+      } else if (this.fixed === 'right') {
+        return this.store.states.rightFixedColumns;
+      }
+      return this.store.states.columns;
+    }
+  },
+
   methods: {
-    toggleAllSelection($event) {
-      this.$parent.toggleAllSelection($event);
+    toggleAllSelection() {
+      this.store.commit('toggleAllSelection');
     },
 
     handleMouseDown(event, column) {
@@ -77,19 +103,19 @@ export default {
 
         this.$parent.resizeProxyVisible = true;
 
-        const gridEl = this.$parent.$el;
-        const gridLeft = gridEl.getBoundingClientRect().left;
+        const tableEl = this.$parent.$el;
+        const tableLeft = tableEl.getBoundingClientRect().left;
         const columnEl = this.$el.querySelector(`th.${column.id}`);
         const columnRect = columnEl.getBoundingClientRect();
-        const minLeft = columnRect.left - gridLeft + 30;
+        const minLeft = columnRect.left - tableLeft + 30;
 
         columnEl.classList.add('noclick');
 
         this.dragState = {
           startMouseLeft: event.clientX,
-          startLeft: columnRect.right - gridLeft,
-          startColumnLeft: columnRect.left - gridLeft,
-          gridLeft: gridLeft
+          startLeft: columnRect.right - tableLeft,
+          startColumnLeft: columnRect.left - tableLeft,
+          tableLeft
         };
 
         const resizeProxy = this.$parent.$refs.resizeProxy;
@@ -98,22 +124,20 @@ export default {
         document.onselectstart = function() { return false; };
         document.ondragstart = function() { return false; };
 
-        const mousemove = (event) => {
+        const handleMouseMove = (event) => {
           const deltaLeft = event.clientX - this.dragState.startMouseLeft;
           const proxyLeft = this.dragState.startLeft + deltaLeft;
 
           resizeProxy.style.left = Math.max(minLeft, proxyLeft) + 'px';
         };
 
-        const mouseup = () => {
+        const handleMouseUp = () => {
           if (this.dragging) {
             const finalLeft = parseInt(resizeProxy.style.left, 10);
             const columnWidth = finalLeft - this.dragState.startColumnLeft;
             column.width = column.realWidth = columnWidth;
 
-            this.$nextTick(() => {
-              this.$parent.$calcColumns();
-            });
+            this.store.scheduleLayout();
 
             document.body.style.cursor = '';
             this.dragging = false;
@@ -123,8 +147,8 @@ export default {
             this.$parent.resizeProxyVisible = false;
           }
 
-          document.removeEventListener('mousemove', mousemove);
-          document.removeEventListener('mouseup', mouseup);
+          document.removeEventListener('mousemove', handleMouseMove);
+          document.removeEventListener('mouseup', handleMouseUp);
           document.onselectstart = null;
           document.ondragstart = null;
 
@@ -133,8 +157,8 @@ export default {
           }, 0);
         };
 
-        document.addEventListener('mousemove', mousemove);
-        document.addEventListener('mouseup', mouseup);
+        document.addEventListener('mousemove', handleMouseMove);
+        document.addEventListener('mouseup', handleMouseUp);
       }
     },
 
@@ -146,13 +170,14 @@ export default {
       if (!this.dragging && this.border) {
         let rect = target.getBoundingClientRect();
 
+        var bodyStyle = document.body.style;
         if (rect.width > 12 && rect.right - event.pageX < 8) {
-          document.body.style.cursor = 'col-resize';
+          bodyStyle.cursor = 'col-resize';
           this.draggingColumn = column;
         } else if (!this.dragging) {
-          document.body.style.cursor = '';
+          bodyStyle.cursor = '';
           this.draggingColumn = null;
-          if (column.sortable) document.body.style.cursor = 'pointer';
+          if (column.sortable) bodyStyle.cursor = 'pointer';
         }
       }
     },
@@ -176,14 +201,14 @@ export default {
 
       if (!column.sortable) return;
 
-      const grid = this.$parent;
+      const sortCondition = this.store.states.sortCondition;
 
-      if (grid.sortingColumn !== column) {
-        if (grid.sortingColumn) {
-          grid.sortingColumn.direction = '';
+      if (sortCondition.column !== column) {
+        if (sortCondition.column) {
+          sortCondition.column.direction = '';
         }
-        grid.sortingColumn = column;
-        grid.sortingProperty = column.property;
+        sortCondition.column = column;
+        sortCondition.property = column.property;
       }
 
       if (!column.direction) {
@@ -192,25 +217,12 @@ export default {
         column.direction = 'descending';
       } else {
         column.direction = '';
-        grid.sortingColumn = null;
-        grid.sortingProperty = null;
+        sortCondition.column = null;
+        sortCondition.property = null;
       }
+      sortCondition.direction = column.direction === 'descending' ? -1 : 1;
 
-      grid.sortingDirection = column.direction === 'descending' ? -1 : 1;
-    },
-
-    $setVisibleFilter(property) {
-      if (this.visibleFilter) {
-        this.visibleFilter = null;
-      } else {
-        this.visibleFilter = property;
-      }
-    }
-  },
-
-  watch: {
-    visibleFilter(val) {
-      this.$parent.visibleFilter = val;
+      this.store.commit('changeSortCondition');
     }
   },
 
@@ -218,9 +230,7 @@ export default {
     return {
       draggingColumn: null,
       dragging: false,
-      dragState: {},
-      columnsMap: null,
-      visibleFilter: null
+      dragState: {}
     };
   }
 };

+ 186 - 0
packages/table/src/table-layout.js

@@ -0,0 +1,186 @@
+import { getScrollBarWidth } from './util';
+import Vue from 'vue';
+
+let GUTTER_WIDTH;
+
+class TableLayout {
+  constructor(options) {
+    this.table = null;
+    this.store = null;
+    this.columns = null;
+    this.fit = true;
+
+    this.scrollX = false;
+    this.scrollY = false;
+    this.bodyWidth = null;
+    this.fixedWidth = null;
+    this.rightFixedWidth = null;
+    this.tableHeight = null;
+    this.headerHeight = 44; // Table Header Height
+    this.viewportHeight = null; // Table Height - Scroll Bar Height
+    this.bodyHeight = null; // Table Height - Table Header Height
+    this.fixedBodyHeight = null; // Table Height - Table Header Height - Scroll Bar Height
+
+    if (GUTTER_WIDTH === undefined) {
+      GUTTER_WIDTH = getScrollBarWidth();
+    }
+    this.gutterWidth = GUTTER_WIDTH;
+
+    for (let name in options) {
+      if (options.hasOwnProperty(name)) {
+        this[name] = options[name];
+      }
+    }
+
+    if (!this.table) {
+      throw new Error('table is required for Table Layout');
+    }
+    if (!this.store) {
+      throw new Error('store is required for Table Layout');
+    }
+  }
+
+  syncHeight() {
+    Vue.nextTick(() => {
+      const { bodyWrapper, fixedBodyWrapper } = this.table.$refs;
+
+      // 若非固定列中的某行内容被撑高, 需要固定列中对应行高度与其保持一致
+      const bodyHeight = bodyWrapper.offsetHeight;
+      const fixedBodyHeight = fixedBodyWrapper.offsetHeight;
+
+      if (bodyHeight !== fixedBodyHeight) {
+        const rows = bodyWrapper.querySelectorAll('tr');
+        const fixedRows = fixedBodyWrapper.querySelectorAll('tr');
+
+        [].forEach.call(rows, (row, i) => {
+          const fixedRow = fixedRows[i];
+          const rowHeight = row.offsetHeight;
+          const fixedRowHeight = fixedRow.offsetHeight;
+          if (rowHeight !== fixedRowHeight) {
+            fixedRow.style.height = rowHeight + 'px';
+          }
+        });
+      }
+    });
+  }
+
+  updateScrollY() {
+    const bodyWrapper = this.table.$refs.bodyWrapper;
+    if (this.table.$el && bodyWrapper) {
+      const body = bodyWrapper.querySelector('.el-table__body');
+
+      this.scrollY = body.offsetHeight > bodyWrapper.offsetHeight;
+    }
+  }
+
+  setHeight(height) {
+    if (typeof height === 'string' && /^\d+$/.test(height)) {
+      height = Number(height);
+    }
+
+    const el = this.table.$el;
+    if (!isNaN(height) && el) {
+      el.style.height = height + 'px';
+
+      this.updateHeight();
+    }
+  }
+
+  updateHeight() {
+    const height = this.tableHeight = this.table.$el.clientHeight;
+    const { headerWrapper } = this.table.$refs;
+    if (!headerWrapper) return;
+    const headerHeight = this.headerHeight = headerWrapper.offsetHeight;
+    const bodyHeight = this.bodyHeight = height - headerHeight;
+    this.fixedBodyHeight = this.scrollX ? bodyHeight - this.gutterWidth : bodyHeight;
+    this.viewportHeight = this.scrollX ? height - this.gutterWidth : height;
+  }
+
+  update() {
+    const fit = this.fit;
+    const columns = this.table.columns;
+    const bodyWidth = this.table.$el.clientWidth;
+    let bodyMinWidth = 0;
+
+    const flattenColumns = [];
+    columns.forEach((column) => {
+      if (column.isColumnGroup) {
+        flattenColumns.push.apply(flattenColumns, column.columns);
+      } else {
+        flattenColumns.push(column);
+      }
+    });
+
+    let flexColumns = flattenColumns.filter((column) => typeof column.width !== 'number');
+
+    if (flexColumns.length > 0 && fit) {
+      flattenColumns.forEach((column) => {
+        bodyMinWidth += column.width || column.minWidth || 80;
+      });
+
+      if (bodyMinWidth < bodyWidth - this.gutterWidth) { // DON'T HAVE SCROLL BAR
+        this.scrollX = false;
+
+        const totalFlexWidth = bodyWidth - this.gutterWidth - bodyMinWidth;
+
+        if (flexColumns.length === 1) {
+          flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth;
+        } else {
+          const allColumnsWidth = flexColumns.reduce((prev, column) => prev + (column.minWidth || 80), 0);
+          const flexWidthPerPixel = totalFlexWidth / allColumnsWidth;
+          let noneFirstWidth = 0;
+
+          flexColumns.forEach((column, index) => {
+            if (index === 0) return;
+            const flexWidth = Math.floor((column.minWidth || 80) * flexWidthPerPixel);
+            noneFirstWidth += flexWidth;
+            column.realWidth = (column.minWidth || 80) + flexWidth;
+          });
+
+          flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth - noneFirstWidth;
+        }
+      } else { // HAVE HORIZONTAL SCROLL BAR
+        this.scrollX = true;
+        flexColumns.forEach(function(column) {
+          column.realWidth = column.minWidth;
+        });
+      }
+
+      this.bodyWidth = Math.max(bodyMinWidth, bodyWidth);
+    } else {
+      flattenColumns.forEach((column) => {
+        if (!column.width && !column.minWidth) {
+          column.realWidth = 80;
+        }
+
+        bodyMinWidth += column.realWidth;
+      });
+      this.scrollX = bodyMinWidth > bodyWidth;
+
+      this.bodyWidth = bodyMinWidth;
+    }
+
+    const fixedColumns = this.store.states.fixedColumns;
+
+    if (fixedColumns.length > 0) {
+      let fixedWidth = 0;
+      fixedColumns.forEach(function(column) {
+        fixedWidth += column.realWidth;
+      });
+
+      this.fixedWidth = fixedWidth;
+    }
+
+    const rightFixedColumns = this.store.states.rightFixedColumns;
+    if (rightFixedColumns.length > 0) {
+      let rightFixedWidth = 0;
+      rightFixedColumns.forEach(function(column) {
+        rightFixedWidth += column.realWidth;
+      });
+
+      this.rightFixedWidth = rightFixedWidth;
+    }
+  }
+}
+
+export default TableLayout;

+ 176 - 0
packages/table/src/table-store.js

@@ -0,0 +1,176 @@
+import Vue from 'vue';
+import debounce from 'throttle-debounce/debounce';
+import { orderBy } from './util';
+
+const TableStore = function(table, initialState = {}) {
+  if (!table) {
+    throw new Error('Table is required.');
+  }
+  this.table = table;
+
+  this.states = {
+    _columns: [],
+    columns: [],
+    fixedColumns: [],
+    rightFixedColumns: [],
+    _data: null,
+    data: null,
+    sortCondition: {
+      column: null,
+      property: null,
+      direction: null
+    },
+    isAllSelected: false,
+    selection: null,
+    allowNoSelection: false,
+    selectionMode: 'none',
+    selectable: null,
+    currentRow: null,
+    hoverRow: null
+  };
+
+  for (let prop in initialState) {
+    if (initialState.hasOwnProperty(prop) && this.states.hasOwnProperty(prop)) {
+      this.states[prop] = initialState[prop];
+    }
+  }
+};
+
+TableStore.prototype.mutations = {
+  setData(states, data) {
+    states._data = data;
+    if (data && data[0] && typeof data[0].$selected === 'undefined') {
+      data.forEach((item) => Vue.set(item, '$selected', false));
+    }
+    states.data = orderBy((data || []), states.sortCondition.property, states.sortCondition.direction);
+    this.updateSelectedRow();
+
+    if (states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0) Vue.nextTick(() => this.table.syncHeight());
+    Vue.nextTick(() => this.table.updateScrollY());
+  },
+
+  changeSortCondition(states) {
+    states.data = orderBy((states._data || []), states.sortCondition.property, states.sortCondition.direction);
+
+    if (states.fixedColumns.length > 0 || states.rightFixedColumns.length > 0) Vue.nextTick(() => this.table.syncHeight());
+    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);
+    }
+    if (column.type === 'selection') {
+      states.selectable = column.selectable;
+    }
+
+    this.scheduleLayout();
+  },
+
+  removeColumn(states, column) {
+    let _columns = states._columns;
+    if (_columns) {
+      _columns.splice(_columns.indexOf(column), 1);
+    }
+
+    this.scheduleLayout();
+  },
+
+  setHoverRow(states, row) {
+    states.hoverRow = row;
+  },
+
+  rowSelectedChanged(states) {
+    let isAllSelected = true;
+    const data = states.data || [];
+    for (let i = 0, j = data.length; i < j; i++) {
+      const item = data[i];
+      if (states.selectable) {
+        if (states.selectable.call(null, item, i) && !item.$selected) {
+          isAllSelected = false;
+          break;
+        }
+      } else {
+        if (!item.$selected) {
+          isAllSelected = false;
+          break;
+        }
+      }
+    }
+    states.isAllSelected = isAllSelected;
+  },
+
+  toggleAllSelection: debounce(10, function(states) {
+    const data = states.data || [];
+    const value = !states.isAllSelected;
+    data.forEach((item, index) => {
+      if (states.selectable) {
+        if (states.selectable.call(null, item, index)) {
+          item.$selected = value;
+        }
+      } else {
+        item.$selected = value;
+      }
+    });
+    states.isAllSelected = value;
+  }),
+
+  setSelectedRow(states, row) {
+    if (states.selectionMode === 'single') {
+      states.currentRow = row;
+    }
+  }
+};
+
+TableStore.prototype.updateColumns = function() {
+  const states = this.states;
+  const _columns = states._columns || [];
+  states.fixedColumns = _columns.filter((column) => column.fixed === true || column.fixed === 'left');
+  states.rightFixedColumns = _columns.filter((column) => column.fixed === 'right');
+
+  if (states.fixedColumns.length > 0 && _columns[0] && _columns[0].type === 'selection' && !_columns[0].fixed) {
+    _columns[0].fixed = true;
+    states.fixedColumns.unshift(_columns[0]);
+  }
+  states.columns = [].concat(states.fixedColumns).concat(_columns.filter((column) => !column.fixed)).concat(states.rightFixedColumns);
+};
+
+TableStore.prototype.updateSelectedRow = function() {
+  const states = this.states;
+  const table = this.table;
+  const data = states.data || [];
+  if (states.selectionMode === 'single') {
+    const oldSelectedRow = states.currentRow;
+    if (oldSelectedRow === null && !states.allowNoSelection) {
+      states.currentRow = data[0];
+      if (states.currentRow !== oldSelectedRow) {
+        table.$emit('selectionchange', states.currentRow);
+      }
+    } else if (data.indexOf(oldSelectedRow) === -1) {
+      if (!states.allowNoSelection) {
+        states.currentRow = data[0];
+      } else {
+        states.currentRow = null;
+      }
+      if (states.currentRow !== oldSelectedRow) {
+        table.$emit('selectionchange', states.currentRow);
+      }
+    }
+  }
+};
+
+TableStore.prototype.scheduleLayout = function() {
+  this.table.debouncedLayout();
+};
+
+TableStore.prototype.commit = function(name, ...args) {
+  const mutations = this.mutations;
+  if (mutations[name]) {
+    mutations[name].apply(this, [this.states].concat(args));
+  }
+};
+
+export default TableStore;

+ 144 - 344
packages/table/src/table.vue

@@ -1,38 +1,99 @@
 <template>
-  <div class="el-table" :class="{ 'el-table--fit': fit, 'el-table--striped': stripe, 'el-table--border': border }" @mouseleave="handleMouseLeave($event)">
+  <div class="el-table"
+    :class="{ 'el-table--fit': fit, 'el-table--striped': stripe, 'el-table--border': border }"
+    @mouseleave="handleMouseLeave($event)">
     <div class="hidden-columns" ref="hiddenColumns"><slot></slot></div>
-    <div class="el-table__header-wrapper">
-      <table-header :columns="columns" :all-selected="allSelected" @allselectedchange="handleAllSelectedChange" :selection="selection" :style="{ width: bodyWidth ? bodyWidth + 'px' : '' }" :border="border"></table-header>
+    <div class="el-table__header-wrapper" ref="headerWrapper">
+      <table-header
+        :store="store"
+        :layout="layout"
+        :border="border"
+        :style="{ width: layout.bodyWidth ? layout.bodyWidth + 'px' : '' }">
+      </table-header>
     </div>
-    <div class="el-table__body-wrapper">
-      <table-body :columns="columns" :selection="selection" :data="filterData" :style="{ width: bodyWidth ? bodyWidth - (showVScrollBar ? currentGutterWidth : 0 ) + 'px' : '' }"></table-body>
+    <div class="el-table__body-wrapper" ref="bodyWrapper"
+      :style="{ height: layout.bodyHeight ? layout.bodyHeight + 'px' : '' }">
+      <table-body
+        :store="store"
+        :layout="layout"
+        :row-class-name="rowClassName"
+        :style="{ width: layout.bodyWidth ? layout.bodyWidth - (layout.scrollY ? layout.gutterWidth : 0 ) + 'px' : '' }">
+      </table-body>
     </div>
-    <div class="el-table__fixed" :style="{ width: fixedBodyWidth ? fixedBodyWidth + 'px' : '' }" ref="fixed">
-      <div class="el-table__fixed-header-wrapper" v-if="fixedColumnCount > 0">
-        <table-header :columns="fixedColumns" :all-selected="allSelected" @allselectedchange="handleAllSelectedChange" :selection="selection" :style="{ width: fixedBodyWidth ? fixedBodyWidth + 'px' : '' }" :border="border"></table-header>
+    <div class="el-table__fixed" ref="fixedWrapper"
+      :style="{
+        width: layout.fixedWidth ? layout.fixedWidth + 'px' : '',
+        height: layout.viewportHeight ? layout.viewportHeight + 'px' : ''
+      }">
+      <div class="el-table__fixed-header-wrapper" ref="fixedHeaderWrapper"
+        v-if="fixedColumns.length > 0">
+        <table-header
+          fixed="left"
+          :border="border"
+          :store="store"
+          :layout="layout"
+          :style="{ width: layout.fixedWidth ? layout.fixedWidth + 'px' : '' }"></table-header>
       </div>
-      <div class="el-table__fixed-body-wrapper" v-if="fixedColumnCount > 0" :style="{ top: headerHeight + 'px' }">
-        <table-body :columns="fixedColumns" fixed :selection="selection" :data="filterData" :style="{ width: fixedBodyWidth ? fixedBodyWidth + 'px' : '' }"></table-body>
+      <div class="el-table__fixed-body-wrapper" ref="fixedBodyWrapper"
+        v-if="fixedColumns.length > 0"
+        :style="{
+          top: layout.headerHeight + 'px',
+          height: layout.fixedBodyHeight ? layout.fixedBodyHeight + 'px' : ''
+        }">
+        <table-body
+          fixed="left"
+          :store="store"
+          :layout="layout"
+          :row-class-name="rowClassName"
+          :style="{ width: layout.fixedWidth ? layout.fixedWidth + 'px' : '' }">
+        </table-body>
+      </div>
+    </div>
+    <div class="el-table__fixed-right" ref="rightFixedWrapper"
+      :style="{
+        width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '',
+        height: layout.viewportHeight ? layout.viewportHeight + 'px' : '',
+        right: layout.scrollY ? layout.gutterWidth + 'px' : ''
+      }">
+      <div class="el-table__fixed-header-wrapper" ref="rightFixedHeaderWrapper"
+        v-if="rightFixedColumns.length > 0">
+        <table-header
+          fixed="right"
+          :border="border"
+          :store="store"
+          :layout="layout"
+          :style="{ width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '' }"></table-header>
+      </div>
+      <div class="el-table__fixed-body-wrapper" ref="rightFixedBodyWrapper"
+        v-if="rightFixedColumns.length > 0"
+        :style="{
+          top: layout.headerHeight + 'px',
+          height: layout.fixedBodyHeight ? layout.fixedBodyHeight + 'px' : ''
+        }">
+        <table-body
+          fixed="right"
+          :store="store"
+          :layout="layout"
+          :row-class-name="rowClassName"
+          :style="{ width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '' }">
+        </table-body>
       </div>
     </div>
     <div class="el-table__column-resize-proxy" ref="resizeProxy" v-show="resizeProxyVisible"></div>
-    <slot name="bottom"></slot>
   </div>
 </template>
 
 <script type="text/babel">
   import throttle from 'throttle-debounce/throttle';
   import debounce from 'throttle-debounce/debounce';
-  import { getScrollBarWidth, orderBy } from './util';
-  import objectAssign from 'object-assign';
   import { addResizeListener, removeResizeListener } from './resize-event';
-
-  let gridIdSeed = 1;
-  let GUTTER_WIDTH;
-
+  import TableStore from './table-store';
+  import TableLayout from './table-layout';
   import TableBody from './table-body';
   import TableHeader from './table-header';
 
+  let tableIdSeed = 1;
+
   export default {
     name: 'el-table',
 
@@ -53,39 +114,18 @@
         default: true
       },
 
-      stripe: {
-        type: Boolean,
-        default: false
-      },
-
-      border: {
-        type: Boolean,
-        default: false
-      },
+      stripe: Boolean,
 
-      fixedColumnCount: {
-        type: Number,
-        default: 0
-      },
+      border: Boolean,
 
       selectionMode: {
         type: String,
         default: 'none'
       },
 
-      selection: {},
-
-      allowNoSelection: {
-        type: Boolean,
-        default: false
-      },
-
-      gutterWidth: {
-        default: 0
-      },
+      allowNoSelection: Boolean,
 
-      customCriteria: Array,
-      customBackgroundColors: Array
+      rowClassName: [String, Function]
     },
 
     components: {
@@ -94,371 +134,131 @@
     },
 
     methods: {
-      handleAllSelectedChange(val) {
-        this.allSelected = val;
-      },
-
-      doOnDataChange(data) {
-        data = data || [];
-
-        if (this.selectionMode === 'single') {
-          const oldSelection = this.selected;
-          if (oldSelection === null) {
-            if (!this.allowNoSelection) {
-              this.selected = data[0];
-              if (this.selected !== oldSelection) {
-                this.$emit('selectionchange', this.selected);
-              }
-            }
-          } else if (data.indexOf(oldSelection) === -1) {
-            if (!this.allowNoSelection) {
-              this.selected = data[0];
-            } else {
-              this.selected = null;
-            }
-            if (this.selected !== oldSelection) {
-              this.$emit('selectionchange', this.selected);
-            }
-          }
-        }
-      },
-
-      toggleAllSelection() {
-        setTimeout(() => {
-          this.tableData.forEach(item => {
-            item.$selected = this.allSelected;
-          });
-        }, 0);
-      },
-
-      $calcColumns() {
-        let fit = this.fit;
-        let columns = this.columns;
-
-        let bodyWidth = this.$el.clientWidth;
-        let bodyMinWidth = 0;
-
-        let flattenColumns = [];
-
-        columns.forEach((column) => {
-          if (column.isColumnGroup) {
-            flattenColumns.push.apply(flattenColumns, column.columns);
-          } else {
-            flattenColumns.push(column);
-          }
-        });
-
-        if (fit) {
-          let flexColumns = [];
-          // let definedWidthColumnsWidth = 0;
-          // let definedMinWidthSum = 0;
-
-          flattenColumns.forEach((column) => {
-            // definedMinWidthSum += column.minWidth || 80;
-            bodyMinWidth += column.width || column.minWidth || 80;
-
-            if (typeof column.width === 'number') {
-              // definedWidthColumnsWidth += column.width;
-            } else {
-              flexColumns.push(column);
-            }
-          });
-
-          if (bodyMinWidth < bodyWidth - this.currentGutterWidth) { // do not have scroll bar.
-            let flexWidthTotal = bodyWidth - this.currentGutterWidth - columns.length - bodyMinWidth;
-            let flexWidthPerColumn = Math.floor(flexWidthTotal / flexColumns.length);
-            let flexWidthFirstColumn = flexWidthTotal - flexWidthPerColumn * flexColumns.length + flexWidthPerColumn;
-
-            flexColumns.forEach((column, index) => {
-              if (index === 0) {
-                column.realWidth = (column.minWidth || 80) + flexWidthFirstColumn;
-              } else {
-                column.realWidth = (column.minWidth || 80) + flexWidthPerColumn;
-              }
-            });
-          } else { // need horizontal scroll bar.
-            this.showHScrollBar = true;
-            flexColumns.forEach(function(column) {
-              column.realWidth = column.minWidth;
-            });
-          }
-
-          this.bodyWidth = Math.max(bodyMinWidth, bodyWidth);
-        } else {
-          flattenColumns.forEach((column) => {
-            if (!column.width && !column.minWidth) {
-              column.realWidth = 80;
-            }
-
-            bodyMinWidth += column.realWidth;
-          });
-          this.showHScrollBar = bodyMinWidth > bodyWidth;
-
-          this.bodyWidth = bodyMinWidth;
-        }
-
-        if (this.styleNode) {
-          let styleSheet = this.styleNode.sheet;
-
-          if (!styleSheet) return;
-          for (let i = 0, j = styleSheet.cssRules.length; i < j; i++) {
-            styleSheet.deleteRule(0);
-          }
-
-          columns.forEach(function(column) {
-            const addRule = function(rule) {
-              styleSheet.insertRule(rule, styleSheet.cssRules.length);
-            };
-
-            if (column.isColumnGroup) {
-              let childColumns = column.columns;
-              let groupWidth = 0;
-              childColumns.forEach(function(childColumn) {
-                groupWidth += childColumn.realWidth;
-                addRule(`.${childColumn.id}, .${childColumn.id} > div { width: ${childColumn.realWidth}px; }`);
-              });
-
-              addRule(`.${column.id}, .${column.id} > div { width: ${groupWidth}px; }`);
-            } else {
-              addRule(`.${column.id}, .${column.id} > div { width: ${column.realWidth}px; }`);
-            }
-          });
-        }
-
-        if (this.fixedColumnCount > 0) {
-          let fixedBodyWidth = 0;
-          let fixedColumnCount = this.fixedColumnCount;
-          columns.forEach(function(column, index) {
-            if (index < fixedColumnCount) {
-              fixedBodyWidth += column.realWidth;
-            }
-          });
-
-          this.fixedBodyWidth = fixedBodyWidth;
-        }
-
-        this.$nextTick(() => {
-          this.headerHeight = this.$el.querySelector('.el-table__header-wrapper').offsetHeight;
-        });
+      handleMouseLeave() {
+        this.store.commit('setHoverRow', null);
+        if (this.hoverState) this.hoverState = null;
       },
 
-      $calcHeight(height) {
-        if (typeof height === 'string' && /^\d+$/.test(height)) {
-          height = Number(height);
-        }
-
-        if (!isNaN(height) && this.$el) {
-          const headerHeight = this.headerHeight = this.$el.querySelector('.el-table__header-wrapper').offsetHeight;
-          const bodyHeight = (height - headerHeight);
-          const gridWrapper = this.$el.querySelector('.el-table__body-wrapper');
-          gridWrapper.style.height = bodyHeight + 'px';
-          this.$el.style.height = height + 'px';
-          if (this.$refs.fixed) {
-            this.$refs.fixed.style.height = height + 'px';
-          }
-          const fixedBodyWrapper = this.$el.querySelector('.el-table__fixed-body-wrapper');
-          if (fixedBodyWrapper) {
-            fixedBodyWrapper.style.height = (this.showHScrollBar ? gridWrapper.offsetHeight - this.currentGutterWidth : gridWrapper.offsetHeight) + 'px';
-          }
-        }
+      updateScrollY() {
+        this.layout.updateScrollY();
       },
 
-      handleMouseLeave() {
-        this.hoverRowIndex = null;
-        const hoverState = this.hoverState;
-        if (hoverState) {
-          this.hoverState = null;
-        }
+      syncHeight() {
+        this.layout.syncHeight();
       },
 
-      updateScrollInfo() {
-        this.$nextTick(() => {
-          if (this.$el) {
-            let gridBodyWrapper = this.$el.querySelector('.el-table__body-wrapper');
-            let gridBody = this.$el.querySelector('.el-table__body-wrapper .el-table__body');
-
-            this.showVScrollBar = gridBody.offsetHeight > gridBodyWrapper.offsetHeight;
-          }
+      bindEvents() {
+        const { bodyWrapper, headerWrapper } = this.$refs;
+        const refs = this.$refs;
+        bodyWrapper.addEventListener('scroll', function() {
+          headerWrapper.scrollLeft = this.scrollLeft;
+          if (refs.fixedBodyWrapper) refs.fixedBodyWrapper.scrollTop = this.scrollTop;
+          if (refs.rightFixedBodyWrapper) refs.rightFixedBodyWrapper.scrollTop = this.scrollTop;
         });
-      },
 
-      doRender() {
-        let bodyWrapper = this.$el.querySelector('.el-table__body-wrapper');
-        let headerWrapper = this.$el.querySelector('.el-table__header-wrapper');
-        const el = this.$el;
-
-        if (!this.$ready) {
-          bodyWrapper.addEventListener('scroll', function() {
-            headerWrapper.scrollLeft = this.scrollLeft;
-            let fixedBodyWrapper = el.querySelector('.el-table__fixed-body-wrapper');
-            if (fixedBodyWrapper) {
-              fixedBodyWrapper.scrollTop = this.scrollTop;
-            }
-          });
-        }
-
-        this.$calcColumns();
-
-        if (!this.$ready && this.fit) {
+        if (this.fit) {
           this.windowResizeListener = throttle(50, () => {
-            this.$calcColumns();
+            if (this.$ready) this.doLayout();
           });
           addResizeListener(this.$el, this.windowResizeListener);
         }
+      },
 
+      doLayout() {
+        this.store.updateColumns();
+        this.layout.update();
+        this.updateScrollY();
         this.$nextTick(() => {
           if (this.height) {
-            this.$calcHeight(this.height);
+            this.layout.setHeight(this.height);
+          } else if (this.shouldUpdateHeight) {
+            this.layout.updateHeight();
           }
         });
       }
     },
 
     created() {
-      this.gridId = 'grid_' + gridIdSeed + '_';
-
-      if (GUTTER_WIDTH === undefined) {
-        GUTTER_WIDTH = getScrollBarWidth();
-      }
-      this.currentGutterWidth = GUTTER_WIDTH;
-
-      this.debouncedReRender = debounce(50, () => {
-        this.doRender();
-      });
+      this.tableId = 'el-table_' + tableIdSeed + '_';
+      this.debouncedLayout = debounce(50, () => this.doLayout());
     },
 
     computed: {
+      shouldUpdateHeight() {
+        return typeof this.height === 'number' ||
+          this.fixedColumns.length > 0 ||
+          this.rightFixedColumns.length > 0;
+      },
+
       selection() {
         if (this.selectionMode === 'multiple') {
           const data = this.tableData || [];
           return data.filter(item => item.$selected === true);
         } else if (this.selectionMode === 'single') {
-          return this.selected;
-        } else {
-          return null;
+          return this.store.currentRow;
         }
+        return null;
+      },
+
+      columns() {
+        return this.store.states.columns;
+      },
+
+      tableData() {
+        return this.store.states.data;
       },
 
       fixedColumns() {
-        const columns = this.columns || [];
-        const fixedColumnCount = this.fixedColumnCount;
-        return columns.filter(function(item, index) {
-          return index < fixedColumnCount;
-        });
+        return this.store.states.fixedColumns;
       },
 
-      filterData() {
-        return orderBy(this.tableData, this.sortingProperty, this.sortingDirection);
+      rightFixedColumns() {
+        return this.store.states.rightFixedColumns;
       }
     },
 
     watch: {
-      fixedColumnCount() {
-        this.debouncedReRender();
-      },
-
       selection(val) {
         this.$emit('selectionchange', val);
-        if (this.selectionMode === 'multiple') {
-          this.allSelected = this.tableData.length > 0 && val.length === this.tableData.length;
-        }
-      },
-
-      visibleFilter(val) {
-        this.$broadcast('toggleFilterPopup', val);
       },
 
       height(value) {
-        this.$calcHeight(value);
+        this.layout.setHeight(value);
       },
 
       data: {
         immediate: true,
         handler(val) {
-          if (val && this.selectionMode === 'multiple') {
-            this.tableData = val.map(item => objectAssign({ '$selected': false }, item));
-          } else {
-            this.tableData = val;
-          }
+          this.store.commit('setData', val);
         }
-      },
-
-      tableData(newVal) {
-        this.$nextTick(_ => this.doRender());
-        this.doOnDataChange(newVal);
-        this.updateScrollInfo();
       }
     },
 
     destroyed() {
-      if (this.styleNode) {
-        this.styleNode.parentNode.removeChild(this.styleNode);
-      }
-
-      if (this.windowResizeListener) {
-        removeResizeListener(this.$el, this.windowResizeListener);
-      }
+      if (this.windowResizeListener) removeResizeListener(this.$el, this.windowResizeListener);
     },
 
     mounted() {
-      const styleNode = document.createElement('style');
-      styleNode.type = 'text/css';
-      styleNode.rel = 'stylesheet';
-      styleNode.title = 'Grid Column Style';
-      document.getElementsByTagName('head')[0].appendChild(styleNode);
-
-      this.styleNode = styleNode;
-
-      this.doRender();
+      this.bindEvents();
+      this.doLayout();
 
       this.$ready = true;
-      if (this.tableData) {
-        this.doOnDataChange(this.tableData);
-      }
-      this.updateScrollInfo();
-      if (this.fixedColumnCount > 0) {
-        this.$nextTick(() => {
-          const style = this.$refs.fixed.style;
-          if (!style) return;
-          // style.height = this.$el.clientHeight + 'px';
-          // 存在横向滚动条的时候应该要减去滚动条的高度
-          style.height = (this.showHScrollBar ? this.$el.clientHeight - this.currentGutterWidth : this.$el.clientHeight) + 'px';
-          // 若非固定列中的某行内容被撑高, 需要固定列中对应行高度与其保持一致
-          let bodyHeight = this.$el.querySelector('.el-table__body-wrapper').offsetHeight;
-          let fixedBodyHeight = this.$el.querySelector('.el-table__fixed-body-wrapper').offsetHeight;
-          if (bodyHeight !== fixedBodyHeight) {
-            let bodyTrs = this.$el.querySelector('.el-table__body-wrapper').querySelectorAll('tr');
-            let fixedBodyTrs = this.$el.querySelector('.el-table__fixed-body-wrapper').querySelectorAll('tr');
-            bodyTrs.forEach((tr, index) => {
-              let trHeight = tr.offsetHeight;
-              let fixedTrHeight = fixedBodyTrs[index].offsetHeight;
-              if (trHeight !== fixedTrHeight) {
-                fixedBodyTrs[index].style.height = trHeight + 'px';
-              }
-            });
-          }
-        });
-      }
     },
 
     data() {
+      const store = new TableStore(this, {
+        allowNoSelection: this.allowNoSelection,
+        selectionMode: this.selectionMode
+      });
+      const layout = new TableLayout({
+        store,
+        table: this,
+        fit: this.fit
+      });
       return {
-        tableData: this.data,
-        showHScrollBar: false,
-        showVScrollBar: false,
-        hoverRowIndex: null,
-        headerHeight: 35,
-        selected: null,
-        allSelected: false,
-        columns: [],
-        resizeProxyVisible: false,
-        bodyWidth: '',
-        fixedBodyWidth: '',
-        sortingColumn: null,
-        sortingProperty: null,
-        sortingDirection: 1,
-        visibleFilter: null,
-        currentGutterWidth: this.gutterWidth
+        store,
+        layout,
+        resizeProxyVisible: false
       };
     }
   };

+ 1 - 5
packages/table/src/util.js

@@ -1,4 +1,4 @@
-var scrollBarWidth;
+let scrollBarWidth;
 
 export const getScrollBarWidth = () => {
   if (scrollBarWidth !== undefined) return scrollBarWidth;
@@ -75,7 +75,3 @@ export const orderBy = function(array, sortKey, reverse) {
     return a === b ? 0 : a > b ? order : -order;
   });
 };
-
-export const getChild = function(event) {
-  return event.target.querySelector('.cell');
-};

+ 9 - 1
packages/theme-default/src/table.css

@@ -105,7 +105,7 @@
       box-sizing: border-box;
     }
 
-    @e fixed {
+    @e fixed, fixed-right {
       position: absolute;
       top: 0;
       left: 0;
@@ -123,6 +123,14 @@
       }
     }
 
+    @e fixed-right {
+      top: 0;
+      left: auto;
+      right: 0;
+
+      box-shadow: -1px 0 8px #d3d4d6;
+    }
+
     @e fixed-header-wrapper {
       position: absolute;
       left: 0;