在 React 和 umi.js 中使用 Handsontable 电子表格组件

本文同时发布于掘金,地址:https://juejin.cn/post/6844903928614797319

Handsontable 是一个 js 表格组件,提供了数据绑定、验证、排序、上下文菜单等功能。

它在 7.0 后开始转为付费软件,最后一个开源免费的版本是 6.2.2

@handsontable/react 是 Handsontable 的官方 React 包装器,本项目将结合 官方 React 包装器文档 的全部示例,演示其在 umi.js 中的使用,附带中文注释和线上demo。

安装

其中本项目创建和安装过程如下:

1、创建 umi 项目,安装过程中选择使用 dva

yarn create umi
yarn install

2、安装 Handsontable 社区版 6.2.2 和 React 包装器

npm install handsontable@6.2.2 @handsontable/react

3、在 app.js 中引入 Handsontable 样式中文语言包

import 'handsontable/dist/handsontable.full.css';
import "handsontable/languages/zh-CN";

4、在组件中使用 @handsontable/react

import React from 'react';
import { HotTable } from '@handsontable/react';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: [
        ['', 'Ford', 'Volvo', 'Toyota', 'Honda'],
        ['2016', 10, 11, 12, 13],
        ['2017', 20, 11, 14, 13],
        ['2018', 30, 15, 12, 13],
      ],
    };
  }

  render() {
    return (
      <div>
        <HotTable
          data={this.state.data}
          colHeaders={true}
          rowHeaders={true}
          width="600"
          height="300"
          stretchH="all"
          language="zh-CN"
        />
      </div>
    );
  }
}

export default function() {
  return <App></App>;
}

基础使用

简单示例

import React from 'react';
import { HotTable } from '@handsontable/react';
import Handsontable from 'handsontable';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      handsontableData: Handsontable.helper.createSpreadsheetData(6, 10)
    };
  }

  render() {
    return (
      <div>
        <HotTable
          id="hot"
          data={this.state.handsontableData}
          colHeaders={true}
          rowHeaders={true}
          language="zh-CN"
          />
      </div>
    );
  }
}

export default function() {
  return (
    <App></App>
  );
}

使用单个 setting 属性

import React from 'react';
import { HotTable } from '@handsontable/react';
import Handsontable from 'handsontable';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      handsontableData: Handsontable.helper.createSpreadsheetData(6, 10)
    };
  }

  render() {
    return (
      <div>
        <HotTable settings={{
            data: this.state.handsontableData,
            language: 'zh-CN',
            colHeaders: true,
            rowHeaders: true
          }}/>
      </div>
    );
  }
}

export default function() {
  return (
    <App></App>
  );
}

使用外部按钮控制 table 行为

import React from 'react';
import { HotTable } from '@handsontable/react';
import Handsontable from 'handsontable';

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      settings: {
        language: 'zh-CN',
        data: Handsontable.helper.createSpreadsheetData(15, 20),
        width: 570,
        height: 220,
      },
    };
  }

  // 通过控制 settings 操作表格行为
  handleChange = (setting, states) => {
    return event => {
      this.setState({
        settings: {
          [setting]: states[event.target.checked ? 1 : 0],
        },
      });
    };
  };

  render() {
    return (
      <div>
        <div className="controllers">
          <label>
            <input onChange={this.handleChange('fixedRowsTop', [0, 2])} type="checkbox" />
            Add fixed rows
          </label>
          <br />
          <label>
            <input onChange={this.handleChange('fixedColumnsLeft', [0, 2])} type="checkbox" />
            Add fixed columns
          </label>
          <br />
          <label>
            <input onChange={this.handleChange('rowHeaders', [false, true])} type="checkbox" />
            Enable row headers
          </label>
          <br />
          <label>
            <input onChange={this.handleChange('colHeaders', [false, true])} type="checkbox" />
            Enable column headers
          </label>
          <br />
        </div>
        <HotTable root="hot" settings={this.state.settings} />
      </div>
    );
  }
}

export default function() {
  return <MyComponent></MyComponent>;
}

自定义右键菜单

import React from 'react';
import { HotTable } from '@handsontable/react';
import Handsontable from 'handsontable';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.hotSettings = {
      language: 'zh-CN',
      data: Handsontable.helper.createSpreadsheetData(5, 5),
      colHeaders: true,
      contextMenu: {
        items: { // 右键菜单列表
          row_above: { // Insert row above 自定义名称
            name: '在此上方插入行(自定义的名称)',
          },
          row_below: {}, // Insert row below
          separator: Handsontable.plugins.ContextMenu.SEPARATOR, // 分割线
          clear_custom: { // 自定义的菜单项
            name: '清除所有单元格(自定义菜单)',
            callback: function() {
              // this是Handsontable实例
              this.clear();
            },
          },
        },
      },
    };
  }

  render() {
    return (
      <div>
        <HotTable id="hot" settings={this.hotSettings} />
      </div>
    );
  }
}

