localeCode: en-US order: 61 category: Show title: Table subTitle: Table icon: doc-table dir: column
Into the header. columns
And data. DataSource
To render.
Please provide a "key" for each data item in the dataSource that is different from the value of the other data items, or use the row Key parameter to specify an attribute name as the primary key, alternative row operation functions such as row selection and expansion of the table.
import React from 'react';
import { Table } from '@douyinfe/semi-ui';
function App() {
const columns = [
{
title: 'Title',
dataIndex: 'name',
},
{
title: 'Size',
dataIndex: 'size',
},
{
title: 'Owner',
dataIndex: 'owner',
},
{
title: 'Update',
dataIndex: 'updateTime',
}
];
const data = [
{
key: '1',
name: 'Semi Design design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
];
return <Table columns={columns} dataSource={data} pagination={false} />;
}
For tables, the two most basic parameters are dataSource
and columns
, the former is the data item, the latter is the configuration of each column, both are array types.
import React from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
function App() {
const columns = [
{
title: 'Title',
dataIndex: 'name',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
}
},
{
title: 'Size',
dataIndex: 'size',
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
}
},
];
const data = [
{
key: '1',
name: 'Semi Design design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
];
return <Table columns={columns} dataSource={data} pagination={false} />;
}
render(App);
You can also use JSX syntax definitions columns
, note that Table only supports columns
JSX syntax definition. You cannot use any component package Table.Column
.
Note: JSX-written tables are not supported the parameter
Resizable
.
<div>When columns are written in JSX, please do not use it at the same time as the configuration method; if used at the same time, only the configuration method will take effect, and no aggregation operation will be performed.</div>
import React from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
const { Column } = Table;
function App() {
const data = [
{
key: '1',
name: 'Semi Design design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
];
const renderName = (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
};
const renderOwner = (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
};
return (
<Table dataSource={data} pagination={false}>
<Column title="Title" dataIndex="name" key="name" render={renderName} />
<Column title="Size" dataIndex="size" key="size" />
<Column title="Owner" dataIndex="owner" key="owner" render={renderOwner} />
<Column title="Update" dataIndex="updateTime" key="updateTime" />
<Column title="" dataIndex="operate" key="operate" render={() => <IconMore />} />
</Table>
);
}
render(App);
This feature can be turned on by passing in rowSelection
.
dataSource
that are not in the state of disabled
will be selected. The callback function for selecting all rows is onSelectAll
;onSelect
;Note: Be sure to provide a "key" for each row of data that is different from other row values, or use the rowKey parameter to specify a property name as the primary key.
import React from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
function App() {
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
}
},
{
title: 'Size',
dataIndex: 'size',
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
}
},
];
const data = [
{
key: '1',
name: 'Semi Design design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
{
key: '4',
name: 'Semi Pro design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '5',
name: 'Semi Pro share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '6',
name: 'Semi Pro Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
];
const rowSelection = {
getCheckboxProps: record => ({
disabled: record.name === 'Design docs', // Column configuration not to be checked
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
const pagination = useMemo(() => ({
pageSize: 3
}), []);
return <Table columns={columns} dataSource={data} rowSelection={rowSelection} pagination={pagination} />;
}
render(App);
Users can use Column.render to customize the rendering of a column of cells, which is suitable for rendering more complex cell content.
import React from 'react';
import { Table, Avatar, Button, Empty, Typography } from '@douyinfe/semi-ui';
import { IconDelete } from '@douyinfe/semi-icons';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
const { Text } = Typography;
const raw = [
{
key: '1',
name: 'Semi Design design draft title may be a bit long Tooltip should be displayed at this time.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
{
key: '4',
name: 'Semi Pro design draft title may be a bit long Tooltip should be displayed at this time.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Jiang Qi',
updateTime: '2020-01-26 11:01',
avatarBg: 'green'
}
];
function App() {
const [dataSource, setData] = useState(raw);
const removeRecord = (key) => {
let newDataSource = [...dataSource];
if (key != null) {
let idx = newDataSource.findIndex(data => data.key === key);
if (idx > -1) {
newDataSource.splice(idx, 1);
setData(newDataSource);
}
}
};
const resetData = () => {
const newDataSource = [...raw];
setData(newDataSource);
};
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<span style={{ display: 'flex', alignItems: 'center' }}>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{/* The width calculation method is the cell setting width minus the non-text content width */}
<Text heading={5} ellipsis={{ showTooltip: true }} style={{ width: 'calc(400px - 76px)' }}>
{text}
</Text>
</span>
);
}
},
{
title: 'Size',
dataIndex: 'size',
width: 150,
},
{
title: 'Owner',
dataIndex: 'owner',
width: 300,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
width: 200,
},
{
title: '',
dataIndex: 'operate',
render: (text, record) => <Button icon={<IconDelete />} theme='borderless' onClick={() => removeRecord(record.key)} />
},
];
const empty = (
<Empty
image={<IllustrationNoResult />}
darkModeImage={<IllustrationNoResultDark />}
description={'No result'}
/>
);
return (
<>
<Button onClick={resetData} style={{ marginBottom: 10 }}>Reset</Button>
<Table style={{ minHeight: 350 }} columns={columns} dataSource={dataSource} pagination={false} empty={empty} />
</>
);
}
render(App);
Table paging currently supports two modes: controlled and uncontrolled.
Note: The custom
pagination.total
field passed in uncontrolled conditions is supported only after version 0.25.0.
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
const DAY = 24 * 60 * 60 * 1000;
function App() {
const [dataSource, setData] = useState([]);
const rowSelection = useMemo(() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: record => ({
disabled: record.name === 'Michael James', // Column configuration not to be checked
name: record.name,
}),
}), []);
const scroll = useMemo(() => ({ y: 300 }), []);
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}
render(App);
Under normal circumstances, the data is often not obtained at one time. We will retrieve the data from the interface when clicking on the page number, filter or sort button. In this case, please use Controlled mode To handle pagination. The user needs to pass in the pagination.currentPage
field, where the rendering of the pagination component depends entirely on the incoming pagination object.
<div>1. When pagination is an object type, literal is not recommended because it causes the table to render to its original state (it looks like the pager is not working). Please try to define reference type parameters outside the render method. If hooks are used, please use useMemo or useState for storage.</div>
<div>2. In the controlled mode, Table will not paginate dataSource, please pass in current page data to dataSource</div>
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const pageSize = 5;
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
const data = getData();
function App() {
const [dataSource, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [currentPage, setPage] = useState(1);
const fetchData = (currentPage = 1) => {
setLoading(true);
setPage(currentPage);
return new Promise((res, rej) => {
setTimeout(() => {
const data = getData();
let dataSource = data.slice((currentPage - 1) * pageSize, currentPage * pageSize);
res(dataSource);
}, 300);
}).then(dataSource => {
setLoading(false);
setData(dataSource);
});
};
const handlePageChange = page => {
fetchData(page);
};
useEffect(() => {
fetchData();
}, []);
return (
<Table
columns={columns}
dataSource={dataSource}
pagination={{
currentPage,
pageSize: 5,
total: data.length,
onPageChange: handlePageChange
}}
loading={loading}
/>
);
}
render(App);
You can fix the column by setting the Fixed attribute of the column and scroll.x, and fix the header by setting scroll.y.
- Make sure that all elements inside the table do not affect the height of the cells after rendering (e.g. containing unloaded pictures, etc.). In this case, give the stator element a definite height to ensure that the left and right Fixed columns of cells are not deranged.
- If the column header is not aligned with the content or there is a column duplication, specify the width width of the fixed column. If the specified width is not effective, try to recommend leaving a column with no width to accommodate the elastic layout, or check for ultra-long continuous fields to destroy the layout.
- It is recommended to specify scroll.x as a fixed value or percentage greater than the width of the table. It is recommended to set a fixed value of
>= the sum of all fixed column widths + the sum of all table column widths
.
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: 'Title',
dataIndex: 'name',
fixed: true,
width: 250,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
width: 200,
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
width: 200,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
width: 200,
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
render: () => {
return <IconMore />;
}
},
];
function App() {
const [dataSource, setData] = useState([]);
const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
const rowSelection = useMemo(() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: record => ({
disabled: record.name === 'Michael James', // Column configuration not to be checked
name: record.name,
}),
fixed: true,
}), []);
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}
render(App);
Filters and sorting controls are integrated inside the table, and users can pass in the sorter display of the sorter open header by passing filters in Column and the filter control display of the onFilter open header.
Note: Be sure to provide a "key" for each row of data that is different from other row values, or use the rowKey parameter to specify a property name as the primary key.
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
sorter: (a, b) => a.name.length - b.name.length > 0 ? 1 : -1,
},
{
title: 'Size',
dataIndex: 'size',
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
function App() {
const [dataSource, setData] = useState([]);
const rowSelection = useMemo(() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: record => ({
disabled: record.name === 'Michael James', // Column configuration not to be checked
name: record.name,
}),
}), []);
const scroll = useMemo(() => ({ y: 300 }), []);
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}
render(App);
Since the 1.1.0 version, it is supported to pass in renderFilterDropdownItem
to customize the rendering method of each filter item.
This method accepts an Object
input parameter, and the meaning of each attribute is as follows:
onChange: e => void
current filter item selection/reverse selection event;text: ReactNode
copy of the current screening item;value: any
the value of the current filter item;checked: boolean
whether the current filter item has been selected;filteredValue: any[]
all currently filtered values;level: number
is the level of the current filter item, if it is a nested filter item, the value will be >= 1;filterMultiple: boolean
whether the current filter item is multi-select.
import React, { useState, useMemo } from 'react';
import { Table, Avatar, Dropdown } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
renderFilterDropdownItem: ({ text, checked, onChange }) => (
<Dropdown.Item onClick={onChange} active={checked}>
{text}
</Dropdown.Item>
),
filterDropdownProps: {
showTick: true,
},
sorter: (a, b) => a.name.length - b.name.length > 0 ? 1 : -1,
},
{
title: 'Size',
dataIndex: 'size',
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
function App() {
const [dataSource, setData] = useState([]);
const rowSelection = useMemo(() => ({
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: record => ({
disabled: record.name === 'Michael James', // Column configuration not to be checked
name: record.name,
}),
}), []);
const scroll = useMemo(() => ({ y: 300 }), []);
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
useEffect(() => {
const data = getData();
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}
render(App);
<div>1. Since version `0.27.0`, the unfold button will be rendered in the same cell as the first column, and you can open a separate column of rendering by passing in `hideExpandedColumn = {false}`.</div>
<div>2. Be sure to provide a "key" for each row of data that is different from the other row values, or use the rowKey parameter to specify an attribute name as the primary key.</div>
If you need to render a table that can be expanded, in addition to the need to pass expandedRowRender
In addition to this method, you must specify rowKey
(by default key
), Table will be based on rowKey
Gets a row unique identifier.
rowKey
is a Function, the result of rowKey(record)
is taken as the row unique IDIf rowKey
is a string type, you take record[rowKey]
as the row unique ID
import React from 'react';
import { Table, Avatar, Descriptions, Tag } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
const columns = [
{
title: 'Title',
width: 500,
dataIndex: 'name',
render: (text, record, index) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{text}
</span>
);
}
},
{
title: 'Size',
dataIndex: 'size',
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
}
},
];
const data = [
{
key: '1',
name: 'Semi Design design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
];
const expandData = {
'0': [
{ key: 'DAU', value: '1,480,000' },
{ key: 'Day7 Retention Ratio', value: '98%' },
{ key: 'Security Level', value: '3级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Designer</Tag> },
{ key: 'Certification', value: 'No Verified' },
],
'1': [
{ key: 'DAU', value: '2,480,000' },
{ key: 'Day7 Retention Ratio', value: '90%' },
{ key: 'Security Level', value: '1级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Template</Tag> },
{ key: 'Certification', value: 'Verified' },
],
'2': [
{ key: 'DAU', value: '2,920,000' },
{ key: 'Day7 Retention Ratio', value: '98%' },
{ key: 'Security Level', value: '2级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Docs</Tag> },
{ key: 'Certification', value: 'Verified' },
]
};
function App() {
const expandRowRender = (record, index) => {
return <Descriptions align="justify" data={expandData[index]} />;
};
const rowSelection = {
getCheckboxProps: record => ({
disabled: record.name === 'Design docs', // Column configuration not to be checked
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} rowSelection={rowSelection} pagination={false} />;
}
render(App);
Version: >= 0.27.0
By default, the expansion button will be rendered in the same cell as the first copy, and you can enter hideExpandedColumn={false}
to render as a separate column:
import React from 'react';
import { Table, Avatar, Descriptions, Tag } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
const columns = [
{
title: 'Title',
width: 500,
dataIndex: 'name',
render: (text, record, index) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{text}
</span>
);
}
},
{
title: 'Size',
dataIndex: 'size',
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
}
},
];
const data = [
{
key: '1',
name: 'Semi Design design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
];
const expandData = {
'0': [
{ key: 'DAU', value: '1,480,000' },
{ key: 'Day7 Retention Ratio', value: '98%' },
{ key: 'Security Level', value: '3级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Designer</Tag> },
{ key: 'Certification', value: 'No Verified' },
],
'1': [
{ key: 'DAU', value: '2,480,000' },
{ key: 'Day7 Retention Ratio', value: '90%' },
{ key: 'Security Level', value: '1级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Template</Tag> },
{ key: 'Certification', value: 'Verified' },
],
'2': [
{ key: 'DAU', value: '2,920,000' },
{ key: 'Day7 Retention Ratio', value: '98%' },
{ key: 'Security Level', value: '2级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Docs</Tag> },
{ key: 'Certification', value: 'Verified' },
]
};
function App() {
const expandRowRender = (record, index) => {
return <Descriptions align="justify" data={expandData[index]} />;
};
const rowSelection = {
getCheckboxProps: record => ({
disabled: record.name === 'Design docs', // Column configuration not to be checked
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
return (
<Table
rowKey="name"
columns={columns}
dataSource={data}
expandedRowRender={expandRowRender}
hideExpandedColumn={false}
rowSelection={rowSelection}
pagination={false}
/>
);
}
render(App);
Version: > = 0.27.0
You can pass the row Expandable method, enter the reference as record, and determine whether the return value is false to close the rendering of the expandable button on a row.
import React from 'react';
import { Table, Avatar, Descriptions, Tag } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
const columns = [
{
title: 'Title',
width: 500,
dataIndex: 'name',
render: (text, record, index) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{text}
</span>
);
}
},
{
title: 'Size',
dataIndex: 'size',
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
}
},
];
const data = [
{
key: '1',
name: 'Semi Design design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
];
const expandData = {
'0': [
{ key: 'DAU', value: '1,480,000' },
{ key: 'Day7 Retention Ratio', value: '98%' },
{ key: 'Security Level', value: '3级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Designer</Tag> },
{ key: 'Certification', value: 'No Verified' },
],
'1': [
{ key: 'DAU', value: '2,480,000' },
{ key: 'Day7 Retention Ratio', value: '90%' },
{ key: 'Security Level', value: '1级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Template</Tag> },
{ key: 'Certification', value: 'Verified' },
],
'2': [
{ key: 'DAU', value: '2,920,000' },
{ key: 'Day7 Retention Ratio', value: '98%' },
{ key: 'Security Level', value: '2级' },
{ key: 'Vertical label', value: <Tag style={{ margin: 0 }}>Docs</Tag> },
{ key: 'Certification', value: 'Verified' },
]
};
function App() {
const expandRowRender = (record, index) => {
return <Descriptions align="justify" data={expandData[index]} />;
};
const rowSelection = {
getCheckboxProps: record => ({
disabled: record.name === 'Design docs', // Column configuration not to be checked
name: record.name,
}),
onSelect: (record, selected) => {
console.log(`select row: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`select all rows: ${selected}`, selectedRows);
},
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
};
return (
<Table
rowKey="name"
columns={columns}
dataSource={data}
expandedRowRender={expandRowRender}
rowExpandable={ record => record.name !== 'Design docs' }
hideExpandedColumn={false}
rowSelection={rowSelection}
pagination={false}
/>
);
}
render(App);
Version: > = 0.27.0
The table supports the display of tree data and is automatically displayed as a tree table when there are children fields in the data. If you do not need or use other fields, you can configure it with the childrenRecordName. In addition, you can control the indent width of each layer by setting indent Size.
Note: Be sure to provide a "key" for each row of data that is different from the other row values, or use the
rowKey
parameter to specify a property name as the primary key.
import React from 'react';
import { Table } from '@douyinfe/semi-ui';
function App() {
const columns = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: 'Data Type',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
},
{
title: 'Default',
dataIndex: 'default',
key: 'default',
width: 100,
},
];
const data = [
{
key: 1,
dataKey: 'videos_info',
name: 'Video Info',
type: 'Object',
description: 'Meta info of video',
default: 'None',
children: [
{
key: 11,
dataKey: 'status',
name: 'Video Status',
type: 'Enum <Integer>',
description: 'Viewable and recommended status of the video',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: 'Video ID',
type: 'String',
description: 'Unique ID that identifies the video',
default: 'None',
children: [
{
dataKey: 'video_url',
name: 'Video url',
type: 'String',
description: 'Unique link to the video',
default: 'None',
},
],
}
],
},
{
key: 2,
dataKey: 'text_info',
name: 'Text Info',
type: 'Object',
description: 'Meta info of video',
default: 'None',
children: [
{
key: 21,
dataKey: 'title',
name: 'Video Title',
type: 'String',
description: 'Title of video',
default: 'None',
},
{
key: 22,
dataKey: 'video_description',
name: 'Video Description',
type: 'String',
description: 'Description of video',
default: 'None',
}
],
},
];
return (
<Table
columns={columns}
defaultExpandAllRows
dataSource={data}
/>
);
};
render(App);
Version: > = 0.27.0
You can do row switching by changing the order of the dataSource elements.
import React, { useState } from 'react';
import { Table, Button } from '@douyinfe/semi-ui';
import { IconArrowUp, IconArrowDown } from '@douyinfe/semi-icons';
const raw = [
{
key: 1,
dataKey: 'videos_info',
name: 'Video Info',
type: 'Object',
description: 'Meta info of video',
default: 'None',
children: [
{
key: 11,
dataKey: 'status',
name: 'Video Status',
type: 'Enum <Integer>',
description: 'Viewable and recommended status of the video',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: 'Video ID',
type: 'String',
description: 'Unique ID that identifies the video',
default: 'None',
children: [
{
dataKey: 'video_url',
name: 'Video url',
type: 'String',
description: 'Unique link to the video',
default: 'None',
},
],
}
],
},
{
key: 2,
dataKey: 'text_info',
name: 'Text Info',
type: 'Object',
description: 'Meta info of video',
default: 'None',
children: [
{
key: 21,
dataKey: 'title',
name: 'Video Title',
type: 'String',
description: 'Title of video',
default: 'None',
},
{
key: 22,
dataKey: 'video_description',
name: 'Video Description',
type: 'String',
description: 'Description of video',
default: 'None',
}
],
},
];
const rowKey= 'key';
const childrenRecordName= 'children';
function App() {
const [expandedRowKeys, setExpandedRowKeys] = useState([1, 2]);
const [data, setData] = useState(raw);
const switchRecord = (key1, key2) => {
const newData = [...data];
if (key1 != null && key2 != null) {
const item1 = findRecordByKey(key1, newData);
const item2 = findRecordByKey(key2, newData);
// you have to copy item1 and item2 first
const copiedItem1 = { ...item1 };
const copiedItem2 = { ...item2 };
coverRecord(item1, copiedItem2);
coverRecord(item2, copiedItem1);
setData(newData);
}
};
const findRecordByKey = (key, data) => {
if (Array.isArray(data) && data.length && key != null) {
for (let item of data) {
if (item[rowKey] === key) {
return item;
}
const children = item[childrenRecordName];
if (Array.isArray(children) && children.length) {
const item = findRecordByKey(key, children);
if (item != null) {
return item;
}
}
}
}
};
const coverRecord = (obj, srcObj) => {
if (obj && typeof obj === 'object' && srcObj && typeof srcObj === 'object') {
const srcKeys = Object.keys(srcObj);
const copied = { ...srcObj };
Object.assign(obj, copied);
Object.keys(obj).forEach(key => {
if (!srcKeys.includes(key)) {
delete obj[key];
}
});
}
return obj;
};
const getSameLevelRecords = (key, data = []) => {
if (key != null && Array.isArray(data) && data.length) {
if (data.find(item => item[rowKey] === key)) {
return data;
}
for (let item of data) {
const records = getSameLevelRecords(key, item[childrenRecordName]);
if (records.length) {
return records;
}
}
}
return [];
};
const columns = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: 'Data Type',
dataIndex: 'type',
key: 'type',
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
},
{
title: 'Default',
dataIndex: 'default',
key: 'default',
width: 100,
},
{
key: 'operation',
render: record => {
const records = getSameLevelRecords(record[rowKey], data);
const index = records.findIndex(item => item[rowKey] === record[rowKey]);
const upProps = {};
const downProps = {};
if (index > 0) {
const upRow = records[index - 1];
upProps.onClick = () => switchRecord(record[rowKey], upRow[rowKey]);
} else {
upProps.disabled = true;
}
if (index < records.length - 1) {
const downRow = records[index + 1];
downProps.onClick = () => switchRecord(record[rowKey], downRow[rowKey]);
} else {
downProps.disabled = true;
}
return (
<>
<Button icon={<IconArrowUp />} {...upProps} />
<Button icon={<IconArrowDown />} {...downProps} />
</>
);
},
},
];
return (
<Table
columns={columns}
dataSource={data}
rowKey={rowKey}
childrenRecordName={childrenRecordName}
expandedRowKeys={expandedRowKeys}
onExpandedRowsChange={rows => setExpandedRowKeys(rows.map(item => item[rowKey]))}
/>
);
};
render(App);
Version: >=0.27.0
By default, the row selection of the table is independent. You can simulate a tree selection by defining selectedRowKeys.
import React, { useMemo, useState, useCallback } from 'react';
import { get, union, pullAll } from 'lodash-es';
import { Table } from '@douyinfe/semi-ui';
const childrenRecordName = 'children';
const rowKey = 'key';
const getKey = record => get(record, rowKey, 'key');
const ChildrenDataSelectedDemo = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const columns = useMemo(
() => [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: 'Data Type',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
},
{
title: 'Default',
dataIndex: 'default',
key: 'default',
width: 100,
},
],
[]
);
const data = useMemo(
() => [
{
key: 1,
dataKey: 'videos_info',
name: 'Video Info',
type: 'Object',
description: 'Meta info of video',
default: 'None',
children: [
{
key: 11,
dataKey: 'status',
name: 'Video Status',
type: 'Enum <Integer>',
description: 'Viewable and recommended status of the video',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: 'Video ID',
type: 'String',
description: 'Unique ID that identifies the video',
default: 'None',
children: [
{
key: 121,
dataKey: 'video_url',
name: 'Video url',
type: 'String',
description: 'Unique link to the video',
default: 'None',
},
],
}
],
},
{
key: 2,
dataKey: 'text_info',
name: 'Text Info',
type: 'Object',
description: 'Meta info of video',
default: 'None',
children: [
{
key: 21,
dataKey: 'title',
name: 'Video Title',
type: 'String',
description: 'Title of video',
default: 'None',
},
{
key: 22,
dataKey: 'video_description',
name: 'Video Description',
type: 'String',
description: 'Description of video',
default: 'None',
}
],
},
],
[]
);
const isRecordDisabled = (record) => {
return false;
};
const traverse = (data, res) => {
for (let record of data) {
const children = get(record, 'children');
const disabled = isRecordDisabled(record);
if (!disabled) {
const key = getKey(record);
res.push(key);
}
if (Array.isArray(children)) {
traverse(children, res);
}
}
};
const getAllRowKeys = data => {
const allRowKeys = [];
traverse(data, allRowKeys);
console.log('allRowKeys', allRowKeys);
return allRowKeys;
};
const findShouldSelectRowKeys = (record, selected) => {
let shouldSelectRowKeys;
const children = get(record, 'children');
let childrenRowKeys = [];
if (Array.isArray(children)) {
traverse(children, childrenRowKeys);
}
const key = getKey(record);
if (!selected) {
shouldSelectRowKeys = [...selectedRowKeys];
pullAll(shouldSelectRowKeys, [key, ...childrenRowKeys]);
} else {
shouldSelectRowKeys = union(selectedRowKeys, [key, ...childrenRowKeys]);
}
return shouldSelectRowKeys;
};
const doSelect = useCallback((record, selected) => {
const rowKeys = findShouldSelectRowKeys(record, selected);
setSelectedRowKeys(rowKeys);
console.log('select', record, rowKeys);
}, [selectedRowKeys, rowKey, childrenRecordName]);
const doSelectAll = useCallback((selected, selectedRows) => {
console.log(selected);
let rowKeys = [];
if (selected) {
rowKeys = getAllRowKeys(data);
}
setSelectedRowKeys(rowKeys);
}, []);
const rowSelection = useMemo(
() => ({
selectedRowKeys,
onSelect: doSelect,
onSelectAll: doSelectAll,
}),
[selectedRowKeys, doSelect, doSelectAll]
);
return (
<Table
columns={columns}
rowKey={rowKey}
childrenRecordName={childrenRecordName}
rowSelection={rowSelection}
dataSource={data}
pagination={false}
/>
);
};
render(ChildrenDataSelectedDemo);
onRow
/ onHeaderRow
can define the native event or property of a table or table header line.Column.onCell
/ Column.onHeaderCell
can define a table or header cell native event or property.In principle, the properties or events supported on tr / td / th can be defined. For example:
The third line of the table defines onClick.
import React, { useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui/';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
function EventTable(props = {}) {
const columns = useMemo(
() => [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
],
[]
);
const data = useMemo(() => {
const _data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
_data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return _data;
}, []);
const onRow = useMemo(
() => (record, index) => {
const props = {
className: 'my-tr-class',
};
if (index === 2) {
return {
...props,
onClick: e => console.log('mouse click: ', record, index),
};
} else {
return {
...props,
};
}
},
[]
);
const onHeaderRow = useMemo(
() => (columns, index) => {
return {
onMouseEnter: e => console.log('mouse enter: ', columns, index),
onMouseLeave: e => console.log('mouse leave: ', columns, index),
};
},
[]
);
return <Table columns={columns} dataSource={data} onRow={onRow} onHeaderRow={onHeaderRow} />;
}
render(EventTable);
Use OnRow
to set a background color for each row to create a zebra stripped table.
import React from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
function App() {
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
}
},
{
title: 'Size',
dataIndex: 'size',
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => {
return <IconMore />;
}
},
];
const data = [
{
key: '1',
name: 'Semi Design design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '2',
name: 'Semi Design share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '3',
name: 'Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
{
key: '4',
name: 'Semi Pro design draft.fig',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey'
},
{
key: '5',
name: 'Semi Pro share docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
avatarBg: 'red'
},
{
key: '6',
name: 'Semi Pro Design docs',
nameIconSrc: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue'
},
];
const handleRow = (record, index) => {
if (index % 2 === 0) {
return {
style: {
background: 'var(--semi-color-fill-0)',
}
};
} else {
return {};
}
};
return <Table columns={columns} dataSource={data} onRow={handleRow} pagination={false} />;
}
render(App);
Version > = 0.15.0
For some columns with more content, you can choose to turn on the telescopic column function and pull and pull at the head to realize the real-time change of column width.
But you need to pay attention to some parameters:
resizable
is set to true
or an object
columns
that requires a telescopic function should specify the width
field (if not passed, the column does not have a telescopic function and its column width will be automatically adjusted by the browser)It is not recommended to use with fixed columns at the same time. Fixed columns need to specify
scroll.x
, which stipulates that the table has a width range, and the flexible column will expand the column width, which may cause the table to be misaligned
import React, { useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
function ResizableDemo() {
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
width: 200,
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
width: 200,
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
const data = useMemo(() => {
const _data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
_data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return _data;
}, []);
return <Table columns={columns} dataSource={data} resizable bordered />;
}
render(ResizableDemo);
resizable
can also be an object
, including three event methods:
These three callback will be triggered on changing column width
, start changing column width
and stop changing column width
respectively. Developers can choose to modify the column at this time, such as adding a vertical line effect when dragging, as shown below.
import React, { useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const pagination = { pageSize: 5 };
function ResizableDemo() {
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
width: 200,
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
width: 200,
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
const data = useMemo(() => {
const _data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
_data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return _data;
}, []);
const resizable = {
onResizeStart: curColumn => {
const className = addClass(curColumn.className, 'my-resizing');
return { className };
},
onResizeStop: curColumn => {
const className = removeClass(curColumn.className, 'my-resizing');
return { className };
}
};
return (
<div id="components-table-demo-resizable-column">
<Table columns={columns} dataSource={data} resizable={resizable} pagination={pagination} bordered />
</div>
);
}
render(ResizableDemo);
The CSS style definition used in this example:
#components-table-demo-resizable-column .my-resizing {
border-right: 2px solid red;
}
#components-table-demo-resizable-column .react-resizable-handle:hover {
background-color: red;
}
#components-table-demo-resizable-column .my-resizing:hover .react-resizable-handle {
background-color: inherit;
}
With custom elements, we can integrate react-dnd
To implement drag and drop sorting.
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { DndProvider, DragSource, DropTarget } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import * as dateFns from 'date-fns';
let draggingIndex = -1;
const PAGE_SIZE = 5;
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
function BodyRow(props) {
const { isOver, connectDragSource, connectDropTarget, moveRow, currentPage, ...restProps } = props;
const style = { ...restProps.style, cursor: 'move' };
let { className } = restProps;
if (isOver) {
console.log('true');
if (restProps.index > draggingIndex) {
className += ' drop-over-downward';
}
if (restProps.index < draggingIndex) {
className += ' drop-over-upward';
}
}
return connectDragSource(connectDropTarget(<tr {...restProps} className={className} style={style} />));
}
const rowSource = {
beginDrag(props) {
draggingIndex = props.index;
return {
index: props.index,
};
},
};
const rowTarget = {
drop(props, monitor) {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
if (dragIndex === hoverIndex) {
return;
}
props.moveRow(dragIndex, hoverIndex);
monitor.getItem().index = hoverIndex;
},
};
const DraggableBodyRow = DropTarget('row', rowTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
}))(
DragSource('row', rowSource, connect => ({
connectDragSource: connect.dragSource(),
}))(BodyRow)
);
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
width: 200,
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
width: 200,
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
const initData = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
initData.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
function DragSortingTableDemo(props) {
const [data, setData] = useState([...initData]);
const [currentPage, setCurrentPage] = useState(1);
const [pageData, setPageData] = useState(data.slice(0, PAGE_SIZE));
const components = useMemo(() => ({
body: {
row: DraggableBodyRow,
},
}), []);
const moveRow = (dragIndex, hoverIndex) => {
const totalDragIndex = (currentPage - 1) * PAGE_SIZE + dragIndex;
const totalHoverIndex = (currentPage - 1) * PAGE_SIZE + hoverIndex;
const dragRow = data[totalDragIndex];
const newData = [...data];
newData.splice(totalDragIndex, 1);
newData.splice(totalHoverIndex, 0, dragRow);
setData(newData);
setPageData(newData.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE));
};
const handlePageChange = (pageNum) => {
console.log(pageNum);
setCurrentPage(pageNum);
setPageData(data.slice((pageNum - 1) * PAGE_SIZE, pageNum * PAGE_SIZE));
};
return (
<div id="components-table-demo-drag-sorting">
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
dataSource={pageData}
pagination={{
pageSize: PAGE_SIZE,
total: data.length,
currentPage,
onPageChange: handlePageChange
}}
components={components}
onRow={(record, index) => ({
index,
moveRow,
})}
/>
</DndProvider>
</div>
);
}
render(DragSortingTableDemo);
The CSS styles used in this example are:
#components-table-demo-drag-sorting tr.drop-over-downward td {
border-bottom: 2px dashed #1890ff;
}
#components-table-demo-drag-sorting tr.drop-over-upward td {
border-top: 2px dashed #1890ff;
}
For some tables whose data needs to be displayed in groups, you can pass in groupBy
to define the grouping rules, and use renderGroupSection
to define the rendering of the grouping table header.
Note: Be sure to provide a "key" for each row of data that is different from other row values, or use the rowKey parameter to specify a property name as the primary key.
import React from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
const getData = () => {
const data = [];
for (let i = 0; i < 46; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = i * 1000 % 19 + 100;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
function Demo() {
const rowKey = record => `${record.owner && record.owner.toLowerCase()}-${record.name && record.name.toLowerCase()}`;
return (
<div style={{ padding: '20px 0px' }}>
<Table
dataSource={data}
rowKey={rowKey}
groupBy={'size'}
columns={columns}
renderGroupSection={groupKey => <strong>Group by file size {groupKey} KB</strong>}
onGroupedRow={(group, index) => {
return {
// onMouseEnter: () => {
// console.log(`Grouped row mouse enter: `, group, index);
// },
// onMouseLeave: () => {
// console.log(`Grouped row mouse leave: `, group, index);
// },
onClick: e => { console.log(`Grouped row clicked: `, group, index); }
};
}}
clickGroupedRowToExpand // if you want to click the entire row to expand
scroll={{ y: 480 }}
/>
</div>
);
}
render(Demo);
Virtualization can be used for scenes that need to render large-scale data. You can enable this feature by configuring the virtualized
parameter. have to be aware of is:
scroll.y
(number) andstyle.width
(number);virtualized.itemSize
(when not transmitting, the normal line height defaults to 56
, and the group head line height defaults to 56
), can be of the following types:
number
(index, { sectionRow?: boolean, expandedRow?: boolean }) => number
0.37.0
.The following is an example of rendering 1000 pieces of data.
import React, { useRef } from 'react';
import { Table, Avatar, Button } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 200,
fixed: true,
render: (text, record, index) => {
return (
<div>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
width: 150,
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
fixed: 'right',
width: 150,
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
const getData = () => {
const data = [];
for (let i = 0; i < 1000; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
const data = getData();
function VirtualizedFixedDemo() {
let virtualizedListRef = useRef();
const scroll = { y: 400, x: 900 };
const style = { width: 750, margin: '0 auto' };
return (
<>
<Button onClick={() => virtualizedListRef.current.scrollToItem(100)}>Scroll to 100</Button>
<Table
pagination={false}
columns={columns}
dataSource={data}
scroll={scroll}
style={style}
virtualized
getVirtualizedListRef={ref => virtualizedListRef = ref}
/>
</>
);
}
render(VirtualizedFixedDemo);
Based on the virtualization feature, we can achieve infinite scroll loading data by passing in virtualized.onScroll
.
import React, { useRef } from 'react';
import { Table, Avatar, Button } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 200,
fixed: true,
render: (text, record, index) => {
return (
<div>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
width: 150,
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
fixed: 'right',
width: 150,
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
function InfiniteScrollDemo() {
const [data, setData] = useState([]);
const scroll = { y: 600, x: 1000 };
const style = { width: 750, margin: '0 auto' };
const loadMore = () => {
const pageSize = 20; // load 20 records every time
const newData = [...data];
const currentLength = data.length;
for (let i = currentLength; i < currentLength + pageSize; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
newData.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
setData(newData);
};
const itemSize = 56;
const virtualized = {
itemSize,
onScroll: ({ scrollDirection, scrollOffset, scrollUpdateWasRequested }) => {
if (
scrollDirection === 'forward' &&
scrollOffset >= (data.length - Math.ceil(scroll.y / itemSize) * 1.5) * itemSize &&
!scrollUpdateWasRequested
) {
loadMore();
}
},
};
useEffect(() => {
loadMore();
}, []);
return (
<Table
pagination={false}
columns={columns}
dataSource={data}
scroll={scroll}
style={style}
virtualized={virtualized}
/>
);
}
render(InfiniteScrollDemo);
import React from 'react';
import { Table, Switch, ButtonGroup, Button, Avatar, Space } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
class App extends React.Component {
constructor(props) {
super(props);
const dataTotalSize = 46;
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</span>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
this.getData = () => {
const data = [];
for (let i = 0; i < dataTotalSize; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
const data = this.getData();
this.data = data;
this.mergeColumns = (column, columns, keys = ['dataIndex']) => {
columns = [...columns];
columns.forEach((curColumn, index) => {
let isTarget = !!(keys && keys.length);
for (let key of keys) {
if (column[key] !== curColumn[key]) {
isTarget = false;
break;
}
}
if (isTarget) {
columns[index] = { ...curColumn, ...column };
}
});
return columns;
};
this.filterData = (filters, dataSource) => {
dataSource = [...dataSource];
filters.forEach(filter => {
let filteredValue = filter.filteredValue;
let dataIndex = filter.dataIndex;
if (Array.isArray(filteredValue) && filteredValue.length && dataIndex) {
dataSource = dataSource.filter(
data => filteredValue.filter(value => String(data[dataIndex]).indexOf(value) > -1).length
);
}
});
return dataSource;
};
this.getSelfSorterColumn = columns => {
columns = columns || this.state.columns;
return columns.filter(column => !!column.sorter)[0];
};
this.getSelfFilterColumns = columns => {
columns = columns || this.state.columns;
return columns.filter(column => Array.isArray(column.filteredValue) && column.filteredValue.length);
};
this.sortData = (sortObj, dataSource) => {
let { sorter, sortOrder, dataIndex } = sortObj;
if (sorter && sortOrder && typeof sorter !== 'function') {
sorter = (a, b) => (a[dataIndex] > b[dataIndex] ? 1 : -1);
}
if (typeof sorter === 'function') {
dataSource = [...dataSource].sort(sorter);
if (sortOrder === 'descend') {
dataSource = dataSource.reverse();
}
}
return dataSource;
};
this.fetchData = (currentPage = 1, sorter = {}, filters = []) => {
// console.log(`FetchData currentPage: `, currentPage);
let pagination = { ...this.state.pagination, currentPage };
return new Promise((res, rej) => {
setTimeout(() => {
let data = [...this.data];
data = this.sortData(sorter, data);
data = this.filterData(filters, data);
let dataSource = data.slice(
(currentPage - 1) * pagination.pageSize,
currentPage * pagination.pageSize
);
pagination.total = data.length;
res({
dataSource,
pagination,
sorter,
filters,
});
}, 1500);
});
};
this.setPage = (currentPage, sorter, filters) => {
if (this.state.loading) {
return;
}
if (typeof currentPage !== 'number') {
currentPage = (this.state.pagination && this.state.pagination.currentPage) || 1;
}
sorter = sorter || this.getSelfSorterColumn();
filters = filters || this.getSelfFilterColumns();
this.setState({ loading: true });
this.fetchData(currentPage, sorter, filters)
.then(({ dataSource, pagination, sorter, filters }) => {
let columns = [...this.state.columns];
columns = this.mergeColumns(sorter, columns);
for (let filterObj of filters) {
columns = this.mergeColumns(filterObj, columns);
}
this.setState({
loading: false,
pagination,
dataSource,
columns,
});
})
.catch(err => {
console.error(err);
this.setState({ loading: false });
});
};
this.toggleFixHeader = checked => {
let scroll = { ...this.state.scroll };
if (checked) {
scroll.y = 300;
} else {
scroll.y = null;
}
this.setState({ scroll });
};
this.toggleFixColumns = checked => {
let columns = [...this.state.columns];
let scroll = { ...this.state.scroll };
let expandCellFixed = this.state.expandCellFixed;
let rowSelection = this.state.rowSelection;
if (checked) {
columns[0].fixed = true;
if (rowSelection) {
rowSelection = { ...rowSelection, fixed: true };
}
if (columns.length > 1) {
columns[columns.length - 1].fixed = 'right';
}
scroll.x = '150%';
expandCellFixed = true;
} else {
columns.forEach(column => {
column.fixed = false;
});
scroll.x = null;
expandCellFixed = false;
if (rowSelection) {
rowSelection = { ...rowSelection, fixed: false };
}
}
this.setState({
rowSelection,
expandCellFixed,
columns,
scroll,
});
};
this.toggleRowSelection = checked => {
let rowSelection = this.state.rowSelection;
// const anyColumnFixed = this.state.columns.some(column => !!column.fixed);
if (checked) {
rowSelection = {
width: 48,
fixed: true,
onChange: (selectedRowKeys, selectedRows) =>
console.log(
'Selection changed, selectedRowKeys: ',
selectedRowKeys,
'selectedRows: ',
selectedRows
),
};
} else {
rowSelection = null;
}
this.setState({ rowSelection });
};
this.toggleLoading = checked => {
let loading = this.state.loading;
if (checked) {
loading = true;
} else {
loading = false;
}
this.setState({ loading });
};
this.toggleExpandedRowRender = checked => {
let expandedRowRender = this.state.expandedRowRender;
if (checked) {
expandedRowRender = record => {
return {
children: <p>{record.description}</p>,
fixed: 'left',
};
};
} else {
expandedRowRender = null;
}
this.setState({ expandedRowRender });
};
this.toggleShowSorter = checked => {
let columns = [...this.state.columns];
if (checked) {
columns.forEach(column => column.dataIndex === 'age' && (column.sorter = true));
} else {
columns.forEach(column => (column.sorter = null));
}
this.setState({ columns });
};
this.toggleShowFilter = checked => {
let columns = [...this.state.columns];
if (checked) {
columns.forEach(column => {
if (column.dataIndex === 'name') {
column.filters = [
{
text: 'Name contains 1',
value: '1',
},
{
text: 'Name contains 2',
value: '2',
},
{
text: 'Name contains 3',
value: '3',
},
];
column.filteredValue = [];
}
});
} else {
columns.forEach(column => {
column.filters = null;
column.filteredValue = null;
});
}
this.setState({ columns });
if (!checked) {
this.setPage(null, null, []);
}
};
this.onChange = (data = {}) => {
console.log('Table changed: ', data);
let { pagination, sorter, filters } = data;
this.setPage(pagination.currentPage, sorter, filters);
};
this.onExpandedRowsChange = rows => {
console.log('Expanded rows changed to: ', rows);
const expandedRowKeys = (Array.isArray(rows) && rows.map(row => row.key)) || [];
this.setState({ expandedRowKeys });
};
this.toggleExpandedRowKeys = checked => {
let expandedRowKeys = [];
if (checked) {
let dataSource = [...this.state.dataSource];
expandedRowKeys.push(
...dataSource.reduce((arr, data) => {
if (data.key) {
arr.push(data.key);
}
return arr;
}, [])
);
this.toggleExpandedRowRender(true);
}
this.setState({ expandedRowKeys });
};
this.toggleBordered = checked => {
let bordered = false;
if (checked) {
bordered = true;
}
this.setState({ bordered });
};
this.toggleResizable = checked => {
let resizable = !!checked || false;
this.setState({ resizable, bordered: resizable });
};
this.toggleHideHeader = checked => {
let showHeader = true;
if (checked) {
showHeader = false;
}
this.setState({ showHeader });
};
this.toggleFooter = checked => {
const footer = checked ? dataSource => <p style={{ margin: 0 }}>This is footer.</p> : null;
this.setState({ footer });
};
this.toggleTitle = checked => {
const title = checked ? 'This is title.' : null;
this.setState({ title });
};
this.toggleHidePagination = checked => {
let pagination = checked
? false
: {
currentPage: 1,
pageSize: 8,
total: data.length,
onPageChange: page => this.setPage(page),
};
this.setState({ pagination });
};
this.toggleDataSource = checked => {
if (checked) {
this.setState({ dataSource: [] });
} else {
this.setPage();
}
};
this.switchPagination = position => {
let pagination = this.state.pagination;
const defaultPagination = {
currentPage: 1,
pageSize: 8,
total: data.length,
onPageChange: page => this.setPage(page),
};
const positions = ['bottom', 'top', 'both'];
if (position === true || position === false) {
pagination = position ? { ...defaultPagination, ...pagination } : false;
} else if (positions.includes(position)) {
pagination = { ...defaultPagination, ...pagination, position };
}
this.setState({ pagination });
};
this.state = {
loading: false,
columns,
scroll: {},
rowSelection: null,
expandedRowRender: null,
expandCellFixed: false,
defaultExpandedRowKeys: [],
title: null,
footer: null,
expandedRowKeys: [],
showHeader: true,
resizable: false,
pagination: {
currentPage: 1,
pageSize: 8,
total: data.length,
onPageChange: page => this.setPage(page),
},
dataSource: [],
};
this.TableSwitch = function TableSwitch({
text,
children,
checked,
onChange,
style = { display: 'inline-flex', alignItems: 'center', margin: 5 },
}) {
const switchProps = { onChange };
if (checked != null) {
switchProps.checked = !!checked;
}
return (
<span style={style}>
<span>{text}</span>
{children != null ? children : <Switch size="small" {...switchProps} />}
</span>
);
};
}
componentDidMount() {
this.setPage(1);
}
render() {
let {
columns,
dataSource,
pagination,
loading,
scroll,
rowSelection,
expandedRowRender,
expandCellFixed,
expandedRowKeys,
bordered,
resizable,
title,
footer,
showHeader,
defaultExpandedRowKeys,
} = this.state;
const wrapStyle = { marginBottom: 15, display: 'flex', justifyContent: 'space-around', flexWrap: 'wrap' };
const TableSwitch = this.TableSwitch;
return (
<div>
<Space style={wrapStyle} wrap>
<TableSwitch text="Fixed Header:" checked={scroll && scroll.y} onChange={this.toggleFixHeader} />
<TableSwitch text="Hidden Header:" onChange={this.toggleHideHeader} />
<TableSwitch text="Show Header:" onChange={this.toggleTitle} />
<TableSwitch text="Show Footer:" onChange={this.toggleFooter} />
<TableSwitch text="Fixed Column:" onChange={this.toggleFixColumns} />
<TableSwitch text="Show Selection Column:" onChange={this.toggleRowSelection} />
<TableSwitch text="Show Loading:" onChange={this.toggleLoading} checked={loading} />
<TableSwitch
text="Empty Content:"
onChange={this.toggleDataSource}
checked={!dataSource || !dataSource.length}
/>
<TableSwitch text="Column Sorter:" onChange={this.toggleShowSorter} />
<TableSwitch text="Column Filter:" onChange={this.toggleShowFilter} />
<TableSwitch
text="Row Expandable:"
onChange={this.toggleExpandedRowRender}
checked={typeof expandedRowRender === 'function'}
/>
<TableSwitch text="Expand All Rows:" onChange={this.toggleExpandedRowKeys} />
<TableSwitch text="Show Border:" onChange={this.toggleBordered} checked={bordered} />
<TableSwitch text="Column Resizable:" onChange={this.toggleResizable} />
<TableSwitch text="Show Pagination:">
<ButtonGroup>
<Button onClick={() => this.switchPagination('bottom')}>Bottom</Button>
<Button onClick={() => this.switchPagination('top')}>Top</Button>
<Button onClick={() => this.switchPagination('both')}>Both</Button>
<Button onClick={() => this.switchPagination(false)}>None</Button>
</ButtonGroup>
</TableSwitch>
</Space>
<Table
defaultExpandedRowKeys={defaultExpandedRowKeys}
onExpandedRowsChange={this.onExpandedRowsChange}
title={title}
footer={footer}
showHeader={showHeader}
bordered={bordered}
onChange={this.onChange}
expandCellFixed={expandCellFixed}
expandedRowRender={expandedRowRender}
expandedRowKeys={expandedRowKeys}
rowSelection={rowSelection}
scroll={scroll}
columns={columns}
dataSource={dataSource}
pagination={pagination}
loading={loading}
resizable={resizable}
/>
</div>
);
}
}
render(App);
Version: >=0.34.0
Generally, you can use Column.render
, but you can also passColumn.useFullRender = true
to enable full custom rendering mode. At this time, the components such as checkbox button, expand button, indent will be Pass through to the Column.title
andColumn.render
methods, you can further define the rendering method of the header and cell content.
The input parameters accepted by Column.title
are:
{
filter: ReactNode, // Filter button
sorter: ReactNode, // Sort button
selection: ReactNode, // Select button
}
Column.render
The fourth input parameter is:
{
expandIcon: ReactNode, // Expand button
selection: ReactNode, // Select button
indentTex: ReactNode, // Indentation
}
The example below renders the checkbox and content into the same cell and header.
import React, { useState, useEffect, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: ({ sorter, filter, selection }) => (
<span style={{ display: 'inline-flex', alignItems: 'center', paddingLeft: 20 }}>
{selection}
<span style={{ marginLeft: 8 }}>Name</span>
{sorter}
{filter}
</span>
),
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
useFullRender: true,
render: (text, record, index, { expandIcon, selection, indentText }) => {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
{indentText}
{expandIcon}
{selection}
<span style={{ marginLeft: 8 }}>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</span>
</span>
);
},
},
{
title: 'Size',
dataIndex: 'size',
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
];
const getData = (total) => {
const data = [];
for (let i = 0; i < total; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
function Demo() {
const [dataSource, setDataSource] = useState([]);
const total = 46;
const pagination = useMemo(() => ({
pageSize: 12,
}), []);
const rowSelection = useMemo(() => {
return {
hidden: true,
fixed: 'left',
};
}, []);
useEffect(() => {
const data = getData(total);
setDataSource(data);
}, [total]);
return (
<Table
pagination={pagination}
rowSelection={rowSelection}
columns={columns}
dataSource={dataSource}
onChange={(...args) => console.log(...args)}
expandedRowRender={record => <article>{record.name}</article>}
/>
);
}
render(Demo);
Version: >=1.1.0
Users can use the header merge function to group the header. The header merge can be combined with fixed column, virtualization, data grouping, column scaling and other functions. It also supports JSX or configurable writing.
import React, { useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: 'Basic Info',
fixed: 'left',
children: [
{
title: 'Title',
dataIndex: 'name',
width: 300,
fixed: true,
render: (text, record, index) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</span>
);
},
filters: [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: 'Size',
dataIndex: 'size',
width: 100,
fixed: true,
sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
render: (text) => `${text} KB`
},
]
},
{
title: 'Others Info',
children: [
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
}
}
]
},
{
title: 'More',
fixed: 'right',
width: 100,
align: 'center',
dataIndex: 'operate',
render: () => {
return <IconMore />;
}
},
];
const getData = (total) => {
const data = [];
for (let i = 0; i < total; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
function Demo() {
const data = useMemo(() => {
const _data = getData(46);
return _data;
}, []);
return (
<Table
rowSelection={{ fixed: true }}
expandedRowRender={record => <article>{record.name}</article>}
dataSource={data}
scroll={{ y: 400 }}
onChange={(...args) => console.log(...args)}
columns={columns}
/>
);
}
render(Demo);
import React, { useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import { IconMore } from '@douyinfe/semi-icons';
import * as dateFns from 'date-fns';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const Column = Table.Column;
const getData = (total) => {
const data = [];
for (let i = 0; i < total; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
function Demo() {
const data = useMemo(() => {
const _data = getData(46);
return _data;
}, []);
const nameFilters = [
{
text: 'Semi Design design draft',
value: 'Semi Design design draft',
},
{
text: 'Semi Pro design draft',
value: 'Semi Pro design draft',
},
];
const renderName = (text, record, index) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</span>
);
};
const renderOwner = (text, record, index) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
};
return (
<Table
rowSelection={{ fixed: true }}
expandedRowRender={record => <article>{record.name}</article>}
dataSource={data}
scroll={{ y: 400 }}
onChange={(...args) => console.log(...args)}
>
<Column title="Basic Info" fixed="left">
<Column title="Title" dataIndex="name" width={300} fixed render={renderName} filters={nameFilters} onFilter={(value, record) => record.name.includes(value)} />
<Column title="Size" dataIndex="size" width={100} fixed render={(text) => `${text} KB`} sorter={(a, b) => a.size - b.size > 0 ? 1 : -1} ></Column>
</Column>
<Column title="Others Info">
<Column title="Owner" dataIndex="owner" render={renderOwner} />
<Column title="Update" dataIndex="updateTime" sorter={(a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1} render={(value) => dateFns.format(new Date(value), 'yyyy-MM-dd')}></Column>
</Column>
<Column title="More" dataIndex="operate" fixed="right" width={100} align="center" render={() => <IconMore />} />
</Table>
);
}
render(Demo);
Table supports row/column merging. When the cell attribute colSpan or rowSpan in render is set to 0, the set table will not be rendered.
type Render = (text: string, record: Object, index: number, options?: RenderOptions) => {
children: React.ReactNode;
props: {
colSpan?: number,
rowSpan?: number,
},
[x: string]: any;
}
interface RenderOptions {
expandIcon?: React.ReactNode;
}
import React, { useState, useMemo } from 'react';
import { Table, Avatar } from '@douyinfe/semi-ui';
import * as dateFns from 'date-fns';
const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
const columns = [
{
title: 'Title',
dataIndex: 'name',
width: 400,
render: (text, record, index) => {
const renderObject = {};
const children = (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
{text}
</div>
);
renderObject.children = children;
if (index === 0) {
renderObject.props = {
colSpan: 4,
};
}
if (index === 1) {
renderObject.props = {
rowSpan: 2,
};
}
if (index === 2) {
renderObject.props = {
rowSpan: 0,
};
}
return renderObject;
},
},
{
title: 'Size',
dataIndex: 'size',
render: (text, record, index) => {
if (index === 0) {
return {
children: `${text} KB`,
props: {
colSpan: 0,
}
};
}
if (index === 1) {
return {
children: `${text} KB`,
props: {
rowSpan: 2,
}
};
}
if (index === 2) {
return {
children: `${text} KB`,
props: {
rowSpan: 0,
}
};
}
return `${text} KB`;
}
},
{
title: 'Owner',
dataIndex: 'owner',
render: (text, record, index) => {
const children = (
<div>
<Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
{text}
</div>
);
if (index === 0) {
return {
children,
props: {
colSpan: 0,
}
};
}
return children;
}
},
{
title: 'Update',
dataIndex: 'updateTime',
sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
render: (value, record, index) => {
const children = dateFns.format(new Date(value), 'yyyy-MM-dd');
if (index === 0) {
return {
children,
props: {
colSpan: 0
}
};
}
if (index === 1) {
return {
children,
props: {
rowSpan: 2
}
};
}
if (index === 2) {
return {
children,
props: {
rowSpan: 0
}
};
}
return children;
}
}
];
const DAY = 24 * 60 * 60 * 1000;
function App() {
const [dataSource, setData] = useState([]);
const getData = (total) => {
const data = [];
for (let i = 0; i < total; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design design draft${i}.fig` : `Semi Pro design draft${i}.fig`,
owner: isSemiDesign ? 'Jiang Pengzhi' : 'Hao Xuan',
size: randomNumber,
updateTime: new Date().valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red'
});
}
return data;
};
useEffect(() => {
const data = getData(5);
setData(data);
}, []);
return <Table columns={columns} dataSource={dataSource} pagination={false} />;
}
render(App);
Properties | Instructions | Type | Default | Version |
---|---|---|---|---|
bordered | Whether to display outer and column borders | boolean | false | |
className | Outermost style name | string | ||
clickGroupedRowToExpand | Group content expands or collapses when the group header row is clicked | boolean | 0.29.0 | |
columns | For a configuration description of the table column, see Column | Column [] | [] | |
dataSource | Data | RecordType[] | [] | |
defaultExpandAllRows | All rows are expanded by default | boolean | false | |
defaultExpandAllGroupRows | All grouped rows are expanded by default | boolean | false | 1.30.0 |
defaultExpandedRowKeys | Default expansion of row key array | Array <*> | [] | |
empty | Content displayed when there is no data | ReactNode | ReactNode | 'No data yet. ' |
expandCellFixed | Whether the column of the expansion icon is fixed or not, the same value as the fixed value in Column | boolean|string | false | |
expandIcon | Custom expansion icon, hidden when it is false |
boolean |ReactNode | (expanded: boolean) => ReactNode |
||
expandedRowKeys | Expanded rows, the row expansion function will be controlled when this parameter is introduced. | (string | number)[] | ||
expandedRowRender | Extra unfolding lines | (record: object, index: number, expanded: boolean) => ReactNode | ||
expandAllRows | All rows are expanded | boolean | false | 1.30.0 |
expandAllGroupRows | All grouped rows are expanded | boolean | false | 1.30.0 |
expandRowByClick | Expand row when click row | boolean | false | 1.31.0 |
footer | End of form | string |ReactNode |(pageData: object) => string|ReactNode |
||
groupBy | Grouping basis, generally a method of a key name or a return value of a string or number in the dataSource element | string|number |(record: any) => string|number |
0.29.0 | |
hideExpandedColumn | Whether to hide the expansion button column and turn off the rendering of the expansion button when it is turned on | boolean | true | |
indentSize | indent size of TableCell | number | 20 | |
loading | Table is loading or not | boolean | false | |
pagination | Paging component configuration | boolean|TablePaginationProps | true | |
prefixCls | Style name prefix | string | ||
renderGroupSection | Header rendering method | (groupKey?: string | number, group?: string[] | number[]) => ReactNode | 0.29.0 | |
renderPagination | Customize the rendering method of pagination. | (paginationProps?: TablePaginationProps) => ReactNode | 1.13.0 | |
resizable | Whether to turn on the telescopic column function, the column that needs to be telescopic must provide the value of width | boolean|Resizable | false | |
rowExpandable | Whether the row can be expanded, turning off the rendering of the expandable button when the value is false | (record: RecordType): => boolean | () => true | 0.27.0 |
rowKey | The value of the table row key, which can be a string or a function. | string | (record: RecordType) => string | 'key' | |
rowSelection | See rowSelection | object | null | |
scroll | Whether the table is scrollable, configure the width or height of the scroll area, see scroll | object | - | |
showHeader | Does it show the header? | boolean | true | |
size | Table size, will effect the padding of the rows |
"default"|"middle"|"small" | "default" | 1.0.0 |
title | Table Title | string |ReactNode |(pageData: RecordType[]) => string|ReactNode |
||
virtualized | Virtualization settings | Virtualized | false | 0.33.0 |
virtualized.itemSize | Row height | number|(index: number) => number | 56 | 0.33.0 |
virtualized.onScroll | Virtualization scroll callback method | ( scrollDirection?: 'forward' | 'backward', scrollOffset?: number, scrollUpdateWasRequested?: boolean ) => void | 0.33.0 | |
onChange | Trigger when paging, sorting, filtering changes | ({ pagination: TablePaginationProps, filters: Array<*>, sorter: object, extra: any }) => void |
||
onExpand | Trigger when clicking on the row expansion icon | (expanded: boolean, record: RecordType, DOMEvent: MouseEvent) => void | The third parameter DOMEvent requires version >=0.28.0 | |
onExpandedRowsChange | Triggers when unfolding row changes | (rows: RecordType[]) => void | ||
onGroupedRow | Similar to onRow, but this parameter is used to define the row attribute of the grouping header alone | (record: RecordType, index: number) => object | 0.29.0 | |
onHeaderRow | Set the header row property, and the returned object is merged to the header line | (columns: Column[], index: number) => object | ||
onRow | Set the row property, and the returned object is merged to the table row | (record: RecordType, index: number) => object |
Some of the type definitions used above:
// PaginationProps is the props supported by the Pagination component
interface TablePaginationProps extends PaginationProps {
position?: PaginationPosition;
formatPageText?: FormatPageText;
}
type VirtualizedMode = 'list' | 'grid';
type VirtualizedItemSizeFn = (index?: number) => number;
type VirtualizedOnScrollArgs = {
scrollDirection?: 'forward' | 'backward';
scrollOffset?: number;
scrollUpdateWasRequested?: boolean;
};
type VirtualizedOnScroll = (object: VirtualizedOnScrollArgs) => void;
type Virtualized = boolean | {
mode?: VirtualizedMode;
itemSize?: number | VirtualizedItemSizeFn;
onScroll?: VirtualizedOnScroll;
};
RecordType is a generic parameter of Table and Column, and the default is object type. You can use RecordType like this:
import { ColumnProps } from 'table/interface';
interface Record {
title?: string;
dataIndex?: string;
width?: number;
render?: Function;
key?: string;
name?: string;
age?: number;
address?: string;
}
const columns: ColumnProps<Record>[] = [
{
title: 'Name',
dataIndex: 'name',
width: 200,
},
// ...
];
const data: Record[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park, New York No. 1 Lake Park',
},
// ...
];
function App() {
return (
<Table<Record> columns={columns} dataSource={data} />
);
}
Also in
column.onCell
column.onHeaderCell
Properties or events supported by td / th can also be returned.
import React from 'react';
import { Table } from '@douyinfe/semi-ui';
() => (
<Table
onRow={(record, index) => {
return {
onClick: event => {},
onMouseEnter: event => {},
onMouseLeave: event => {},
className: '',
// ...
// Other attributes or events that can be applied to tr
};
}}
onHeaderRow={(columns, index) => {
return {
onClick: event => {},
onMouseEnter: event => {},
onMouseLeave: event => {},
className: '',
// ...
// Other attributes or events that can be applied to th
};
}}
/>
);
Parameters | Instructions | Type | Default | Version |
---|---|---|---|---|
align | Setting the alignment of columns | 'left '| 'right '| 'center' | 'left' | |
children | Settings for sub-columns when the header is merged | Column[] | ||
className | Column style name | string | ||
colSpan | When header columns merge, set to 0, do not render | number | ||
dataIndex | The key corresponding to the column data in the data item. It is required when using sorter or filter. | string | ||
defaultFilteredValue | Default value of the filter, the filter state of the external control column with a value of the screened value array | any[] | 2.5.0 | |
defaultSortOrder | The default value of sortOrder, one of 'ascend'|'descend'|false | boolean| string | false | 1.31.0 |
filterChildrenRecord | Whether the child data needs to be filtered locally. If this function is enabled, if the child meets the filtering criteria, the parent will retain it even if it does not meet the criteria. | boolean | 0.29.0 | |
filterDropdown | You can customize the filter menu. This function is only responsible for rendering the layer and needs to write a variety of interactions. | ReactNode | ||
filterDropdownProps | Props passing to Dropdown, see more in Dropdown API | object | ||
filterDropdownVisible | Visible of Dropdown, see more in Dropdown API | boolean | ||
filterIcon | Custom filter icon | boolean|ReactNode|(filtered: boolean) => ReactNode | ||
filterMultiple | Whether to choose more | boolean | true | |
filteredValue | Controlled property of the filter, the filter state of the external control column with a value of the screened value array | any[] | ||
filters | Filter menu items for the header | Filter[] | ||
fixed | Whether the column is fixed, optional true (equivalent to left) 'left' 'right' | boolean|string | false | |
key | The key required by React, if a unique dataIndex has been set, can ignore this property | string | ||
render | A rendering function that generates complex data, the parameters are the value of the current row, the current row data, the row index, and the table row / column merge can be set in return object | (text: any, record: RecordType, index: number, { expandIcon?: ReactNode }) => React|object | ||
renderFilterDropdownItem | Customize the rendering method of each filter item. For usage details, see Custom Filter Item Rendering | ({ value: any, text: any, onChange: Function, level: number, ...otherProps }) => ReactNode | - | 1.1.0 |
sortChildrenRecord | Whether to sort child data locally | boolean | 0.29.0 | |
sortOrder | The controlled property of the sorting, the sorting of this control column can be set to 'ascend'|'descended '|false | boolean | false | |
sorter | Sorting function, local sorting uses a function (refer to the compreFunction of Array.sort), requiring a server-side sorting can be set to true | boolean|(r1: RecordType, r2: RecordType) => number | true | |
title | Column header displays text. When a function is passed in, title will use the return value of the function; when other types are passed in, they will be aggregated with sorter and filter | string | ReactNode|({ filter: ReactNode, sorter: ReactNode, selection: ReactNode }) => ReactNode | Function type requires 0.34.0 | |
useFullRender | Whether to completely customize the rendering, see Full Custom Rendering for usage details | boolean | false | 0.34.0 |
width | Column width | string | number | ||
onCell | Set cell properties | (record: RecordType, rowIndex: number) => object | ||
onFilter | Determine the running function of the filter in local mode | (filteredValue: any[], record: RecordType) => boolean | ||
onFilterDropdownVisibleChange | A callback when a custom filter menu is visible | (visible: boolean) => void | ||
onHeaderCell | Set the head cell property | (column: RecordType, columnIndex: number) => object |
Some of the type definitions used above:
// RecordType is a generic parameter of Table and Column, the default is object type
type Filter = {
value: any;
text: React.ReactNode;
children?: Filter[];
};
Parameters | Instructions | Type | Default | Version |
---|---|---|---|---|
className | Style name listed | string | ||
disabled | Disabled Checkbox in Table header or not. |
boolean | false | 0.32.0 |
fixed | Secure the selection box column to the left. | boolean | false | |
getCheckboxProps | Default property configuration for the selection box | (record: RecordType) => object | ||
hidden | Hide selection column or not | boolean | false | 0.34.0 |
selectedRowKeys | Specifies the key array of the selected item, which needs to work with onChange | string [] | ||
title | Custom List Selection Box Title | string | ReactNode | |
width | Custom list selection box width | string | number | |
onChange | A callback in the event of a change in the selected item. The first parameter will save the row keys selected last time, even if you do paging control or update the dataSource FAQ | (selectedRowKeys: number[]|string[], selectedRows: RecordType[]) => void | ||
onSelect | Callback when the user manually clicks the selection box of a row | (record: RecordType, selected: boolean, selectedRows: RecordType[], nativeEvent: MouseEvent) => void | ||
onSelectAll | The user manually clicks the callback of the header selection box, and all optional rows in the dataSource will be selected/unselected | (selected: boolean, selectedRows: RecordType[], changedRows: RecordType[]) => void |
Parameters | Instructions | Type | Default | Version |
---|---|---|---|---|
scrollToFirstRowOnChange | Whether to automatically scroll to the top of the table after paging, sorting, and filtering changes | boolean | false | 1.1.0 |
x | Set the width of the horizontal scroll area, which can be pixel value, percentage, or 'max-content' | string|number | ||
y | Set the height of the vertical scroll area, which can be a pixel value | number |
Page-turning component configuration. Pagination suggests not to use literal value.
Parameters | Instructions | Type | Default | Version |
---|---|---|---|---|
currentPage | Current page number | number | - | |
defaultCurrentPage | Default current page number | number | 1 | >=1.1.0 |
formatPageText | Page-turning area copywriting custom formatting, pass false to close copywriting display; This item affects the copy display on the left of the page turning area of the form. It is different from the showTotal parameter of thePagination component. |
boolean| ({ currentStart: number, currentEnd: number, total: number }) => string|ReactNode | true | 0.27.0 |
pageSize | Number of entries per page | number | 10 | |
position | Location | 'bottom '|'top '|'both' | 'bottom' | |
total | Total number of entries | number | 0 | >=0.25.0 |
For other configurations, see Pagination
The parameters of the resizable object type, which mainly include event methods when the table column is scaled. These event methods can return an object that merges with the final column.
Parameters | Instructions | Type | Default |
---|---|---|---|
onResize | Triggers when the table column changes its width | (column: Column) => Column | |
onResizeStart | Triggers when the table column starts to change the width. | (column: Column) => Column | |
onResizeStop | Triggers when the table column stops changing the width | (column: Column) => Column |
Some internal methods provided by Table can be accessed through ref:
import React, { useRef, useEffect } from 'react';
import { Table } from '@douyinfe/semi-ui';
function Demo() {
const ref = useRef();
useEffect(() => {
ref.getCurrentPageData(); // => { dataSource: [/*...*/], groups: /*...*/ }
}, []);
return (
<Table
columns={
[
/*...*/
]
}
dataSource={
[
/*...*/
]
}
ref={ref}
/>
);
}
Parameters | Instructions | Version |
---|---|---|
getCurrentPageData() | Returns the data object of the current page: { dataSource: RecordType[], groups: Map<{groupKey: string, recordKeys: Set}> } | 0.37.0 |