Skip to content

Commit 7c92447

Browse files
Refactor/spread sheet (#325)
* refactor: refactor Spreadsheet with hooks * test: supply Spreadsheet unit test * docs: optimized Spreadsheet demos * refactor: modify SpreadSheet component the type of _timer
1 parent 2f159bc commit 7c92447

File tree

5 files changed

+151
-140
lines changed

5 files changed

+151
-140
lines changed
Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,65 @@
11
import React from 'react';
22
import SpreadSheet from '../index';
3-
import { render } from '@testing-library/react';
3+
import { render, fireEvent } from '@testing-library/react';
44
import '@testing-library/jest-dom/extend-expect';
55

6+
// mock the CopyUtils
7+
jest.mock('../../utils/copy', () => {
8+
class CopyUtilsMock {
9+
copy = jest.fn();
10+
}
11+
return CopyUtilsMock;
12+
});
13+
614
describe('test spreadSheet ', () => {
15+
const columns = ['name', 'gender', 'age', 'address'];
16+
const data = [
17+
['zhangsan', 'male', '20', 'xihu'],
18+
['lisi', 'male', '18', 'yuhang'],
19+
];
720
test('should render SpreadSheet custom className', () => {
8-
const { container } = render(
9-
<SpreadSheet
10-
columns={['name', 'gender', 'age', 'address']}
11-
data={[
12-
['zhangsan', 'male', '20', 'xihu'],
13-
['lisi', 'male', '18', 'yuhang'],
14-
]}
15-
className="testSpreadSheet"
16-
/>
21+
const { container, getByText, unmount } = render(
22+
<SpreadSheet columns={columns} data={data} className="testSpreadSheet" />
1723
);
1824
expect(container.firstChild).toHaveClass('testSpreadSheet');
25+
expect(getByText('zhangsan')).toBeInTheDocument();
26+
unmount();
27+
});
28+
29+
test('renders without data', () => {
30+
const { getByText, unmount } = render(<SpreadSheet data={[]} columns={columns} />);
31+
expect(getByText('暂无数据')).toBeInTheDocument();
32+
unmount();
33+
});
34+
35+
test('copy value without header', () => {
36+
const { getByText, unmount } = render(<SpreadSheet data={data} columns={columns} />);
37+
const cell = getByText('zhangsan');
38+
fireEvent.contextMenu(cell);
39+
const copyBtn = getByText('复制');
40+
expect(copyBtn).toBeInTheDocument();
41+
fireEvent.click(copyBtn);
42+
unmount();
43+
});
44+
45+
test('copy value with header', () => {
46+
const { getByText, unmount } = render(
47+
<SpreadSheet data={data} columns={columns} options={{ showCopyWithHeader: true }} />
48+
);
49+
const cell = getByText('zhangsan');
50+
fireEvent.contextMenu(cell);
51+
const copyBtn = getByText('复制值以及列名');
52+
expect(copyBtn).toBeInTheDocument();
53+
fireEvent.click(copyBtn);
54+
unmount();
55+
});
56+
57+
test('should call componentDidUpdate when props are updated', () => {
58+
const rerenderData = [['wangwu', 'male', '18', 'yuhang']];
59+
jest.useFakeTimers();
60+
const { rerender, getByText } = render(<SpreadSheet data={data} columns={columns} />);
61+
rerender(<SpreadSheet data={rerenderData} columns={columns} />);
62+
jest.runAllTimers();
63+
expect(getByText('wangwu')).toBeInTheDocument();
1964
});
2065
});

src/spreadSheet/demos/basic.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import { SpreadSheet } from 'dt-react-component';
3+
4+
export default () => {
5+
return (
6+
<SpreadSheet
7+
columns={['name', 'gender', 'age', 'address']}
8+
data={[
9+
['zhangsan', 'male', '20', 'xihu'],
10+
['lisi', 'male', '18', 'yuhang'],
11+
]}
12+
/>
13+
);
14+
};

src/spreadSheet/index.md

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,15 @@ demo:
1414

1515
## 示例
1616

17-
```jsx
18-
/**
19-
* title: "基础使用"
20-
*/
21-
import React from 'react';
22-
import { SpreadSheet } from 'dt-react-component';
23-
24-
export default () => {
25-
return (
26-
<SpreadSheet
27-
columns={['name', 'gender', 'age', 'address']}
28-
data={[
29-
['zhangsan', 'male', '20', 'xihu'],
30-
['lisi', 'male', '18', 'yuhang'],
31-
]}
32-
/>
33-
);
34-
};
35-
```
17+
<code src="./demos/basic.tsx">基础使用</code>
3618

3719
## API
3820

21+
### SpreadSheet
22+
3923
| 参数 | 说明 | 类型 | 默认值 |
4024
| -------------------------- | -------------------------------------- | ----------------- | ------ |
4125
| data | 表格数据 | `Array(二维数组)` | - |
4226
| columns | 列名 | `Array` | - |
27+
| className | 外层组件的 class 名 | `string` | - |
4328
| options.showCopyWithHeader | 右键菜单中是否展示“复制值以及列名”按钮 | `boolean` | - |

src/spreadSheet/index.tsx

Lines changed: 69 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useEffect, useRef } from 'react';
22

33
import CopyUtils from '../utils/copy';
44
import { HotTable } from '@handsontable/react';
@@ -7,7 +7,7 @@ import 'handsontable/dist/handsontable.full.css';
77
import 'handsontable/languages/zh-CN.js';
88
import './style.scss';
99

10-
export interface SpreadSheetProps {
10+
export interface ISpreadSheetProps {
1111
data: Array<Array<string>>;
1212
columns: any;
1313
className?: string;
@@ -16,77 +16,71 @@ export interface SpreadSheetProps {
1616
};
1717
}
1818

19-
class SpreadSheet extends React.PureComponent<SpreadSheetProps, any> {
20-
tableRef: any = React.createRef();
21-
copyUtils = new CopyUtils();
22-
_renderTimer: any;
19+
const SpreadSheet: React.FC<ISpreadSheetProps> = ({ data, columns = [], className, options }) => {
20+
const tableRef = useRef<any>(null);
21+
const copyUtils = new CopyUtils();
22+
const _timer = useRef<number>();
2323

24-
componentDidUpdate(prevProps: any, _prevState: any) {
25-
if (prevProps != this.props) {
26-
if (this.tableRef) {
27-
this.removeRenderClock();
28-
this._renderTimer = setTimeout(() => {
29-
console.log('render sheet');
30-
this.tableRef.current.hotInstance.render();
31-
}, 100);
32-
}
24+
useEffect(() => {
25+
if (tableRef.current) {
26+
removeRenderClock();
27+
_timer.current = window.setTimeout(() => {
28+
tableRef.current.hotInstance.render();
29+
}, 100);
3330
}
34-
}
35-
removeRenderClock() {
36-
if (this._renderTimer) {
37-
clearTimeout(this._renderTimer);
38-
}
39-
}
40-
componentWillUnmount() {
41-
this.removeRenderClock();
42-
}
43-
getData() {
44-
const { data, columns = [] } = this.props;
31+
return () => {
32+
removeRenderClock();
33+
};
34+
}, [data, columns]);
35+
36+
const removeRenderClock = () => {
37+
clearTimeout(_timer.current);
38+
};
39+
40+
const getData = () => {
4541
let showData = data;
46-
if (!showData || !showData.length) {
42+
if (!showData?.length) {
4743
const emptyArr = new Array(columns.length).fill('', 0, columns.length);
4844
emptyArr[0] = '暂无数据';
4945
showData = [emptyArr];
5046
}
5147
return showData;
52-
}
53-
getMergeCells() {
54-
const { data, columns = [] } = this.props;
55-
if (!data || !data.length) {
48+
};
49+
50+
const getMergeCells = () => {
51+
if (!data?.length) {
5652
return [{ row: 0, col: 0, rowspan: 1, colspan: columns.length }];
5753
}
58-
return null;
59-
}
60-
getCell() {
61-
const { data } = this.props;
54+
};
55+
56+
const getCell = () => {
6257
if (!data || !data.length) {
6358
return [{ row: 0, col: 0, className: 'htCenter htMiddle' }];
6459
}
65-
return null;
66-
}
67-
beforeCopy(arr: any, _arr2?: any) {
60+
};
61+
62+
const beforeCopy = (arr: any[]) => {
6863
/**
6964
* 去除格式化
7065
*/
7166
const value = arr
72-
.map((row: any) => {
67+
.map((row: any[]) => {
7368
return row.join('\t');
7469
})
7570
.join('\n');
76-
this.copyUtils.copy(value);
71+
copyUtils.copy(value);
7772
return false;
78-
}
79-
getContextMenu() {
80-
const that = this;
81-
const { columns = [], options } = this.props;
82-
const items: any = {
73+
};
74+
75+
const getContextMenu = () => {
76+
const items: Record<string, { name: string; callback: Function }> = {
8377
copy: {
8478
name: '复制',
8579
callback: function (this: any, _key: any) {
8680
const indexArr = this.getSelected();
8781
// eslint-disable-next-line prefer-spread
8882
const copyDataArr = this.getData.apply(this, indexArr[0]);
89-
that.beforeCopy(copyDataArr);
83+
beforeCopy(copyDataArr);
9084
},
9185
},
9286
};
@@ -106,7 +100,7 @@ class SpreadSheet extends React.PureComponent<SpreadSheetProps, any> {
106100
if (columnArr) {
107101
copyDataArr = [columnArr, ...copyDataArr];
108102
}
109-
that.beforeCopy(copyDataArr);
103+
beforeCopy(copyDataArr);
110104
},
111105
};
112106
// 目前版本不支持 copy_with_column_headers 暂时用 cut 代替,以达到与copy类似的表现
@@ -115,41 +109,33 @@ class SpreadSheet extends React.PureComponent<SpreadSheetProps, any> {
115109
return {
116110
items,
117111
} as any;
118-
}
112+
};
113+
114+
return (
115+
<HotTable
116+
ref={tableRef}
117+
className={classNames('dtc-handsontable-no-border', className)}
118+
language="zh-CN"
119+
// 空数组情况,不显示colHeaders,否则colHeaders默认会按照 A、B...显示
120+
// 具体可见 https://handsontable.com/docs/7.1.1/Options.html#colHeaders
121+
colHeaders={columns?.length > 0 ? columns : false}
122+
data={getData()}
123+
mergeCells={getMergeCells()}
124+
cell={getCell()}
125+
readOnly
126+
rowHeaders // 数字行号
127+
fillHandle={false} // 拖动复制单元格
128+
manualRowResize // 拉伸功能
129+
manualColumnResize // 拉伸功能
130+
autoColumnSize
131+
colWidths={200}
132+
beforeCopy={beforeCopy}
133+
beforeCut={() => false}
134+
columnHeaderHeight={25}
135+
contextMenu={getContextMenu()}
136+
stretchH="all" // 填充空白区域
137+
/>
138+
);
139+
};
119140

120-
render() {
121-
const { columns = [], className = '' } = this.props;
122-
const showData = this.getData();
123-
// 空数组情况,不显示colHeaders,否则colHeaders默认会按照 A、B...显示
124-
// 具体可见 https://handsontable.com/docs/7.1.1/Options.html#colHeaders
125-
let isShowColHeaders = false;
126-
if (columns && columns.length > 0) {
127-
isShowColHeaders = true;
128-
}
129-
return (
130-
// @ts-ignore
131-
<HotTable
132-
ref={this.tableRef}
133-
className={classNames('dtc-handsontable-no-border', className)}
134-
style={{ width: '100%' }}
135-
language="zh-CN"
136-
colHeaders={isShowColHeaders ? columns : false}
137-
data={showData}
138-
mergeCells={this.getMergeCells()}
139-
cell={this.getCell()}
140-
readOnly
141-
rowHeaders // 数字行号
142-
fillHandle={false} // 拖动复制单元格
143-
manualRowResize // 拉伸功能
144-
manualColumnResize // 拉伸功能
145-
colWidths={200}
146-
beforeCopy={this.beforeCopy.bind(this)}
147-
beforeCut={() => false}
148-
columnHeaderHeight={25}
149-
contextMenu={this.getContextMenu()}
150-
stretchH="all" // 填充空白区域
151-
/>
152-
);
153-
}
154-
}
155141
export default SpreadSheet;

src/spreadSheet/style.scss

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,11 @@
1-
$prefix: "dtc-slide-pane";
2-
3-
.#{$prefix} {
4-
background: #FFF;
5-
border-left: 1px solid #DDD;
6-
border-bottom: 1px solid #DDD;
7-
box-shadow: -2px 0 2px 0 rgba(0, 0, 0, 0.1);
8-
position: absolute;
9-
display: inline-block;
10-
transition: transform 0.3s ease-in;
11-
z-index: 99;
12-
&.l-#{$prefix}--fixed {
13-
position: fixed;
14-
top: 0;
15-
right: 0;
16-
}
17-
.#{$prefix}-content {
18-
position: relative;
19-
}
20-
21-
.#{$prefix}-icon {
22-
width: 12px;
23-
height: 56px;
24-
position: absolute;
25-
top: calc(50% - 28px);
26-
left: 0;
27-
z-index: 999;
28-
cursor: pointer;
1+
.dtc-handsontable-no-border {
2+
width: 100%;
3+
.handsontable {
4+
thead tr:first-child th {
5+
border-top: 0;
6+
}
7+
th:first-child {
8+
border-left: 0;
9+
}
2910
}
3011
}

0 commit comments

Comments
 (0)