export default function() {
  return <App></App>;
}

自定义编辑器

添加了自定义编辑器,使用 input 元素的 placeholder 属性。

自定义编辑器部分的代码:

// 自定义编辑器
class CustomEditor extends Handsontable.editors.TextEditor {
  createElements() {
    super.createElements();
    // 使用 input 元素
    this.TEXTAREA = document.createElement('input');
    // 定义 placeholder 属性
    this.TEXTAREA.setAttribute('placeholder', '自定义的 placeholder');
    this.TEXTAREA.className = 'handsontableInput';
    this.textareaStyle = this.TEXTAREA.style;
    Handsontable.dom.empty(this.TEXTAREA_PARENT);
    this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
  }
}

在 handsontable 中的使用:

import React from 'react';
import { HotTable } from '@handsontable/react';
import Handsontable from 'handsontable';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.hotSettings = {
      language: 'zh-CN',
      startRows: 5,
      columns: [
        {
          editor: CustomEditor
        }
      ],
      colHeaders: true,
      colWidths: 200
    };
  }

  render() {
    return (
      <div>
        <HotTable
          id="hot"
          settings={this.hotSettings}
        />
      </div>
    );
  }
}

export default function() {
  return (
    <App></App>
  );
}

自定义渲染器

它以图像url作为输入,并在单元格中呈现图像。

import React from 'react';
import { HotTable } from '@handsontable/react';
import Handsontable from 'handsontable';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.hotSettings = {
      language: 'zh-CN',
      data: [
        ['A1', 'http://ecx.images-amazon.com/images/I/51bRhyVTVGL._SL50_.jpg'],
        ['A2', 'http://ecx.images-amazon.com/images/I/51gdVAEfPUL._SL50_.jpg'],
      ],
      columns: [
        {},
        {
          renderer: function(instance, td, row, col, prop, value, cellProperties) {
            const escaped = Handsontable.helper.stringify(value);
            let img = null;

            if (escaped.indexOf('http') === 0) {
              img = document.createElement('IMG');
              img.src = value;

              Handsontable.dom.addEvent(img, 'mousedown', function(event) {
                event.preventDefault();
              });

              Handsontable.dom.empty(td);
              td.appendChild(img);
            } else {
              Handsontable.renderers.TextRenderer.apply(this, arguments);
            }

            return td;
          },
        },
      ],
      colHeaders: true,
      rowHeights: 55,
    };
  }

  render() {
    return (
      <div>
        <HotTable id="hot" settings={this.hotSettings} />
      </div>
    );
  }
}

export default function() {
  return <App></App>;
}

动态切换语言

效果:从表格上方的选择器中选择一种语言,然后打开上下文菜单查看结果。

通过 Handsontable.languages.getLanguagesDictionaries() 获取所有语言,并通过选择动态改变语言设置。

需要注意,返回的语言跟引入的语言包有关,需要在公共位置引入:

import "handsontable/languages/zh-CN";

示例代码如下:

import React from 'react';
import { HotTable } from '@handsontable/react';
import Handsontable from 'handsontable';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.id = 'hot';
    this.state = {
      hotSettings: {
        data: Handsontable.helper.createSpreadsheetData(5, 10),
        colHeaders: true,
        rowHeaders: true,
        contextMenu: true,
      },
      language: 'zh-CN',
    };

    this.updateHotLanguage = this.updateHotLanguage.bind(this);
  }

  componentDidMount() {
    this.getAllLanguageOptions();
  }

  getAllLanguageOptions() {
    // 需要引入语言包,即可获取到对应的语言
    const allDictionaries = Handsontable.languages.getLanguagesDictionaries();
    console.log(allDictionaries,'allDictionaries')
    const langSelect = document.querySelector('#languages');
    langSelect.innerHTML = '';

    for (let language of allDictionaries) {
      langSelect.innerHTML += `<option value="${language.languageCode}">${language.languageCode}</option>`;
    }
  }

  updateHotLanguage(event) {
    this.setState({ language: event.target.value });
  }

  render() {
    return (
      <div>
        <label htmlFor="languages">选择语言: </label>
        <select onChange={this.updateHotLanguage} id="languages"></select>
        <br />
        <br />
        <HotTable id={this.id} language={this.state.language} settings={this.state.hotSettings} />
      </div>
    );
  }
}

export default function() {
  return <App></App>;
}

Redux / Dva 示例

效果:使用Redux状态管理器实现设置,并通过开关控制readOnly切换。

使用了 umi.js ,这里当然也顺便通过 dva 来实现该案例的效果。

建立 model/setting.js 如下:

import Handsontable from 'handsontable';

