本文同时发布于掘金,地址: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>
);
}