export default {
  namespace: 'setting',
  state: {
    data: Handsontable.helper.createSpreadsheetData(5, 3),
    colHeaders: true,
    rowHeaders: true,
    readOnly: false,
  },
  reducers: {
    save(state, action) {
      return { ...state, ...action.payload };
    },
  },
  effects: {
    *updateData(
      {
        payload: { dataChanges },
      },
      { select, put },
    ) {
      const data = yield select(state => state.setting.data);
      const newData = data.slice(0);

      // eslint-disable-next-line no-unused-vars
      for (let [row, column, oldValue, newValue] of dataChanges) {
        newData[row][column] = newValue;
      }
      yield put({
        type: 'save',
        payload: {
          data: newData,
        },
      });
    },
    *updateReadOnly({ payload: { readOnly } }, { put }) {
      console.log(readOnly, 'readOnly');
      yield put({
        type: 'save',
        payload: {
          readOnly,
        },
      });
    },
  },
};

组件代码:

import React from 'react';
import { connect } from 'dva';
import { HotTable } from '@handsontable/react';

class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    this.toggleReadOnly = this.toggleReadOnly.bind(this);
    this.hotTableComponent = React.createRef();
  }

  componentDidMount() {
    this.updateReduxPreview();
  }

  componentDidUpdate() {
    this.updateReduxPreview();
  }

  onBeforeHotChange = (changes, source) => {
    const { dispatch } = this.props;
    dispatch({
      type: 'setting/updateData',
      payload: {
        dataChanges: changes,
      },
    });
    return false;
  };

  toggleReadOnly = event => {
    const { dispatch } = this.props;
    dispatch({
      type: 'setting/updateReadOnly',
      payload: {
        readOnly: event.target.checked,
      },
    });
  };

  updateReduxPreview() {
    // 此方法仅用作 Redux 的状态的渲染
    const previewTable = document.querySelector('#redux-preview table');
    let newInnerHtml = '<tbody>';

    for (const [key, value] of Object.entries(this.props.setting)) {
      newInnerHtml += `<tr><td>`;

      if (key === 'data' && Array.isArray(value)) {
        newInnerHtml += `<strong>data:</strong> <br><table style="border: 1px solid #d6d6d6;"><tbody>`;

        for (let row of value) {
          newInnerHtml += `<tr>`;

          for (let cell of row) {
            newInnerHtml += `<td>${cell}</td>`;
          }

          newInnerHtml += `</tr>`;
        }
        newInnerHtml += `</tbody></table>`;
      } else {
        newInnerHtml += `<strong>${key}:</strong> ${value}`;
      }
      newInnerHtml += `</td></tr>`;
    }
    newInnerHtml += `</tbody>`;

    previewTable.innerHTML = newInnerHtml;
  }

  render() {
    return (
      <div className="redux-example-container">
        <div id="example-container">
          <div id="example-preview" className="hot">
            <div id="toggle-boxes">
              <br />
              <input onClick={this.toggleReadOnly} id="readOnlyCheck" type="checkbox" />
              <label htmlFor="readOnlyCheck">
                {' '}
                切换 <code>readOnly</code>
              </label>
            </div>
            <br />
            <HotTable
              ref={this.hotTableComponent}
              beforeChange={this.onBeforeHotChange}
              settings={this.props.setting}
            />
          </div>
          <div id="redux-preview" className="table-container">
            <h4>dva store 数据:</h4>
            <table></table>
          </div>
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    setting: state.setting,
  };
}

export default connect(mapStateToProps)(MyComponent);

Referencing the Handsontable instance - 获取 Handsontable 实例

示范了如何从包装器引用Handsontable实例:

  • 为我们的react组件创建一个 ref 为 this.hotTableComponent
  • this.hotTableComponent.current 获取到 wrapper 组件的 ref
  • 对于 wrapper 组件来说, Handsontable 的实例在其 hotInstance 属性
this.hotTableComponent.current.hotInstance.loadData([['new', 'data']]);

完整代码如下:

import React from 'react';
import { HotTable } from '@handsontable/react';
import Handsontable from 'handsontable';
class App extends React.Component {
  constructor(props) {
    super(props);

    this.id = 'hot';
    this.hotSettings = {
      language: 'zh-CN',
      data: Handsontable.helper.createSpreadsheetData(4, 4),
      colHeaders: true
    };
    this.hotTableComponent = React.createRef();
  }

  swapHotData = ()=> {
    // this.hotTableComponent.current 获取到 wrapper 组件的 ref
    // 对于 wrapper 组件来说, Handsontable 的实例在其 hotInstance 属性
    this.hotTableComponent.current.hotInstance.loadData([['new', 'data']]);
  }

  render() {
    return (
      <div>
        <button onClick={this.swapHotData}>加载新数据!</button>
        <br/>
        <HotTable ref={this.hotTableComponent} id={this.id} settings={this.hotSettings}/>
      </div>
    );
  }
}

export default function() {
  return (
    <App></App>
  );
}
本文收录于专栏
收集一些好用的前端开源库,主要是 npm 包