How to Use
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 { Table } from '@fibrous/semi-ui-solid';
function App() {
const columns = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Size',
dataIndex: 'size',
},
{
title: 'Owner',
dataIndex: 'owner',
},
{
title: 'Updated At',
dataIndex: 'updateTime',
},
];
const data = [
{
key: '1',
name: 'Semi Design Mockup.fig',
size: '2M',
owner: 'Jiang Pengzhi',
updateTime: '2020-02-02 05:13',
},
{
key: '2',
name: 'Semi Design Presentation',
size: '2M',
owner: 'Hao Xuan',
updateTime: '2020-01-17 05:31',
},
{
key: '3',
name: 'Design Document',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
},
];
return <Table columns={columns} dataSource={data} pagination={false} />;
}
Demos
Basic Table
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.
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | |
|---|---|---|---|---|---|
| 2M | 已交付 | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 已延期 | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | 待评审 | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconMore, IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
export default function TableDemo() {
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
return <Table columns={columns} dataSource={data} pagination={false} />;
}JSX Writing
You can also use JSX syntax definitions columns, note that Table only supports columns JSX syntax definition. You cannot use any component to wrap Table.Column.
| 标题 | 大小 | 所有者 | 更新时间 | |
|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type AvatarColor } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: string;
updateTime: string;
avatarBg: AvatarColor;
}
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const renderName = (text: string, record: Row) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
};
const renderOwner = (text: string, record: Row) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
};
export default function TableDemo() {
return (
<Table dataSource={data} pagination={false}>
<Table.Column title="标题" dataIndex="name" key="name" render={renderName} />
<Table.Column title="大小" dataIndex="size" key="size" />
<Table.Column title="所有者" dataIndex="owner" key="owner" render={renderOwner} />
<Table.Column title="更新时间" dataIndex="updateTime" key="updateTime" />
<Table.Column title="" dataIndex="operate" key="operate" render={() => <IconMore />} />
</Table>
);
}Row Selection Operation
This feature can be turned on by passing in rowSelection.
- Click the selection box in the header, and all rows in the
dataSourcethat are not in the state ofdisabledwill be selected. The callback function for selecting all rows isonSelectAll; - Clicking on the row selection box will select the current row. Its callback function is
onSelect;
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | ||
|---|---|---|---|---|---|---|
| 2M | 已交付 | 姜姜鹏志 | 2020-02-02 05:13 | |||
| 2M | 已延期 | 郝郝宣 | 2020-01-17 05:31 | |||
| 34KB | 待评审 | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconTickCircle, IconComment, IconClear, IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: 'Semi D2C 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'wait',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '5',
name: 'Semi D2C 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '6',
name: 'Semi D2C 设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'success',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
const rowSelection: RowSelectionProps<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
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 = { pageSize: 3 };
export default function TableDemo() {
return <Table columns={columns} dataSource={data} rowSelection={rowSelection} pagination={pagination} />;
}Custom Rendering
Users can use Column.render to customize the rendering of a column of cells, which is suitable for rendering more complex cell content. render is called again whenever the row data or index changes; read the fields of options in the returned JSX to follow the row state, with no extra subscription needed.
The fourth parameter options of the render function is an object containing the following properties:
expandIcon: Render function of the expand icon (when using tree data or expandable rows); call it where it is inserted, e.g.{options.expandIcon?.()}selection: Render function of the selection checkbox (when row selection is enabled)indentText: Render function of the indent content (when using tree data)isHovering: Whether the current row is in hover state
With the isHovering parameter, you can implement interaction effects such as displaying action buttons on mouse hover.
| 标题 | 大小 | 交付状态 | 所有者 | 更新日期 | |
|---|---|---|---|---|---|
| 2M | 已交付 | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 已延期 | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | 待评审 | ZZoey Edwards | 2020-01-26 11:01 | ||
| 34KB | 已交付 | 姜姜琪 | 2020-01-26 11:01 |
Code
import { createSignal } from 'solid-js';
import { IconTickCircle, IconComment, IconClear, IconDelete } from '@fibrous/semi-icons-solid';
import { IllustrationNoResult, IllustrationNoResultDark } from '@fibrous/semi-illustrations-solid';
import { Table, Avatar, Button, Empty, Typography, Tag, type ColumnProps } from '@fibrous/semi-ui-solid';
const { Text } = Typography;
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
status: 'success' | 'pending' | 'wait';
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue' | 'green';
}
const raw: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿标题可能有点长这时候应该显示 Tooltip.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
status: 'success',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
status: 'pending',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'wait',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: 'Semi D2C 设计文档可能也有点长所以也会显示 Tooltip',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
status: 'success',
owner: '姜琪',
updateTime: '2020-01-26 11:01',
avatarBg: 'green',
},
];
export default function TableDemo() {
const [dataSource, setDataSource] = createSignal<Row[]>(raw);
const removeRecord = (key: string) => {
setDataSource((list) => list.filter((item) => item.key !== key));
};
const resetData = () => {
setDataSource([...raw]);
};
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<span style={{ display: 'flex', 'align-items': 'center' }}>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{/* 宽度 = 单元格设定宽度 − 非文本内容宽度 */}
<Text ellipsis={{ showTooltip: true }} style={{ width: 'calc(400px - 76px)' }}>
{text}
</Text>
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
width: 150,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
width: 300,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
},
{
title: '',
dataIndex: 'operate',
render: (_text, record) => <Button aria-label="删除" icon={<IconDelete />} theme="borderless" onClick={() => removeRecord(record.key)} />,
},
];
return (
<>
<Button onClick={resetData} style={{ 'margin-bottom': '10px' }}>
重置
</Button>
<Table style={{ 'min-height': '350px' }} columns={columns} dataSource={dataSource()} pagination={false} empty={<Empty image={<IllustrationNoResult />} darkModeImage={<IllustrationNoResultDark />} description="搜索无结果" />} />
</>
);
}Table With Pagination
Table paging currently supports two modes: controlled and uncontrolled.
- In controlled mode, the paging state is passed entirely externally, depending on whether the pagination .currentPage field is passed. In general, the controlled mode is suitable for remotely pulling data and rendering.
- In uncontrolled mode, Table passes the incoming dataSource length as total to the Pagination component by default, and of course you can also pass a total field to overwrite the value of the Table component, but we do not recommend users to pass this field in uncontrolled paging mode.
Note: The custom
pagination.totalfield passed in uncontrolled conditions is supported.
标题 | 交付状态 | 所有者 |
|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2024-05-06 | ||
| 5 KB | 待评审 | 郝郝宣 | 2024-05-11 | ||
| 10 KB | 已交付 | 姜姜鹏志 | 2024-05-16 | ||
| 15 KB | 待评审 | 郝郝宣 | 2024-05-21 | ||
| 20 KB | 已交付 | 姜姜鹏志 | 2024-05-26 | ||
| 25 KB | 待评审 | 郝郝宣 | 2024-05-31 | ||
| 30 KB | 已交付 | 姜姜鹏志 | 2024-06-05 | ||
| 35 KB | 待评审 | 郝郝宣 | 2024-06-10 | ||
| 40 KB | 已交付 | 姜姜鹏志 | 2024-06-15 | ||
| 45 KB | 待评审 | 郝郝宣 | 2024-06-20 |
Code
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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;
const getData = () => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: RowSelectionProps<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}Pull Remote Data
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.
标题 | 交付状态 | 所有者 | ||
|---|---|---|---|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 待评审 | 郝郝宣 | 2024-05-11 | |
| 10 KB | 已交付 | 姜姜鹏志 | 2024-05-16 | |
| 15 KB | 待评审 | 郝郝宣 | 2024-05-21 | |
| 20 KB | 已交付 | 姜姜鹏志 | 2024-05-26 |
Code
import { createResource, createSignal } from 'solid-js';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const pageSize = 5;
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const data = getData();
const fetchData = (currentPage: number) => {
return new Promise<Row[]>((res) => {
setTimeout(() => {
res(data.slice((currentPage - 1) * pageSize, currentPage * pageSize));
}, 300);
});
};
export default function TableDemo() {
const [currentPage, setPage] = createSignal(1);
// createResource drops responses for pages the user has already left
const [pageData] = createResource(currentPage, fetchData);
return (
<Table
columns={columns}
dataSource={pageData() ?? []}
pagination={{
currentPage: currentPage(),
pageSize: 5,
total: data.length,
onPageChange: setPage,
}}
loading={pageData.loading}
/>
);
}Fixed Column or Head
You can fix the column by setting the Fixed attribute of the column and scroll.x, and fix the header by setting scroll.y.
- It is recommended to specify scroll.x as a fixed value or percentage greater than the width of the table. If it is a fixed value, set it to >= the sum of all fixed column widths + the sum of all table column widths.
- 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 or when the fixed column fails, specify the width width of the fixed column, if still 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.
标题 | 所有者 |
|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |||
| 5 KB | 郝郝宣 | 2024-05-11 | |||
| 10 KB | 姜姜鹏志 | 2024-05-16 | |||
| 15 KB | 郝郝宣 | 2024-05-21 | |||
| 20 KB | 姜姜鹏志 | 2024-05-26 | |||
| 25 KB | 郝郝宣 | 2024-05-31 | |||
| 30 KB | 姜姜鹏志 | 2024-06-05 | |||
| 35 KB | 郝郝宣 | 2024-06-10 | |||
| 40 KB | 姜姜鹏志 | 2024-06-15 | |||
| 45 KB | 郝郝宣 | 2024-06-20 |
Code
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 200,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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: () => <IconMore />,
},
];
const getData = () => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const scroll = { y: 300, x: 1200 };
const rowSelection: RowSelectionProps<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
fixed: true,
};
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}The header can be fixed to the top of the page with the sticky property. When passing top, you can control the distance from the scroll container.
After turning on sticky, Table will automatically turn on the fixed layout, and the column width will be determined by column.width. Columns without a given width are automatically assigned by the browser.
Table Header With Sorting and Filtering Function
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.
Note: Sorting and filtering columns must set independent "dataIndex"
| 交付状态 | 所有者 |
|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2024-05-06 | ||
| 5 KB | 待评审 | 郝郝宣 | 2024-05-11 | ||
| 10 KB | 已交付 | 姜姜鹏志 | 2024-05-16 | ||
| 15 KB | 待评审 | 郝郝宣 | 2024-05-21 | ||
| 20 KB | 已交付 | 姜姜鹏志 | 2024-05-26 | ||
| 25 KB | 待评审 | 郝郝宣 | 2024-05-31 | ||
| 30 KB | 已交付 | 姜姜鹏志 | 2024-06-05 | ||
| 35 KB | 待评审 | 郝郝宣 | 2024-06-10 | ||
| 40 KB | 已交付 | 姜姜鹏志 | 2024-06-15 | ||
| 45 KB | 待评审 | 郝郝宣 | 2024-06-20 |
Code
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Tag, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: RowSelectionProps<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}When sorter is a function type, the sortOrder status can be obtained through the third parameter of the function. The function type is (a?: RecordType, b?: RecordType, sortOrder?: 'ascend' | 'descend') => number.
You can control whether to display the sorting tip through the showSortTip attribute, which defaults to false. When the tip is turned on, when there is only sorting function, the sorting prompt will be displayed when the mouse is moved to the table header; in other cases, the sorting prompt will be displayed only when the mouse is moved to the sorting icon.
Note: When using the sortOrder attribute for controlled sorting, since the next sort order cannot be predicted, showSortTip does not take effect and the prompt will not be displayed.
| 标题 | 所有者 | 更新日期 | |
|---|---|---|---|
| 3 KB | 姜姜鹏志 | 2020-02-02 | |
| 未知 | 郝郝宣 | 2020-01-17 | |
| 1 KB | ZZoey Edwards | 2020-01-26 | |
| 5 KB | ZZoey Edwards | 2020-01-26 | |
| 未知 | ZZoey Edwards | 2020-01-26 | |
| 2 KB | ZZoey Edwards | 2020-01-26 |
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
const figmaIconUrl = '/assets/placeholder/app.svg';
const docIconUrl = '/assets/docs-icon.png';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: number | undefined;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
sorter: (r1, r2, order) => {
const a = r1.size;
const b = r2.size;
if (typeof a === 'number' && typeof b === 'number') {
return a - b; // 数字正常比较大小
} else if (typeof a === 'undefined') {
return order === 'ascend' ? 1 : -1; // undefined 在后面
} else if (typeof b === 'undefined') {
return order === 'ascend' ? -1 : 1; // undefined 在后面
} else {
return 0; // 保持原来的顺序
}
},
showSortTip: true,
render: (text) => (text ? `${text} KB` : '未知'),
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
const dataSource: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: figmaIconUrl,
size: 3,
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: docIconUrl,
size: undefined,
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档 3',
nameIconSrc: docIconUrl,
size: 1,
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: '设计文档 4',
nameIconSrc: docIconUrl,
size: 5,
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '5',
name: '设计文档 5',
nameIconSrc: docIconUrl,
size: undefined,
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '6',
name: '设计文档 6',
nameIconSrc: docIconUrl,
size: 2,
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} />;
}Custom Header Filtering
To display a filter input box in the header, pass a function returning JSX to title and use it together with filteredValue. filteredValue can be read from a signal; when it changes, the header and the data update together.
标题 | 交付状态 | 所有者 | ||
|---|---|---|---|---|
| 0 KB | 已交付 | 姜姜鹏志 | 2024-01-25 | |
| 5 KB | 待评审 | 郝郝宣 | 2024-01-30 | |
| 10 KB | 已交付 | 姜姜鹏志 | 2024-02-04 | |
| 15 KB | 待评审 | 郝郝宣 | 2024-02-09 | |
| 20 KB | 已交付 | 姜姜鹏志 | 2024-02-14 | |
| 25 KB | 待评审 | 郝郝宣 | 2024-02-19 | |
| 30 KB | 已交付 | 姜姜鹏志 | 2024-02-24 | |
| 35 KB | 待评审 | 郝郝宣 | 2024-02-29 | |
| 40 KB | 已交付 | 姜姜鹏志 | 2024-03-05 | |
| 45 KB | 待评审 | 郝郝宣 | 2024-03-10 |
Code
import { createSignal } from 'solid-js';
import * as dateFns from 'date-fns';
import { IconTickCircle, IconComment, IconClear } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Input, Space, Tag, type ColumnProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
status: 'success' | 'wait';
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 首页${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
status: isSemiDesign ? 'success' : 'wait',
updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const figmaIconUrl = '/assets/placeholder/app.svg';
export default function TableDemo() {
const [filteredValue, setFilteredValue] = createSignal<string[]>([]);
// The composition flag is read by event handlers only, so it does not need to be reactive
let isComposition = false;
const handleChange = (value: string) => {
if (isComposition) {
return;
}
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const handleCompositionStart = () => {
isComposition = true;
};
const handleCompositionEnd = (event: { currentTarget: HTMLInputElement }) => {
isComposition = false;
const value = event.currentTarget.value;
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
const columns: ColumnProps<Row>[] = [
{
title: () => (
<Space>
<span>标题</span>
<Input placeholder="请输入筛选值" style={{ width: '200px' }} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onChange={handleChange} showClear />
</Space>
),
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
onFilter: (value, record) => record.name.includes(value),
// Read in a getter so the table follows the signal after the columns are created
get filteredValue() {
return filteredValue();
},
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '交付状态',
dataIndex: 'status',
render: (text: Row['status']) => {
const tagConfig = {
// getters create only the icon of the row's status
success: {
color: 'green',
get prefixIcon() {
return <IconTickCircle />;
},
text: '已交付',
},
pending: {
color: 'pink',
get prefixIcon() {
return <IconClear />;
},
text: '已延期',
},
wait: {
color: 'cyan',
get prefixIcon() {
return <IconComment />;
},
text: '待评审',
},
} as const;
const tagProps = tagConfig[text];
return (
<Tag shape="circle" {...tagProps} style={{ 'user-select': 'text' }}>
{tagProps.text}
</Tag>
);
},
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
return <Table columns={columns} dataSource={dataSource} />;
}Custom Filter Rendering
Use renderFilterDropdown to customize the filter dropdown panel. The fields of the argument are live; read them in the returned JSX.
You can call setTempFilteredValue to store the filter value while the user types it, and call confirm after the input is complete to apply the filter. You can also filter directly with confirm({ filteredValue }).
type RenderFilterDropdown = (props: RenderFilterDropdownProps) => JSX.Element;
interface RenderFilterDropdownProps {
/** Temporary filter value, initially `filteredValue` or `defaultFilteredValue` */
tempFilteredValue: any[];
/** Set the temporary filter value */
setTempFilteredValue: (tempFilteredValue: any[]) => void;
/** `confirm` assigns `tempFilteredValue` to `filteredValue` by default and triggers the `onChange` event. You can also pass `filteredValue` to set the filter value directly */
confirm: (props?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
/** Clear the filter value and the temporary filter value */
clear: (props?: { closeDropdown?: boolean }) => void;
/** Close the dropdown */
close: () => void;
/** Restore the temporary filter value to the applied filter value */
reset: () => void;
/** Filter options, may be omitted if not needed */
filters?: RenderDropdownProps['filters'];
}
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 10 KB | 姜姜鹏志 | 2024-05-16 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 | |
| 30 KB | 姜姜鹏志 | 2024-06-05 | |
| 40 KB | 姜姜鹏志 | 2024-06-15 | |
| 50 KB | 姜姜鹏志 | 2024-06-25 | |
| 60 KB | 姜姜鹏志 | 2024-07-05 | |
| 70 KB | 姜姜鹏志 | 2024-07-15 | |
| 80 KB | 姜姜鹏志 | 2024-07-25 | |
| 90 KB | 姜姜鹏志 | 2024-08-04 |
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, Input, Button, Space, type ColumnProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const figmaIconUrl = '/assets/placeholder/app.svg';
export default function TableDemo() {
let inputEl: HTMLInputElement | undefined;
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
onFilter: (value, record) => record.name.includes(value),
renderFilterDropdown: (props) => {
const handleChange = (value: string) => {
const filteredValue = value ? [value] : [];
props.setTempFilteredValue(filteredValue);
};
return (
<Space vertical align="start" style={{ padding: '8px' }}>
<Input inputRef={(el) => (inputEl = el)} value={props.tempFilteredValue[0]} onChange={handleChange} />
<Space>
<Button onClick={() => props.confirm({ closeDropdown: true })}>筛选 + 关闭</Button>
<Button onClick={() => props.clear({ closeDropdown: true })}>清除 + 关闭</Button>
<Button onClick={() => props.close()}>直接关闭</Button>
</Space>
</Space>
);
},
onFilterDropdownVisibleChange: () => {
inputEl?.focus();
},
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
onFilter: (value, record) => record.owner.includes(value),
defaultFilteredValue: ['姜鹏志'],
renderFilterDropdown: (props) => {
const handleChange = (value: string) => {
if (value) {
props.setTempFilteredValue([value]);
} else {
props.setTempFilteredValue([]);
}
};
return (
<Space vertical align="start" style={{ padding: '8px' }}>
<Input value={props.tempFilteredValue[0]} onChange={handleChange} />
<Space>
<Button onClick={() => props.confirm({ closeDropdown: false })}>筛选后不关闭</Button>
<Button onClick={() => props.clear({ closeDropdown: false })}>清除后不关闭</Button>
<Button onClick={() => props.close()}>直接关闭</Button>
</Space>
</Space>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
return <Table columns={columns} dataSource={dataSource} />;
}Filter Confirm Mode
By setting filterConfirmMode='confirm', the filter dropdown panel can support confirm mode. In this mode:
- Clicking filter items will not take effect immediately, but will be temporarily stored
- The bottom of the dropdown panel will show "Confirm" and "Reset" buttons
- Clicking the "Confirm" button will apply the filter conditions and close the dropdown panel
- Clicking the "Reset" button will restore to the initial state when the dropdown was opened (will not close the panel)
This feature is suitable for scenarios where multiple filter conditions need to be selected before applying them at once, avoiding triggering filtering on every click.
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-01-25 | |
| 5 KB | 郝郝宣 | 2024-01-30 | |
| 10 KB | 姜姜鹏志 | 2024-02-04 | |
| 15 KB | 郝郝宣 | 2024-02-09 | |
| 20 KB | 姜姜鹏志 | 2024-02-14 | |
| 25 KB | 郝郝宣 | 2024-02-19 | |
| 30 KB | 姜姜鹏志 | 2024-02-24 | |
| 35 KB | 郝郝宣 | 2024-02-29 | |
| 40 KB | 姜姜鹏志 | 2024-03-05 | |
| 45 KB | 郝郝宣 | 2024-03-10 |
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
for (let i = 0; i < 20; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
data.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 首页${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date('2024-01-25').valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const figmaIconUrl = '/assets/placeholder/app.svg';
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 300,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design',
},
{
text: 'Semi Pro 设计稿',
value: 'Semi Pro',
},
],
onFilter: (value, record) => record.name.includes(value),
filterMultiple: true,
filterConfirmMode: 'confirm',
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
filters: [
{
text: '姜鹏志',
value: '姜鹏志',
},
{
text: '郝宣',
value: '郝宣',
},
],
onFilter: (value, record) => record.owner.includes(value),
filterMultiple: true,
filterConfirmMode: 'confirm',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} />;
}Custom Filter Item Rendering
Pass renderFilterDropdownItem in the column to customize how each filter item is rendered. The parameters are:
text: stringThe text of the current filter item;value: anyThe value of the current filter item;checked: booleanWhether the current filter item is checked;filteredValue: any[]All current filter values;level: numberThe level of the current filter item; for nested filter items, this value is >= 1;filterMultiple: booleanWhether the current filter item supports multiple selection.
| 所有者 |
|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | ||
| 5 KB | 郝郝宣 | 2024-05-11 | ||
| 10 KB | 姜姜鹏志 | 2024-05-16 | ||
| 15 KB | 郝郝宣 | 2024-05-21 | ||
| 20 KB | 姜姜鹏志 | 2024-05-26 | ||
| 25 KB | 郝郝宣 | 2024-05-31 | ||
| 30 KB | 姜姜鹏志 | 2024-06-05 | ||
| 35 KB | 郝郝宣 | 2024-06-10 | ||
| 40 KB | 姜姜鹏志 | 2024-06-15 | ||
| 45 KB | 郝郝宣 | 2024-06-20 |
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, Dropdown, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
renderFilterDropdownItem: (itemInfo) => (
<Dropdown.Item onClick={itemInfo.onChange} active={itemInfo.checked}>
{itemInfo.text}
</Dropdown.Item>
),
filterDropdownProps: {
showTick: true,
},
sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowSelection: RowSelectionProps<Row> = {
onChange: (selectedRowKeys, selectedRows) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.name === 'Semi Design 设计稿0.fig', // 这一行不可勾选
name: record.name,
}),
};
const scroll = { y: 300 };
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
}A Table That Can Be Expanded
A Common Table That Can Be Expanded
To render an expandable table, besides passing the expandedRowRender method to Table, you must also specify rowKey (defaults to key). Table uses rowKey to obtain the unique identifier of each row. Expanded rows can be controlled through expandedRowKeys and onExpandedRowsChange.
- If
rowKeyis a Function, the result ofrowKey(record)is taken as the row unique ID - If
rowKeyis a string type, you takerecord[rowKey]as the row unique ID
| 标题 | 大小 | 所有者 | 更新日期 | ||
|---|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | |||
| 2M | 郝郝宣 | 2020-01-17 05:31 | |||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type DescriptionsData, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
width: 500,
dataIndex: 'name',
render: (text, record) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const rowSelection: RowSelectionProps<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
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 expandData: DescriptionsData[][] = [
[
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
[
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
[
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
];
const renderValue = (item: DescriptionsData) => (item.key === '垂类标签' ? <Tag style={{ margin: '0px' }}>{item.value}</Tag> : item.value);
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => <Descriptions align="justify" data={expandData[index]} renderValue={renderValue} />;
export default function TableDemo() {
return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} rowSelection={rowSelection} pagination={false} />;
}The Unfold Button Is Rendered In a Separated Column
By default, the expansion button will be rendered in the same cell as the first copy, and you can set hideExpandedColumn to false to render as a separate column:
| 标题 | 大小 | 所有者 | 更新日期 | |||
|---|---|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type DescriptionsData, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
width: 500,
dataIndex: 'name',
render: (text, record) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const rowSelection: RowSelectionProps<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
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 expandData: DescriptionsData[][] = [
[
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
[
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
[
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
];
const renderValue = (item: DescriptionsData) => (item.key === '垂类标签' ? <Tag style={{ margin: '0px' }}>{item.value}</Tag> : item.value);
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => <Descriptions align="justify" data={expandData[index]} renderValue={renderValue} />;
export default function TableDemo() {
return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} hideExpandedColumn={false} rowSelection={rowSelection} pagination={false} />;
}Turn Off Rendering of Expandable Button of a Row
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.
| 标题 | 大小 | 所有者 | 更新日期 | |||
|---|---|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, Descriptions, Tag, type ColumnProps, type DescriptionsData, type ExpandedRowRender, type RowSelectionProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
width: 500,
dataIndex: 'name',
render: (text, record) => {
return (
<span>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const rowSelection: RowSelectionProps<Row> = {
getCheckboxProps: (record) => ({
disabled: record.name === '设计文档', // 这一行不可勾选
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 expandData: DescriptionsData[][] = [
[
{ key: '实际用户数量', value: '1,480,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '3 级' },
{ key: '垂类标签', value: '设计' },
{ key: '认证状态', value: '未认证' },
],
[
{ key: '实际用户数量', value: '2,480,000' },
{ key: '7 天留存', value: '90%' },
{ key: '安全等级', value: '1 级' },
{ key: '垂类标签', value: '模板' },
{ key: '认证状态', value: '已认证' },
],
[
{ key: '实际用户数量', value: '2,920,000' },
{ key: '7 天留存', value: '98%' },
{ key: '安全等级', value: '2 级' },
{ key: '垂类标签', value: '文档' },
{ key: '认证状态', value: '已认证' },
],
];
const renderValue = (item: DescriptionsData) => (item.key === '垂类标签' ? <Tag style={{ margin: '0px' }}>{item.value}</Tag> : item.value);
const expandRowRender: ExpandedRowRender<Row> = (_record, index) => <Descriptions align="justify" data={expandData[index]} renderValue={renderValue} />;
export default function TableDemo() {
return <Table rowKey="name" columns={columns} dataSource={data} expandedRowRender={expandRowRender} rowExpandable={(record) => record.name !== '设计文档'} hideExpandedColumn={false} rowSelection={rowSelection} pagination={false} />;
}Tree Data Display
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
rowKeyparameter to specify a property name as the primary key.
Simple example of tree data
| Key | 名称 | 数据类型 | 描述 | 默认值 |
|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 |
Code
import { Table, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key?: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: Row[];
}
const columns: ColumnProps<Row>[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
];
const data: Row[] = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
dataKey: 'video_url',
name: '视频地址',
type: 'String 字符串',
description: '视频的唯一链接',
default: '无',
},
],
},
],
},
{
key: 2,
dataKey: 'text_info',
name: '文本信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 21,
dataKey: 'title',
name: '视频标题',
type: 'String 字符串',
description: '视频的标题',
default: '无',
},
{
key: 22,
dataKey: 'video_description',
name: '视频描述',
type: 'String 字符串',
description: '视频的描述',
default: '无',
},
],
},
];
export default function TableDemo() {
return <Table columns={columns} defaultExpandAllRows dataSource={data} />;
}Rows of Interchangeable Tree Data
You can do row switching by changing the order of the dataSource elements.
| Key | 名称 | 数据类型 | 描述 | 默认值 | |
|---|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 | |
| status | 视频状态 | Enum <Integer> 枚举 | 视频的可见、推荐状态 | 1 | |
| vid | 视频 ID | String 字符串 | 标识视频的唯一 ID | 无 | |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 | |
| title | 视频标题 | String 字符串 | 视频的标题 | 无 | |
| video_description | 视频描述 | String 字符串 | 视频的描述 | 无 |
Code
import { createSignal } from 'solid-js';
import { IconArrowUp, IconArrowDown } from '@fibrous/semi-icons-solid';
import { Table, Button, type ColumnProps } from '@fibrous/semi-ui-solid';
type TreeRow = {
key?: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: TreeRow[];
};
const raw: TreeRow[] = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
dataKey: 'video_url',
name: '视频地址',
type: 'String 字符串',
description: '视频的唯一链接',
default: '无',
},
],
},
],
},
{
key: 2,
dataKey: 'text_info',
name: '文本信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 21,
dataKey: 'title',
name: '视频标题',
type: 'String 字符串',
description: '视频的标题',
default: '无',
},
{
key: 22,
dataKey: 'video_description',
name: '视频描述',
type: 'String 字符串',
description: '视频的描述',
default: '无',
},
],
},
];
// Swaps two sibling records; returns undefined when the keys are not found, so the caller keeps the old tree
const swapSiblings = (list: TreeRow[], key1: number, key2: number) => {
const index1 = list.findIndex((item) => item.key === key1);
const index2 = list.findIndex((item) => item.key === key2);
if (index1 > -1 && index2 > -1) {
const next = [...list];
[next[index1], next[index2]] = [next[index2], next[index1]];
return next;
}
for (let i = 0; i < list.length; i++) {
const children = list[i].children;
const swapped = children && swapSiblings(children, key1, key2);
if (swapped) {
const next = [...list];
next[i] = { ...list[i], children: swapped };
return next;
}
}
return undefined;
};
const getSameLevelRecords = (key: number | undefined, list: TreeRow[]): TreeRow[] => {
if (key != null) {
if (list.some((item) => item.key === key)) {
return list;
}
for (const item of list) {
const records = item.children ? getSameLevelRecords(key, item.children) : [];
if (records.length) {
return records;
}
}
}
return [];
};
export default function TableDemo() {
const [expandedRowKeys, setExpandedRowKeys] = createSignal<number[]>([1, 2]);
const [data, setData] = createSignal<TreeRow[]>(raw);
const switchRecord = (key1: number, key2: number) => {
const swapped = swapSiblings(data(), key1, key2);
if (swapped) {
setData(swapped);
}
};
const siblingKey = (key: number | undefined, offset: number) => {
const records = getSameLevelRecords(key, data());
const index = records.findIndex((item) => item.key === key);
return records[index + offset]?.key;
};
const columns: ColumnProps<TreeRow>[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
{
key: 'operation',
render: (record) => {
// Read through functions so the buttons follow the tree after a swap
const upKey = () => siblingKey(record.key, -1);
const downKey = () => siblingKey(record.key, 1);
return (
<>
<Button
aria-label="上移"
icon={<IconArrowUp />}
disabled={upKey() === undefined}
onClick={() => {
const key = upKey();
if (key !== undefined && record.key !== undefined) switchRecord(record.key, key);
}}
/>
<Button
aria-label="下移"
icon={<IconArrowDown />}
disabled={downKey() === undefined}
onClick={() => {
const key = downKey();
if (key !== undefined && record.key !== undefined) switchRecord(record.key, key);
}}
/>
</>
);
},
},
];
return <Table columns={columns} dataSource={data()} rowKey="key" childrenRecordName="children" expandedRowKeys={expandedRowKeys()} onExpandedRowsChange={(rows = []) => setExpandedRowKeys(rows.flatMap((item) => ('key' in item && item.key !== undefined ? [item.key] : [])))} />;
}Tree Selection
By default, the row selection of the table is independent. You can simulate a tree selection by defining selectedRowKeys.
| Key | 名称 | 数据类型 | 描述 | 默认值 | |
|---|---|---|---|---|---|
| videos_info | 视频信息 | Object 对象 | 视频的元信息 | 无 | |
| text_info | 文本信息 | Object 对象 | 视频的元信息 | 无 |
Code
import { createSignal } from 'solid-js';
import { union, pull } from 'es-toolkit';
import { Table, type ColumnProps } from '@fibrous/semi-ui-solid';
type TreeRow = {
key: number;
dataKey: string;
name: string;
type: string;
description: string;
default: string;
children?: TreeRow[];
};
type RowKey = string | number;
const traverse = (data: TreeRow[], res: RowKey[]) => {
for (const record of data) {
res.push(record.key);
if (Array.isArray(record.children)) {
traverse(record.children, res);
}
}
};
const getAllRowKeys = (data: TreeRow[]) => {
const allRowKeys: RowKey[] = [];
traverse(data, allRowKeys);
return allRowKeys;
};
const findShouldSelectRowKeys = (record: TreeRow, selected: boolean, selectedRowKeys: RowKey[]) => {
const childrenRowKeys: RowKey[] = [];
if (Array.isArray(record.children)) {
traverse(record.children, childrenRowKeys);
}
if (!selected) {
return pull([...selectedRowKeys], [record.key, ...childrenRowKeys]);
}
return union(selectedRowKeys, [record.key, ...childrenRowKeys]);
};
const columns: ColumnProps<TreeRow>[] = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 400,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '默认值',
dataIndex: 'default',
key: 'default',
width: 100,
},
];
const data: TreeRow[] = [
{
key: 1,
dataKey: 'videos_info',
name: '视频信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 11,
dataKey: 'status',
name: '视频状态',
type: 'Enum <Integer> 枚举',
description: '视频的可见、推荐状态',
default: '1',
},
{
key: 12,
dataKey: 'vid',
name: '视频 ID',
type: 'String 字符串',
description: '标识视频的唯一 ID',
default: '无',
children: [
{
key: 121,
dataKey: 'video_url',
name: '视频地址',
type: 'String 字符串',
description: '视频的唯一链接',
default: '无',
},
],
},
],
},
{
key: 2,
dataKey: 'text_info',
name: '文本信息',
type: 'Object 对象',
description: '视频的元信息',
default: '无',
children: [
{
key: 21,
dataKey: 'title',
name: '视频标题',
type: 'String 字符串',
description: '视频的标题',
default: '无',
},
{
key: 22,
dataKey: 'video_description',
name: '视频描述',
type: 'String 字符串',
description: '视频的描述',
default: '无',
},
],
},
];
export default function TableDemo() {
const [selectedRowKeys, setSelectedRowKeys] = createSignal<RowKey[]>([]);
const doSelect = (record: TreeRow, selected: boolean) => {
const rowKeys = findShouldSelectRowKeys(record, selected, selectedRowKeys());
setSelectedRowKeys(rowKeys);
console.log('select', record, rowKeys);
};
const doSelectAll = (selected: boolean) => {
setSelectedRowKeys(selected ? getAllRowKeys(data) : []);
};
return (
<Table
columns={columns}
rowKey="key"
childrenRecordName="children"
rowSelection={{
selectedRowKeys: selectedRowKeys(),
onSelect: doSelect,
onSelectAll: doSelectAll,
}}
dataSource={data}
pagination={false}
/>
);
}Tree Selection Association (checkRelation)
By setting rowSelection.checkRelation to 'related', you can enable parent-child node selection association. Selecting a parent node automatically selects all child nodes, and selecting a child node affects the parent node's state (checked/half-checked/unchecked).
| Key | 名称 | 数据类型 | 描述 | |
|---|---|---|---|---|
| videos_info | 视频信息 | Object | 视频的元信息 | |
| text_info | 文本信息 | Object | 文本的元信息 |
Code
import { createSignal } from 'solid-js';
import { Table, type RowSelectionProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
dataKey: string;
name: string;
type: string;
description: string;
children?: Row[];
}
const columns = [
{
title: 'Key',
dataIndex: 'dataKey',
key: 'dataKey',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '数据类型',
dataIndex: 'type',
key: 'type',
width: 200,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
];
const data: Row[] = [
{
key: '1',
dataKey: 'videos_info',
name: '视频信息',
type: 'Object',
description: '视频的元信息',
children: [
{
key: '1-1',
dataKey: 'status',
name: '视频状态',
type: 'Enum',
description: '视频的可见状态',
},
{
key: '1-2',
dataKey: 'vid',
name: '视频 ID',
type: 'String',
description: '标识视频的唯一 ID',
children: [
{
key: '1-2-1',
dataKey: 'video_url',
name: '视频地址',
type: 'String',
description: '视频的唯一链接',
},
],
},
],
},
{
key: '2',
dataKey: 'text_info',
name: '文本信息',
type: 'Object',
description: '文本的元信息',
children: [
{
key: '2-1',
dataKey: 'title',
name: '标题',
type: 'String',
description: '文本标题',
},
{
key: '2-2',
dataKey: 'description',
name: '描述',
type: 'String',
description: '文本描述',
},
],
},
];
export default function TableDemo() {
const [selectedRowKeys, setSelectedRowKeys] = createSignal<(string | number)[]>([]);
const rowSelection = (): RowSelectionProps<Row> => ({
selectedRowKeys: selectedRowKeys(),
onChange: (keys = []) => {
setSelectedRowKeys(keys);
},
checkRelation: 'related',
});
return <Table columns={columns} dataSource={data} rowSelection={rowSelection()} pagination={false} />;
}Custom Row or Cell Events and Properties
- The incoming
onRow/onHeaderRowcan define the native event or property of a table or table header line. - The incoming
Column.onCell/Column.onHeaderCellcan define a table or header cell native event or property.
In principle, any attribute or event supported on tr/td/th can be defined. Returned objects use JSX attribute names: class, style, data-*, and events such as onClick, onMouseEnter, onMouseLeave, onDblClick and onContextMenu. For example:
- The tr of the header defines
onMouseEnter/onMouseLeave - The tr of the table defines
class - The third row of the table defines
onClick
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 郝郝宣 | 2024-05-11 | |
| 10 KB | 姜姜鹏志 | 2024-05-16 | |
| 15 KB | 郝郝宣 | 2024-05-21 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 | |
| 25 KB | 郝郝宣 | 2024-05-31 | |
| 30 KB | 姜姜鹏志 | 2024-06-05 | |
| 35 KB | 郝郝宣 | 2024-06-10 | |
| 40 KB | 姜姜鹏志 | 2024-06-15 | |
| 45 KB | 郝郝宣 | 2024-06-20 |
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnHeaderRow, type OnRow } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
const onRow: OnRow<Row> = (record, index) => {
if (index === 2) {
return {
class: 'my-tr-class',
onClick: () => console.log('mouse click: ', record, index),
};
}
return {
class: 'my-tr-class',
};
};
const onHeaderRow: OnHeaderRow<Row> = (headerColumns, index) => {
return {
onMouseEnter: () => console.log('mouse enter: ', headerColumns, index),
onMouseLeave: () => console.log('mouse leave: ', headerColumns, index),
};
};
export default function TableDemo() {
return <Table columns={columns} dataSource={data} onRow={onRow} onHeaderRow={onHeaderRow} />;
}Zebra Pattern Table
Use OnRow to set a background color for each row to create a zebra stripped table.
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 | ||
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps, type OnRow } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
{
key: '4',
name: 'Semi D2C 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '5',
name: 'Semi D2C 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '6',
name: 'Semi D2C 设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
const handleRow: OnRow<Row> = (_record, index) => {
if (index % 2 === 0) {
return {
style: {
background: 'var(--semi-color-fill-0)',
},
};
}
return {};
};
export default function TableDemo() {
return <Table columns={columns} dataSource={data} onRow={handleRow} pagination={false} />;
}Custom Header Style
You can return a specific style or class from Column.onHeaderCell to customize the header style.
The example below changes the header background color by passing in background-color.
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const onHeaderCell = () => ({
style: {
'background-color': 'var(--semi-color-fill-0)',
},
});
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 280,
onHeaderCell,
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
width: 100,
dataIndex: 'size',
onHeaderCell,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
onHeaderCell,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
width: 300,
dataIndex: 'updateTime',
onHeaderCell,
},
{
title: '',
dataIndex: 'operate',
onHeaderCell,
render: () => <IconMore />,
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={data} pagination={false} />;
}Custom Cell Hover Style
Table applies the hover style to the whole row by default. To change it, override the styles with CSS.
In the example below, CSS overrides change the hover background from row highlight to cell highlight.
.component-table-demo-cell-hover-custom {
.semi-table-tbody {
// Remove the original row hover style
.semi-table-row:hover {
.semi-table-row-cell {
background-color: transparent;
background-image: none;
&.semi-table-cell-fixed-left::before {
background-color: transparent;
}
}
// Add a style to a single cell
.semi-table-row-cell:hover {
background-color: var(--semi-light-green-1);
}
}
}
}
| 标题 | 大小 | 所有者 | 更新日期 | |
|---|---|---|---|---|
| 2M | 姜姜鹏志 | 2020-02-02 05:13 | ||
| 2M | 郝郝宣 | 2020-01-17 05:31 | ||
| 34KB | ZZoey Edwards | 2020-01-26 11:01 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
render: (text, record) => {
return (
<div>
<Avatar size="small" shape="square" src={record.nameIconSrc} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
},
{
title: '大小',
width: 100,
dataIndex: 'size',
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
width: 300,
dataIndex: 'updateTime',
},
{
title: '',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi Design 设计稿.fig',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: '姜鹏志',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: 'Semi Design 分享演示文稿',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣',
updateTime: '2020-01-17 05:31',
avatarBg: 'red',
},
{
key: '3',
name: '设计文档',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: 'Zoey Edwards',
updateTime: '2020-01-26 11:01',
avatarBg: 'light-blue',
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={data} class="component-table-demo-cell-hover-custom" pagination={false} />;
}Column Ellipsis
Use ellipsis to make cells automatically clipped.
| 所有者 | 大小 | 更新日期 | ||
|---|---|---|---|---|
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 2M | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 2M | 2020-02-02 05:13 | |
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 34KB | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 34KB | 2020-02-02 05:13 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const filters = [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
];
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
filters,
onFilter: (value, record) => record.name.includes(value),
sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
ellipsis: true,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
ellipsis: true,
},
{
title: '大小',
dataIndex: 'size',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
ellipsis: true,
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
render: () => <IconMore />,
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
updateTime: '2020-02-02 05:13',
avatarBg: 'red',
},
{
key: '3',
nameIconSrc: '/assets/docs-icon.png',
name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
size: '34KB',
owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
updateTime: '2020-02-02 05:13',
avatarBg: 'light-blue',
},
{
key: '4',
name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
updateTime: '2020-02-02 05:13',
avatarBg: 'light-blue',
},
];
export default function TableDemo() {
return <Table scroll={{ x: 1200 }} columns={columns} dataSource={data} pagination={false} />;
}Set ellipsis.showTitle to false to hide the default native HTML title. Use render together with it to customize the content tooltip.
| 所有者 | 大小 | 更新日期 | ||
|---|---|---|---|---|
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang | 2M | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣 | 2M | 2020-02-02 05:13 | |
| Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks | Pengzhi Jiang | 34KB | 2020-02-02 05:13 | |
| 由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。 | 郝宣 | 34KB | 2020-02-02 05:13 |
Code
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Typography, type ColumnProps } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
nameIconSrc: string;
size: string;
owner: string;
updateTime: string;
avatarBg: 'grey' | 'red' | 'light-blue';
}
const filters = [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
];
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
fixed: true,
width: 250,
filters,
onFilter: (value, record) => record.name.includes(value),
sorter: (a, b) => (a.name.length - b.name.length > 0 ? 1 : -1),
ellipsis: { showTitle: false },
render: (text) => <Typography.Text ellipsis={{ showTooltip: true }}>{text}</Typography.Text>,
},
{
title: '所有者',
dataIndex: 'owner',
width: 200,
ellipsis: { showTitle: false },
render: (text) => <Typography.Text ellipsis={{ showTooltip: true }}>{text}</Typography.Text>,
},
{
title: '大小',
dataIndex: 'size',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updateTime',
width: 200,
ellipsis: true,
},
{
title: '',
dataIndex: 'operate',
fixed: 'right',
align: 'center',
width: 100,
render: () => <IconMore />,
},
];
const data: Row[] = [
{
key: '1',
name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
nameIconSrc: '/assets/placeholder/app.svg',
size: '2M',
owner: 'Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang Pengzhi Jiang',
updateTime: '2020-02-02 05:13',
avatarBg: 'grey',
},
{
key: '2',
name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
nameIconSrc: '/assets/docs-icon.png',
size: '2M',
owner: '郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣郝宣',
updateTime: '2020-02-02 05:13',
avatarBg: 'red',
},
{
key: '3',
nameIconSrc: '/assets/docs-icon.png',
name: 'Semi is designed based on FA architecture, and the main logic is extracted as Foundation package, which is easy to migrate to other frameworks',
size: '34KB',
owner: 'Pengzhi Jiang',
updateTime: '2020-02-02 05:13',
avatarBg: 'light-blue',
},
{
key: '4',
name: '由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。由抖音前端与 UED 团队维护,易于定制的现代化设计系统,帮助设计师与开发者打造高质量产品。',
nameIconSrc: '/assets/docs-icon.png',
size: '34KB',
owner: '郝宣',
updateTime: '2020-02-02 05:13',
avatarBg: 'light-blue',
},
];
export default function TableDemo() {
return <Table scroll={{ x: 1200 }} columns={columns} dataSource={data} pagination={false} />;
}Resizable Column
Basic Resizable Column
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:
resizableis set totrueor anobject- Any column in
columnsthat requires a telescopic function should specify thewidthfield (if not passed, the column does not have a telescopic function and its column width will be automatically adjusted by the browser) column.resizecan take effect after resizable is enabled. After setting to false, the column no longer supports scaling.- A column that has been dragged keeps its dragged width, while columns that have not been dragged follow the
widthincolumns
When used with fixed columns, you need to specify a column without setting the width
It is not recommended to use it with
scroll.xat the same time. scroll.x specifies that the table has a width range, and stretching columns will expand the column width, which may cause the table to be misaligned
标题 | 所有者 | 操作列 | ||
|---|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | ||
| 5 KB | 郝郝宣 | 2024-05-11 | ||
| 10 KB | 姜姜鹏志 | 2024-05-16 | ||
| 15 KB | 郝郝宣 | 2024-05-21 | ||
| 20 KB | 姜姜鹏志 | 2024-05-26 | ||
| 25 KB | 郝郝宣 | 2024-05-31 | ||
| 30 KB | 姜姜鹏志 | 2024-06-05 | ||
| 35 KB | 郝郝宣 | 2024-06-10 | ||
| 40 KB | 姜姜鹏志 | 2024-06-15 | ||
| 45 KB | 郝郝宣 | 2024-06-20 |
Code
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 300,
resize: false,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 200,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
width: 200,
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
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',
width: 100,
resize: false,
render: () => <IconMore />,
},
];
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} resizable bordered />;
}Advanced Telescopic Columns
resizable can also be an object, including three event methods:
- onResize
- onResizeStart
- onResizeStop
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.
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 郝郝宣 | 2024-05-11 | |
| 10 KB | 姜姜鹏志 | 2024-05-16 | |
| 15 KB | 郝郝宣 | 2024-05-21 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 |
Code
import * as dateFns from 'date-fns';
import { addClass, removeClass } from '@fibrous/semi-foundation/utils/classnames';
import { Table, Avatar, type ColumnProps, type TableResizableProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const pagination = { pageSize: 5 };
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 200,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
width: 200,
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
const resizable: TableResizableProps<Row> = {
onResizeStart: (curColumn) => {
return { class: addClass(curColumn.class, 'my-resizing') };
},
onResizeStop: (curColumn) => {
return { class: removeClass(curColumn.class, 'my-resizing') };
},
};
export default function TableDemo() {
return (
<div id="components-table-demo-resizable-column">
<Table columns={columns} dataSource={dataSource} resizable={resizable} pagination={pagination} bordered />
</div>
);
}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 .semi-table-resize-handle:hover {
background-color: red;
}
#components-table-demo-resizable-column .my-resizing:hover .semi-table-resize-handle {
background-color: inherit;
}
Drag Sorting
Use dnd-kit with @dnd-kit/solid and the components property to implement drag sorting easily: components.body.row takes a Solid component that receives the properties of each data row (including data-row-key) and the index returned by onRow. Call useSortable inside it. Wrap the Table with DragDropProvider, and in onDragEnd adjust dataSource according to source.initialIndex and source.index.
标题 | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 郝郝宣 | 2024-05-06 | |
| 10 KB | 姜姜鹏志 | 2024-05-06 | |
| 15 KB | 郝郝宣 | 2024-05-06 | |
| 20 KB | 姜姜鹏志 | 2024-05-06 | |
| 25 KB | 郝郝宣 | 2024-05-06 | |
| 30 KB | 姜姜鹏志 | 2024-05-06 | |
| 35 KB | 郝郝宣 | 2024-05-06 | |
| 40 KB | 姜姜鹏志 | 2024-05-06 | |
| 45 KB | 郝郝宣 | 2024-05-06 |
Code
import { createMemo, createSignal, splitProps, type JSX } from 'solid-js';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { Accessibility } from '@dnd-kit/dom';
import { arrayMove } from '@dnd-kit/helpers';
import { DragDropProvider } from '@dnd-kit/solid';
import { isSortable, useSortable } from '@dnd-kit/solid/sortable';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type OnRowReturnObject, type TableProps } from '@fibrous/semi-ui-solid';
interface DemoRow {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = () => {
const data: DemoRow[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
interface SortableRowProps extends JSX.HTMLAttributes<HTMLTableRowElement> {
index: number;
'data-row-key': string;
}
// index comes from the table's onRow; it must not reach the tr
function SortableRow(rawProps: SortableRowProps) {
const [local, rest] = splitProps(rawProps, ['index']);
const { ref, isDragging } = useSortable({
get id() {
return rawProps['data-row-key'];
},
get index() {
return local.index;
},
});
return <tr {...rest} classList={{ isDragging: isDragging() }} ref={(node) => ref(node)} style={{ cursor: 'grabbing', ...(isDragging() ? { 'z-index': 999, position: 'relative' } : {}) }} />;
}
export default function TableDemo() {
const pageSize = 10;
const [dataSource, setData] = createSignal(getData());
const [pageNum, setPageNum] = createSignal(1);
const pageData = createMemo(() => dataSource().slice((pageNum() - 1) * pageSize, pageNum() * pageSize));
const columns: ColumnProps<DemoRow>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src="/assets/placeholder/app.svg" alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
// Sortable indexes are relative to the current page
const handleDragEnd: Parameters<typeof DragDropProvider>[0]['onDragEnd'] = (event) => {
const { source } = event.operation;
if (event.canceled || !isSortable(source) || source.initialIndex === source.index) {
return;
}
const offset = (pageNum() - 1) * pageSize;
setData((prev) => arrayMove(prev, offset + source.initialIndex, offset + source.index));
};
const handleChange: TableProps['onChange'] = ({ pagination }) => {
if (pagination?.currentPage) {
setPageNum(pagination.currentPage);
}
};
return (
<DragDropProvider
// Accessibility would add role="button" and a Tab stop to each row; keep table row semantics
plugins={(defaults) => defaults.filter((plugin) => plugin !== Accessibility)}
modifiers={[RestrictToVerticalAxis]}
onDragEnd={handleDragEnd}
>
<Table
components={{
body: {
row: SortableRow,
},
}}
rowKey="key"
columns={columns}
dataSource={pageData()}
onRow={(_record, index) => {
const rowProps: OnRowReturnObject & { index: number } = { index };
return rowProps;
}}
pagination={{ currentPage: pageNum(), pageSize: pageSize, total: dataSource().length }}
onChange={handleChange}
/>
</DragDropProvider>
);
}Table Grouping
For tables whose data needs to be displayed in groups, pass groupBy to define the grouping rule and use renderGroupSection(groupKey) to define how the group header is rendered.
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.
标题 | 所有者 |
|---|
| 100 KB | 姜姜鹏志 | 2024-08-14 | |
| 100 KB | 郝郝宣 | 2024-08-14 | |
| 100 KB | 姜姜鹏志 | 2024-08-14 | |
| 112 KB | 郝郝宣 | 2024-08-26 | |
| 112 KB | 姜姜鹏志 | 2024-08-26 | |
| 112 KB | 郝郝宣 | 2024-08-26 | |
| 105 KB | 姜姜鹏志 | 2024-08-19 | |
| 105 KB | 郝郝宣 | 2024-08-19 | |
| 105 KB | 姜姜鹏志 | 2024-08-19 | |
| 117 KB | 郝郝宣 | 2024-08-31 |
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const rowKey = (record: Row) => `${record.owner.toLowerCase()}-${record.name.toLowerCase()}`;
const onGroupedRow = (group: { groupKey: string | number; records: Row[] }, index: number) => {
return {
onClick: () => {
console.log(`Grouped row clicked: `, group, index);
},
};
};
export default function TableDemo() {
return (
<div style={{ padding: '20px 0px' }}>
<Table
dataSource={dataSource}
rowKey={rowKey}
groupBy="size"
columns={columns}
renderGroupSection={(groupKey) => <strong>根据文件大小分组 {groupKey} KB</strong>}
onGroupedRow={onGroupedRow}
clickGroupedRowToExpand // 点击整行即可展开
scroll={{ y: 480 }}
/>
</div>
);
}Virtualized Table
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:
- Need to pass the height of each line
virtualized.itemSize(when not passed, the default row height followssize:53fordefault,45formiddleand37forsmall, and a group header row is as high as a normal row), can be of the following types:number(index, { sectionRow?: boolean, expandedRow?: boolean }) => number
- Table grouping virtualization is supported.
- The
virtualizedobject also takesoverscanCount: the number of rows rendered above and below the visible area, 2 by default
- Must pass
scroll.y(number); the table width comes fromstyle.width, or fromscroll.xwhen it is not set.stylelands on the root element, so writewidthas a string with a unit, such as'750px';
Use getVirtualizedListRef to obtain the instance of the virtual list. The instance provides scrollTo(offset) and scrollToItem(index, align), where align is auto (default), smart, center, start, or end.
The following is an example of rendering 1000 pieces of data.
标题 | 所有者 |
|---|
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, Button, type ColumnProps, type TableProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
type VirtualizedList = Parameters<NonNullable<TableProps['getVirtualizedListRef']>>[0];
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 200,
fixed: true,
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 150,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData();
const scroll = { y: 400, x: 900 };
const style = { width: '750px', margin: '0 auto' };
export default function TableDemo() {
let virtualizedList: VirtualizedList | undefined;
return (
<>
<Button onClick={() => virtualizedList?.scrollToItem(100)}>Scroll to 100</Button>
<Table
pagination={false}
columns={columns}
dataSource={dataSource}
scroll={scroll}
style={style}
virtualized
getVirtualizedListRef={(list) => {
virtualizedList = list;
}}
/>
</>
);
}Infinite Scroll
Based on the virtualization feature, you can implement infinite scroll loading by passing virtualized.onScroll.
标题 | 所有者 |
|---|
Code
import { createSignal } from 'solid-js';
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type TableVirtualizedProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 200,
fixed: true,
render: (text) => {
return <div>{text}</div>;
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 150,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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 pageSize = 20; // 每次加载 20 条
const getPage = (start: number) => {
const page: Row[] = [];
for (let i = start; i < start + pageSize; i++) {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
page.push({
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return page;
};
const scroll = { y: 600, x: 1000 };
const style = { width: '750px', margin: '0 auto' };
const itemSize = 56;
export default function TableDemo() {
const [data, setData] = createSignal<Row[]>(getPage(0));
const loadMore = () => {
setData([...data(), ...getPage(data().length)]);
};
const virtualized: TableVirtualizedProps = {
itemSize,
onScroll: ({ scrollDirection, scrollOffset = 0, scrollUpdateWasRequested }) => {
if (scrollDirection === 'forward' && scrollOffset >= (data().length - Math.ceil(scroll.y / itemSize) * 1.5) * itemSize && !scrollUpdateWasRequested) {
loadMore();
}
},
};
return <Table pagination={false} columns={columns} dataSource={data()} scroll={scroll} style={style} virtualized={virtualized} />;
}Controlled Dynamic Tables
标题 | 所有者 |
|---|
Code
import { createSignal, children as resolveChildren, type JSX } from 'solid-js';
import * as dateFns from 'date-fns';
import { Table, Switch, ButtonGroup, Button, Avatar, type ColumnProps, type RowSelectionProps, type TableProps, type TablePaginationPosition as PaginationPosition } from '@fibrous/semi-ui-solid';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
description?: string;
}
interface PageState {
currentPage: number;
pageSize: number;
total: number;
}
interface FetchResult {
dataSource: Row[];
pagination: PageState;
sorter: ColumnProps<Row>;
filters: ColumnProps<Row>[];
}
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
const dataTotalSize = 46;
const wrapStyle = {
'margin-bottom': '15px',
display: 'flex',
'justify-content': 'space-around',
'flex-wrap': 'wrap',
} as const;
const initialColumns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
render: (text) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</span>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
];
const allData: Row[] = Array.from({ length: dataTotalSize }, (_v, i): Row => {
const isSemiDesign = i % 2 === 0;
const randomNumber = (i * 1000) % 199;
return {
key: '' + i,
name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
description: `这是第 ${i} 份设计稿的详细说明。`,
};
});
const defaultPagination: PageState = {
currentPage: 1,
pageSize: 8,
total: allData.length,
};
const renderExpandedRow = (record: Row) => {
return {
children: <p>{record.description}</p>,
fixed: 'left' as const,
};
};
const renderFooter = () => <p style={{ margin: '0px' }}>This is footer.</p>;
const mergeColumns = (column: ColumnProps<Row>, columns: ColumnProps<Row>[], keys: (keyof ColumnProps<Row>)[] = ['dataIndex']) => {
const nextColumns = [...columns];
nextColumns.forEach((curColumn, index) => {
let isTarget = !!(keys && keys.length);
for (const key of keys) {
if (column[key] !== curColumn[key]) {
isTarget = false;
break;
}
}
if (isTarget) {
nextColumns[index] = { ...curColumn, ...column };
}
});
return nextColumns;
};
const filterData = (filters: ColumnProps<Row>[], dataSource: Row[]) => {
let result = [...dataSource];
filters.forEach((filter) => {
const filteredValue = filter.filteredValue;
const dataIndex = filter.dataIndex as keyof Row | undefined;
if (Array.isArray(filteredValue) && filteredValue.length && dataIndex) {
result = result.filter((data) => filteredValue.filter((value) => String(data[dataIndex]).indexOf(value) > -1).length);
}
});
return result;
};
const sortData = (sortObj: ColumnProps<Row>, dataSource: Row[]) => {
const { sortOrder } = sortObj;
const dataIndex = (sortObj.dataIndex ?? '') as keyof Row;
let sorter = sortObj.sorter;
let result = dataSource;
if (sorter && sortOrder && typeof sorter !== 'function') {
sorter = (a, b) => ((a?.[dataIndex] ?? '') > (b?.[dataIndex] ?? '') ? 1 : -1);
}
if (typeof sorter === 'function') {
result = [...result].sort(sorter);
if (sortOrder === 'descend') {
result = result.reverse();
}
}
return result;
};
const fetchData = (pagination: PageState, currentPage = 1, sorter: ColumnProps<Row> = {}, filters: ColumnProps<Row>[] = []) => {
const nextPagination = { ...pagination, currentPage };
return new Promise<FetchResult>((resolve) => {
setTimeout(() => {
let data = sortData(sorter, allData);
data = filterData(filters, data);
const dataSource = data.slice((currentPage - 1) * nextPagination.pageSize, currentPage * nextPagination.pageSize);
nextPagination.total = data.length;
resolve({
dataSource,
pagination: nextPagination,
sorter,
filters,
});
}, 1500);
});
};
const getSorterColumn = (columns: ColumnProps<Row>[]) => columns.filter((column) => !!column.sorter)[0];
const getFilterColumns = (columns: ColumnProps<Row>[]) => columns.filter((column) => Array.isArray(column.filteredValue) && column.filteredValue.length);
interface TableSwitchProps {
text: string;
children?: JSX.Element;
checked?: boolean | number | string;
onChange?: (checked: boolean) => void;
}
function TableSwitch(props: TableSwitchProps) {
// Resolve the children once: reading props.children again would build new nodes
const content = resolveChildren(() => props.children);
return (
<span style={{ display: 'inline-flex', 'align-items': 'center', margin: '5px' }}>
<span>{props.text}</span>
{content() ?? <Switch size="small" aria-label={props.text} checked={props.checked != null ? !!props.checked : undefined} onChange={props.onChange} />}
</span>
);
}
export default function TableDemo() {
const [loading, setLoading] = createSignal(true);
const [columns, setColumns] = createSignal<ColumnProps<Row>[]>(initialColumns);
const [scroll, setScroll] = createSignal<TableProps<Row>['scroll']>({});
const [rowSelection, setRowSelection] = createSignal<RowSelectionProps<Row>>();
const [expandable, setExpandable] = createSignal(false);
const [expandCellFixed, setExpandCellFixed] = createSignal(false);
const [showTitle, setShowTitle] = createSignal(false);
const [showFooter, setShowFooter] = createSignal(false);
const [expandedRowKeys, setExpandedRowKeys] = createSignal<string[]>([]);
const [showHeader, setShowHeader] = createSignal(true);
const [bordered, setBordered] = createSignal(false);
const [resizable, setResizable] = createSignal(false);
const [pagination, setPagination] = createSignal<PageState | false>(defaultPagination);
const [dataSource, setDataSource] = createSignal<Row[]>([]);
const load = (currentPagination: PageState, currentPage?: number, sorter?: ColumnProps<Row>, filters?: ColumnProps<Row>[]) => {
fetchData(currentPagination, currentPage, sorter, filters)
.then((result) => {
let nextColumns = mergeColumns(result.sorter, columns());
for (const filterObj of result.filters) {
nextColumns = mergeColumns(filterObj, nextColumns);
}
setColumns(nextColumns);
setLoading(false);
setPagination(result.pagination);
setDataSource(result.dataSource);
})
.catch((err) => {
console.error(err);
setLoading(false);
});
};
const setPage = (page?: number | null, sorter?: ColumnProps<Row> | null, filters?: ColumnProps<Row>[]) => {
if (loading()) {
return;
}
const current = pagination();
const currentPage = typeof page === 'number' ? page : (current && current.currentPage) || 1;
setLoading(true);
load(current || defaultPagination, currentPage, sorter || getSorterColumn(columns()), filters || getFilterColumns(columns()));
};
// Initial load: the first page with the default sorter
load(defaultPagination, 1, getSorterColumn(initialColumns), []);
const toggleFixHeader = (checked: boolean) => {
setScroll({ ...scroll(), y: checked ? 300 : undefined });
};
const toggleFixColumns = (checked: boolean) => {
const cols = columns();
const lastIndex = cols.length - 1;
if (checked) {
setColumns(
cols.map((column, index) => {
if (index === 0) {
return { ...column, fixed: true };
}
return index === lastIndex ? { ...column, fixed: 'right' } : column;
})
);
} else {
setColumns(cols.map((column) => ({ ...column, fixed: false })));
}
const selection = rowSelection();
if (selection) {
setRowSelection({ ...selection, fixed: checked });
}
setScroll({ ...scroll(), x: checked ? '150%' : undefined });
setExpandCellFixed(checked);
};
const toggleRowSelection = (checked: boolean) => {
setRowSelection(
checked
? {
width: 48,
fixed: true,
onChange: (selectedRowKeys, selectedRows) => console.log('Selection changed, selectedRowKeys: ', selectedRowKeys, 'selectedRows: ', selectedRows),
}
: undefined
);
};
const toggleShowSorter = (checked: boolean) => {
setColumns(
columns().map((column) => ({
...column,
sorter: checked ? initialColumns.find((item) => item.dataIndex === column.dataIndex)?.sorter : undefined,
}))
);
};
const toggleShowFilter = (checked: boolean) => {
if (checked) {
setColumns(
columns().map((column) =>
column.dataIndex === 'name'
? {
...column,
filters: [
{
text: '姓名中包含 1',
value: '1',
},
{
text: '姓名中包含 2',
value: '2',
},
{
text: '姓名中包含 3',
value: '3',
},
],
filteredValue: [],
}
: column
)
);
} else {
setColumns(columns().map((column) => ({ ...column, filters: undefined, filteredValue: undefined })));
setPage(null, null, []);
}
};
const onExpandedRowsChange: TableProps<Row>['onExpandedRowsChange'] = (rows) => {
console.log('Expanded rows changed to: ', rows);
// The demo has no grouped rows, so every entry is a data row
setExpandedRowKeys(rows.map((row) => (row as Row).key));
};
const toggleExpandedRowKeys = (checked: boolean) => {
if (checked) {
setExpandedRowKeys(
dataSource()
.filter((data) => data.key)
.map((data) => data.key)
);
setExpandable(true);
} else {
setExpandedRowKeys([]);
}
};
const toggleResizable = (checked: boolean) => {
setResizable(!!checked);
setBordered(!!checked);
};
const toggleDataSource = (checked: boolean) => {
if (checked) {
setDataSource([]);
} else {
setPage();
}
};
const switchPagination = (position: PaginationPosition | false) => {
setPagination(position ? { ...defaultPagination, ...pagination(), position } : false);
};
return (
<div>
<div style={wrapStyle}>
<TableSwitch text="固定表头:" checked={scroll()?.y} onChange={toggleFixHeader} />
<TableSwitch text="隐藏表头:" onChange={(checked) => setShowHeader(!checked)} />
<TableSwitch text="显示标题:" onChange={(checked) => setShowTitle(checked)} />
<TableSwitch text="显示底部:" onChange={(checked) => setShowFooter(checked)} />
<TableSwitch text="固定列:" onChange={toggleFixColumns} />
<TableSwitch text="显示选择列:" onChange={toggleRowSelection} />
<TableSwitch text="显示加载状态:" onChange={(checked) => setLoading(checked)} checked={loading()} />
<TableSwitch text="无数据:" onChange={toggleDataSource} checked={!dataSource() || !dataSource().length} />
<TableSwitch text="开启排序功能:" onChange={toggleShowSorter} checked={columns().some((column) => column.sorter)} />
<TableSwitch text="开启过滤功能:" onChange={toggleShowFilter} checked={columns().some((column) => column.filters)} />
<TableSwitch text="开启行展开功能:" onChange={(checked) => setExpandable(checked)} checked={expandable()} />
<TableSwitch text="展开当前所有行:" onChange={toggleExpandedRowKeys} />
<TableSwitch text="显示边框:" onChange={(checked) => setBordered(checked)} checked={bordered()} />
<TableSwitch text="开启列伸缩功能:" onChange={toggleResizable} />
<TableSwitch text="分页控件:">
<ButtonGroup>
<Button onClick={() => switchPagination('bottom')}>Bottom</Button>
<Button onClick={() => switchPagination('top')}>Top</Button>
<Button onClick={() => switchPagination('both')}>Both</Button>
<Button onClick={() => switchPagination(false)}>None</Button>
</ButtonGroup>
</TableSwitch>
</div>
<Table
onExpandedRowsChange={onExpandedRowsChange}
title={showTitle() ? 'This is title.' : undefined}
footer={showFooter() ? renderFooter : undefined}
showHeader={showHeader()}
bordered={bordered()}
onChange={(data) => {
console.log('Table changed: ', data);
setPage(data.pagination?.currentPage, data.sorter, data.filters);
}}
expandCellFixed={expandCellFixed()}
expandedRowRender={expandable() ? renderExpandedRow : undefined}
expandedRowKeys={expandedRowKeys()}
rowSelection={rowSelection()}
scroll={scroll()}
columns={columns()}
dataSource={dataSource()}
pagination={pagination() && { ...pagination(), onPageChange: setPage }}
loading={loading()}
resizable={resizable()}
/>
</div>
);
}Fully custom rendering
In general, Column.render is enough, but you can also pass Column.useFullRender=true to enable fully custom rendering. The checkbox, expand button, indentation and other nodes are then passed to Column.title and Column.render, where you can further define how the header and cell content are rendered.
The argument Column.title receives when it is a function is:
{
filter?: () => JSX.Element, // Render function of the filter button; missing when the column has no filter
sorter?: () => JSX.Element, // Render function of the sort button; missing when the column has no sorter
selection?: () => JSX.Element, // Render function of the selection button
}
The fourth argument of Column.render is an object with the following structure:
{
expandIcon?: () => JSX.Element, // Render function of the expand button
selection?: () => JSX.Element, // Render function of the selection button
indentText?: () => JSX.Element, // Render function of the indentation
}
These nodes are render functions: each call creates the node, so call one only where it is shown (for example {parts.sorter?.()}), and a node that is never called is never created. With useFullRender, the title function is called only once, and the fields of its argument are live: reading them in the returned JSX makes them follow the state.
The example below renders the checkbox and content into the same cell and header.
| Name | 所有者 | ||
|---|---|---|---|
| 0 KB | 姜姜鹏志 | 2024-05-06 | |
| 5 KB | 郝郝宣 | 2024-05-11 | |
| 10 KB | 姜姜鹏志 | 2024-05-16 | |
| 15 KB | 郝郝宣 | 2024-05-21 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 | |
| 25 KB | 郝郝宣 | 2024-05-31 | |
| 30 KB | 姜姜鹏志 | 2024-06-05 | |
| 35 KB | 郝郝宣 | 2024-06-10 | |
| 40 KB | 姜姜鹏志 | 2024-06-15 | |
| 45 KB | 郝郝宣 | 2024-06-20 | |
| 50 KB | 姜姜鹏志 | 2024-06-25 | |
| 55 KB | 郝郝宣 | 2024-06-30 |
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps, type RowSelectionProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
// Each part is a render function: call it where the button is inserted
title: (props) => (
<span style={{ display: 'inline-flex', 'align-items': 'center', 'padding-left': '20px' }}>
{props.selection?.()}
<span style={{ 'margin-left': '8px' }}>Name</span>
{props.sorter?.()}
{props.filter?.()}
</span>
),
dataIndex: 'name',
width: 400,
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
useFullRender: true,
// The fourth render parameter holds the expand button, selection button and text
render: (text, _record, _index, options) => {
return (
<span style={{ display: 'inline-flex', 'align-items': 'center', 'justify-content': 'center' }}>
{options.indentText?.()}
{options.expandIcon?.()}
{options.selection?.()}
<span style={{ 'margin-left': '8px' }}>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</span>
</span>
);
},
},
{
title: '大小',
dataIndex: 'size',
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
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: number) => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(46);
const pagination = {
pageSize: 12,
};
const rowSelection: RowSelectionProps<Row> = {
hidden: true,
fixed: 'left',
};
export default function TableDemo() {
return <Table pagination={pagination} rowSelection={rowSelection} columns={columns} dataSource={dataSource} onChange={(changeInfo) => console.log(changeInfo)} expandedRowRender={(record) => <article>{record.name}</article>} />;
}Header Merge
Users can merge table headers to group them. Header merging can be combined with fixed columns, virtualization, data grouping, column resizing and other features, and supports both JSX and configuration-based writing.
Merged Header Configuration Writing
| 基本信息 | 其他信息 | 更多 | |||
|---|---|---|---|---|---|
标题 | 所有者 | ||||
| 0 KB | 姜姜鹏志 | 2024-05-06 | |||
| 5 KB | 郝郝宣 | 2024-05-11 | |||
| 10 KB | 姜姜鹏志 | 2024-05-16 | |||
| 15 KB | 郝郝宣 | 2024-05-21 | |||
| 20 KB | 姜姜鹏志 | 2024-05-26 | |||
| 25 KB | 郝郝宣 | 2024-05-31 | |||
| 30 KB | 姜姜鹏志 | 2024-06-05 | |||
| 35 KB | 郝郝宣 | 2024-06-10 | |||
| 40 KB | 姜姜鹏志 | 2024-06-15 | |||
| 45 KB | 郝郝宣 | 2024-06-20 |
Code
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '基本信息',
fixed: 'left',
children: [
{
title: '标题',
dataIndex: 'name',
width: 300,
fixed: true,
render: (text) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</span>
);
},
filters: [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
],
onFilter: (value, record) => record.name.includes(value),
},
{
title: '大小',
dataIndex: 'size',
width: 100,
fixed: true,
sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
render: (text) => `${text} KB`,
},
],
},
{
title: '其他信息',
children: [
{
title: '所有者',
dataIndex: 'owner',
render: (text, record) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
},
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
render: (value) => {
return dateFns.format(new Date(value), 'yyyy-MM-dd');
},
},
],
},
{
title: '更多',
fixed: 'right',
width: 100,
align: 'center',
dataIndex: 'operate',
render: () => <IconMore />,
},
];
const getData = (total: number) => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(46);
export default function TableDemo() {
return <Table rowSelection={{ fixed: true }} expandedRowRender={(record) => <article>{record.name}</article>} dataSource={dataSource} scroll={{ x: 1000, y: 400 }} onChange={(changeInfo) => console.log(changeInfo)} columns={columns} />;
}Merged Header JSX Writing
| 基本信息 | 其他信息 | 更多 | |||
|---|---|---|---|---|---|
标题 | 所有者 | ||||
| 0 KB | 姜姜鹏志 | 2024-05-06 | |||
| 5 KB | 郝郝宣 | 2024-05-11 | |||
| 10 KB | 姜姜鹏志 | 2024-05-16 | |||
| 15 KB | 郝郝宣 | 2024-05-21 | |||
| 20 KB | 姜姜鹏志 | 2024-05-26 | |||
| 25 KB | 郝郝宣 | 2024-05-31 | |||
| 30 KB | 姜姜鹏志 | 2024-06-05 | |||
| 35 KB | 郝郝宣 | 2024-06-10 | |||
| 40 KB | 姜姜鹏志 | 2024-06-15 | |||
| 45 KB | 郝郝宣 | 2024-06-20 |
Code
import * as dateFns from 'date-fns';
import { IconMore } from '@fibrous/semi-icons-solid';
import { Table, Avatar } from '@fibrous/semi-ui-solid';
const DAY = 24 * 60 * 60 * 1000;
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const getData = (total: number) => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(46);
const nameFilters = [
{
text: 'Semi Design 设计稿',
value: 'Semi Design 设计稿',
},
{
text: 'Semi D2C 设计稿',
value: 'Semi D2C 设计稿',
},
];
const renderName = (text: string) => {
return (
<span>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</span>
);
};
const renderOwner = (text: string, record: Row) => {
return (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
);
};
export default function TableDemo() {
return (
<Table rowSelection={{ fixed: true }} expandedRowRender={(record) => <article>{record.name}</article>} dataSource={dataSource} scroll={{ x: 1000, y: 400 }} onChange={(changeInfo) => console.log(changeInfo)}>
<Table.Column title="基本信息" fixed="left">
<Table.Column title="标题" dataIndex="name" width={300} fixed render={renderName} filters={nameFilters} onFilter={(value, record) => (record as Row).name.includes(value)} />
<Table.Column title="大小" dataIndex="size" width={100} fixed render={(text) => `${text} KB`} sorter={(a, b) => ((a as Row).size - (b as Row).size > 0 ? 1 : -1)} />
</Table.Column>
<Table.Column title="其他信息">
<Table.Column title="所有者" dataIndex="owner" render={renderOwner} />
<Table.Column title="更新日期" dataIndex="updateTime" sorter={(a, b) => ((a as Row).updateTime - (b as Row).updateTime > 0 ? 1 : -1)} render={(value) => dateFns.format(new Date(value), 'yyyy-MM-dd')} />
</Table.Column>
<Table.Column title="更多" dataIndex="operate" fixed="right" width={100} align="center" render={() => <IconMore />} />
</Table>
);
}colSpan and rowSpan
- Besides merging through
children, a header can merge columns by settingcolumn.colSpan. - Tables support row/column merging.
column.onCellreturns the cell attributescolSpanorrowSpan; when set to 0, the cell is not rendered andrenderis not called for it.
type OnCell = (record: RecordType, rowIndex: number) => JSX.TdHTMLAttributes<HTMLElement>;
| 标题 | 大小 | 所有者 | |
|---|---|---|---|
| 5 KB | 郝郝宣 | 2024-05-11 | |
姜姜鹏志 | |||
| 15 KB | 郝郝宣 | 2024-05-21 | |
| 20 KB | 姜姜鹏志 | 2024-05-26 | |
Code
import * as dateFns from 'date-fns';
import { Table, Avatar, type ColumnProps } from '@fibrous/semi-ui-solid';
const figmaIconUrl = '/assets/placeholder/app.svg';
interface Row {
key: string;
name: string;
owner: string;
size: number;
updateTime: number;
avatarBg: 'grey' | 'red';
}
const columns: ColumnProps<Row>[] = [
{
title: '标题',
dataIndex: 'name',
width: 400,
onCell: (_record, index) => {
if (index === 0) {
return { colSpan: 4 };
}
if (index === 1) {
return { rowSpan: 2 };
}
if (index === 2) {
return { rowSpan: 0 };
}
return {};
},
render: (text) => (
<div>
<Avatar size="small" shape="square" src={figmaIconUrl} alt="" style={{ 'margin-right': '12px' }} />
{text}
</div>
),
},
{
title: '大小',
dataIndex: 'size',
onCell: (_record, index) => {
if (index === 0) {
return { colSpan: 0 };
}
if (index === 1) {
return { rowSpan: 2 };
}
if (index === 2) {
return { rowSpan: 0 };
}
return {};
},
render: (text) => `${text} KB`,
},
{
title: '所有者',
dataIndex: 'owner',
onCell: (_record, index) => (index === 0 ? { colSpan: 0 } : {}),
render: (text, record) => (
<div>
<Avatar size="small" color={record.avatarBg} style={{ 'margin-right': '4px' }}>
{typeof text === 'string' && text.slice(0, 1)}
</Avatar>
{text}
</div>
),
},
{
title: '更新日期',
dataIndex: 'updateTime',
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
onCell: (_record, index) => {
if (index === 0) {
return { colSpan: 0 };
}
if (index === 1) {
return { rowSpan: 2 };
}
if (index === 2) {
return { rowSpan: 0 };
}
return {};
},
render: (value) => dateFns.format(new Date(value), 'yyyy-MM-dd'),
},
];
const DAY = 24 * 60 * 60 * 1000;
const getData = (total: number) => {
const data: Row[] = [];
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 设计稿${i}.fig` : `Semi D2C 设计稿${i}.fig`,
owner: isSemiDesign ? '姜鹏志' : '郝宣',
size: randomNumber,
updateTime: new Date(2024, 4, 6, 12).valueOf() + randomNumber * DAY,
avatarBg: isSemiDesign ? 'grey' : 'red',
});
}
return data;
};
const dataSource = getData(5);
export default function TableDemo() {
return <Table columns={columns} dataSource={dataSource} pagination={false} />;
}API Reference
Table
Props
| Property | Description | Type | Default |
|---|---|---|---|
| bordered | Whether to show the outer border and the column borders. | boolean | false |
| children | Table.Column children, read when columns is not given. | JSX.Element | - |
| childrenRecordName | The field of a record that holds its child records in a tree table. | string | "children" |
| clickGroupedRowToExpand | Clicking a group row expands or collapses the group. | boolean | - |
| columns | The column configuration, see Column. When given, an empty array included, it takes precedence over the Table.Column children. | TypeColumnProps<RecordType>[] | - |
| components | Replaces the elements Table renders, such as table, body, row, td and th, with a tag name or a component; the component receives the attributes and events of the element it replaces. | TableComponents | - |
| dataSource | The data. Give every record a unique key, or name the primary key field with rowKey. | RecordType[] | [] |
| defaultExpandAllGroupRows | Whether all group rows start expanded. Has no effect on data loaded later. | boolean | false |
| defaultExpandAllRows | Whether all rows start expanded. Has no effect on data loaded later. | boolean | false |
| defaultExpandedRowKeys | The keys of the rows that start expanded. Has no effect on data loaded later. | (string | number)[] | [] |
| direction | The text direction. Defaults to the direction of ConfigProvider and can be set here per table. | Direction | - |
| empty | The content shown when there is no data. Defaults to the empty text of the locale. | JSX.Element | - |
| expandAllGroupRows | Whether all group rows are expanded. | boolean | false |
| expandAllRows | Whether all rows are expanded. | boolean | false |
| expandCellFixed | Whether the column of the expand button is fixed, with the same values as the fixed field of a column. | Fixed | false |
| expandedRowKeys | The keys of the expanded rows. When given, row expansion is controlled. | (string | number)[] | - |
| expandedRowRender | The extra row rendered under a record. Returns an element, or an object { children, fixed } that sets how the expanded row is fixed. Give every record a unique key, or name the primary key field with rowKey. | TypeExpandedRowRender<RecordType> | - |
| expandIcon | false hides the expand buttons. An element or a function replaces the icon; the function receives expanded and is called again when the expanded state changes. | ExpandIcon | - |
| expandRowByClick | Whether clicking a row expands an expandable row. | boolean | false |
| footer | The table footer: an element, or a function that receives the data of the current page and returns an element. | TypeFooter<RecordType> | - |
| getVirtualizedListRef | Called with the list handle, which provides scrollTo and scrollToItem, each time a virtualized table creates its list. Only effective with virtualized. | GetVirtualizedListRef | - |
| groupBy | The grouping rule: a record field name, or a function that returns a string or a number. | TypeGroupBy<RecordType> | - |
| headerStyle | Inline style of every header cell, fixed headers included. Property names use kebab-case. | JSX.CSSProperties | - |
| hideExpandedColumn | In an expandable table the expand button shares the first cell by default; set false to render it as its own column. | boolean | true |
| indentSize | The indent of each tree level, in pixels. | number | 20 |
| keepDOM | Whether collapsing a row keeps its DOM. | boolean | - |
| loading | Whether the table shows its loading state. | boolean | false |
| onChange | Called when pagination, sorting or filtering changes. changeInfo holds pagination, filters, sorter and extra. | TypeOnChange<RecordType> | - |
| onExpand | Called when the expand icon of a row is clicked, with whether the row is now expanded, the record and the mouse event. | TypeOnExpand<RecordType> | - |
| onExpandedRowsChange | Called when the expanded rows change, with the records of the expanded rows. | TypeOnExpandedRowsChange<RecordType> | - |
| onGroupedRow | Like onRow, for group rows: the returned object is merged into the tr of the group row. index is the group's ordinal, or its position among the virtual list's rows when virtualized is on. | TypeOnGroupedRow<RecordType> | - |
| onHeaderRow | Sets the attributes and events of a header row. The returned object is merged into the tr of the header row, without style; use headerStyle for it. | TypeOnHeaderRow<RecordType> | - |
| onRow | Sets the attributes and events of a data row; the third argument is the row state. The returned object is merged into the tr of the row, and its events run after the row's own handling and the onRowXxx events. | TypeOnRow<RecordType> | - |
| onRowContextMenu | Called when a data row receives the context menu, before the onContextMenu returned by onRow; both run. | Type(record: RecordType, e: MouseEvent) => void | - |
| onRowDoubleClick | Called when a data row is double-clicked, before the onDblClick returned by onRow; both run. | Type(record: RecordType, e: MouseEvent) => void | - |
| onRowMouseEnter | Called when the mouse enters a data row, before the onMouseEnter returned by onRow; both run. | Type(record: RecordType, e: MouseEvent) => void | - |
| onRowMouseLeave | Called when the mouse leaves a data row, before the onMouseLeave returned by onRow; both run. | Type(record: RecordType, e: MouseEvent) => void | - |
| pagination | The pagination configuration. Pass false to turn pagination off. | TablePagination | true |
| prefixCls | The prefix of the class names. | string | "semi-table" |
| ref | The root element, which carries the getCurrentPageData method below. | TypeRef<TableElement<RecordType>> | - |
| renderGroupSection | Renders the content of a group row. An element is the content of the single cell of the group row; an object { children, ...column } gives the content in children and the other fields as the configuration of that column. | RenderGroupSection | - |
| renderPagination | Renders a custom pager from the pagination configuration, and returns an element. | RenderPagination | - |
| resizable | Whether columns can be resized; a resizable column needs a width. An object configures onResize, onResizeStart, onResizeStop and handlerClassName. | TypeResizable<RecordType> | - |
| rowExpandable | Tells whether a row can be expanded. A row that returns false has no expand button. | TypeRowExpandable<RecordType> | - |
| rowKey | The row key: a record field name, or a function that returns a string or a number. | TypeRowKey<RecordType> | "key" |
| rowSelection | The row selection configuration. Pass true for the defaults, see rowSelection. | TypeRowSelection<RecordType> | - |
| rowSpanHover | Hovering a cell with rowSpan highlights every row the cell spans. | boolean | - |
| scroll | The scroll area: x is the width and y the height, see scroll. | Scroll | - |
| showHeader | Whether to show the header. | boolean | true |
| size | The table size, which sets the padding of the cells. | Size | "default" |
| sticky | Sticks the header to the top of the page. With an object, top is the distance from the top of the scroll container. | Sticky | - |
| title | The table title: an element, or a function that receives the data of the current page and returns an element. | TypeTitle<RecordType> | - |
| virtualized | Turns on virtualization, which needs scroll.y as a number. An object sets the row height itemSize, the scroll callback onScroll and the extra rows overscanCount. | Virtualized | - |
Methods
| Method | Description | Type |
|---|---|---|
| getCurrentPageData | Returns the data of the current page, deep cloned, as { dataSource, groups }. | Type() => Pick<BasePageData<RecordType>, 'dataSource' | 'groups'> |
Native attributes such as class, style, id and data-* land on the outermost element. aria-label, aria-labelledby and aria-describedby land on the element with role="table". The instance obtained through ref provides methods such as getCurrentPageData, see "Methods" below.
headerStyle Example
headerStyle will be applied to all header <th> elements, including fixed columns.
onRow/onHeaderRow Usage
Also in
column.onCellcolumn.onHeaderCellProperties or events supported by td / th can also be returned.
The third parameter rowStatus of onRow can get the current row's status information, including disabled and selected properties. This is useful when you need to execute different logic based on the row's selected or disabled state, for example, determining whether to allow selection when clicking a row.
Column
A column accepts only the properties below, and a misspelled property name is a type error. To carry custom fields on a column, extend the type with an intersection, such as ColumnProps<RecordType> & { tooltip: string }.
Props
| Property | Description | Type | Default |
|---|---|---|---|
| align | The alignment of the cell content. | Align | - |
| children | Nested Table.Column children, which group the header. | JSX.Element | - |
| class | The class name of the column, set on its col element. | string | - |
| colSpan | The colSpan of the header cell. | number | - |
| dataIndex | The field of a record that the column shows. A unique dataIndex is required when sorter or onFilter is used. | string | - |
| defaultFilteredValue | The default filter values, used as the initial filter state when filteredValue is not given. | any[] | - |
| defaultSortOrder | The default sort order, used as the initial sort state when sortOrder is not given. | SortOrder | - |
| ellipsis | Truncates text that overflows the cell. An object sets showTitle (the native title hint) and showTooltip (a Tooltip). | BaseEllipsis | - |
| filterChildrenRecord | Whether filtering also filters the child records of a tree table. | boolean | - |
| filterConfirmMode | The filter confirm mode: immediate filters when an option is chosen, confirm filters after the confirm button is clicked. | FilterConfirmMode | - |
| filterDropdown | Renders what replaces the filter trigger and its menu. | Type() => JSX.Element | - |
| filterDropdownProps | The props of the filter dropdown, the same as Dropdown except render and onVisibleChange. | FilterDropdownProps | - |
| filterDropdownVisible | Whether the filter dropdown is visible. | boolean | - |
| filteredValue | The controlled filter values. When given, the filter state is controlled by the caller. | any[] | - |
| filterIcon | The filter icon: a function that receives filtered and returns an element, called again when the filtered state changes. | FilterIcon | - |
| filterMultiple | Whether the filter menu allows multiple selection. | boolean | - |
| filters | The filter menu options of the header. Not required with renderFilterDropdown. An option's text is plain text; an option with markup goes to renderFilterDropdownItem. | Filter[] | - |
| fixed | Fixes the column to the left or the right; true means left. | Fixed | - |
| key | Identifies the column. Without it, the key is built from dataIndex or the position. | string | number | - |
| onCell | Sets the attributes and events of the data cells. It receives the record and the row index, and the returned object is merged into the td; with colSpan or rowSpan 0 the cell is not rendered and render is not called for it. | TypeOnCell<RecordType> | - |
| onFilter | The filter function. It receives the filter value and a record, and keeps the record when it returns true. | TypeOnFilter<RecordType> | - |
| onFilterDropdownVisibleChange | Called when the visibility of the filter dropdown changes. | OnFilterDropdownVisibleChange | - |
| onHeaderCell | Sets the attributes and events of the header cell. It receives the column, the column index and the row index, and the returned object is merged into the th. | TypeOnHeaderCell<RecordType> | - |
| render | Renders the cell content from (text, record, index, options) and returns an element; cell attributes and merging go to onCell. The fields of options are live, read them in the returned JSX; expandIcon, selection and indentText are render functions, called where they are shown. | TypeColumnRender<RecordType> | - |
| renderFilterDropdown | Renders the content of the filter dropdown. The argument holds the temporary filter values, their setter, and the confirm, clear and close methods; its fields are live, read them in the returned JSX. | Type(props: RenderFilterDropdownProps) => JSX.Element | - |
| renderFilterDropdownItem | Renders each item of the filter menu, with its text, its checked state and its onChange. | RenderFilterDropdownItem | - |
| resize | With resizable on the table, false leaves the column without a resize handle. | boolean | - |
| showSortTip | Whether to show the sort tip while sorting. | boolean | - |
| sortChildrenRecord | Whether sorting also sorts the child records of a tree table. | boolean | - |
| sorter | The sort function. It receives two records and the current sort order, and returns a negative number, zero or a positive number. dataIndex is required with it. | TypeSorter<RecordType> | - |
| sortIcon | The sort icon. It receives sortOrder and returns an element. sortOrder is live, read it in the returned JSX. | SortIcon | - |
| sortOrder | The controlled sort order. When given, the sort state is controlled by the caller. | SortOrder | - |
| title | The header content: text, or a function that receives the sorter, filter and selection render functions and returns an element, calling each where it is shown. With useFullRender, the fields of that argument are live. | ColumnTitle | - |
| useFullRender | Whether the column renders its header and cells in full, which lets title and render call the sorter, filter, selection and expandIcon render functions. | boolean | - |
| width | The column width; a number is in pixels. Required with resizable. | string | number | - |
Column is a child declaration of Table and renders nothing by itself. Table reads its props as the column configuration.
rowSelection
scroll
| Parameters | Instructions | Type | Default |
|---|---|---|---|
| scrollToFirstRowOnChange | Whether to automatically scroll to the top of the table after paging, sorting, and filtering changes. When scroll.y is set, it scrolls the table body to the top; when scroll.y is not set, it scrolls the page to the table header position | boolean | false |
| 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 |
pagination
Page-turning component configuration. Pagination suggests not to use literal value.
Note: After pagination.onChange is set, Table onChange no longer responds to pagination changes.
For other configurations, see Pagination
Resizable
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 |
|---|---|---|---|
| handlerClassName | The class name added to the column while it is being resized | string | 'resizing' |
| onResize | Triggers when the table column changes its width | (column: Column) => Column | void | |
| onResizeStart | Triggers when the table column starts to change the width. | (column: Column) => Column | void | |
| onResizeStop | Triggers when the table column stops changing the width | (column: Column) => Column | void |
Methods
Access the internal methods Table provides through ref:
| Name | Description |
|---|---|
| getCurrentPageData() | Returns the data object of the current page: { dataSource: RecordType[], groups: Map<string | number, Set<string | number>> }, where the keys of groups are group keys and the values are the sets of row keys in that group |
Accessibility
ARIA
Table follows the WAI-ARIA Table pattern: it is a static table, arrow keys do not move between cells, and every interactive part (sort, filter, expand, select) is a native control reachable with Tab.
- The table's role is table, rows are row, header cells are columnheader and cells are cell. A tree table is a table too and does not expose row levels
- The sort button is a native
buttonin the header cell, named by the column title. The sort direction isaria-sorton the header cell (ascendingordescending); unsorted columns do not carry it. TheshowSortTiptip shows when the button is focused - The expand button is a native
buttonnamed "Expand this row", witharia-expandedtelling whether the row is expanded - The row selection checkboxes have accessible names such as "Select this row" and "Select all rows"
- With a fixed header (
scroll.y,sticky) or virtualization, the header and the body render as two table elements; their common container has the table role and the inner tables have role none, so cells stay associated with their column headers - In a virtualized table
aria-rowcountis the header row count plus the total data row count, and data rows'aria-rowindexcontinues after the header rows, so screen readers announce rows that are not in the DOM
Known limitations:
- In a sortable column the
titlerenders inside the sort button, so do not put buttons, links or other interactive content in it
- The default column filter icon is a native
button. It can be focused with Tab, and Enter, Space or the down arrow opens the filter menu. It carriesaria-haspopup="menu"andaria-expanded. When you customizefilterIcon, the caller must make sure it is focusable.
RTL/LTR
- RTL default value of Table is controlled by ConfigProvider
- The align and fixed properties of the Table column will be automatically switched in RTL, left <-> right.
- Table tree data does not support RTL (Chrome and Safari browsers behave differently from Firefox)
Content Guidelines
- Table title
- The title of the table should clearly make the user perceive the purpose of the table;
- Add descriptions to complex tables to provide users with more contextual information about the table;
- use sentence case;
- Column headers
- Keep column headings concise, it is recommended to use 1-2 words as column headings;
- When the column header is long, it is recommended to display it in 2 lines, and the remaining text is abbreviated and displayed completely in the Tooltip;
- Adopt the capitalization rules of Sentence case;
- Use sentence case for column headings;
- Table operation area
- You can follow Button's content Guidelines
Design Tokens
| Sass variable | Value | Usage |
|---|---|---|
| $transition_duration-table_body-bg | var(--semi-transition_duration-none) | 表格-背景颜色-动画持续时间 |
| $transition_function-table_body-bg | var(--semi-transition_function-easeOut) | 表格-背景颜色-过渡曲线 |
| $transition_delay-table_body-bg | 0ms | 表格-背景颜色-延迟时间 |
| $transition_duration-table_row-head-bg | var(--semi-transition_duration-none) | 表格-行头-背景颜色-动画持续时间 |
| $transition_function-table_row-head-bg | linear | 表格-行头-背景颜色-过渡曲线 |
| $spacing-table-paddingY | $spacing-base | 表格空数据占位及虚拟列表分组垂直内边距 |
| $spacing-table-paddingX | $spacing-base-tight | 表格空数据占位及虚拟列表分组水平内边距 |
| $spacing-table_middle-paddingY | $spacing-base-tight | 中尺寸表格单元格垂直内边距 |
| $spacing-table_small-paddingY | $spacing-tight | 小尺寸表格单元格垂直内边距 |
| $spacing-table_title-paddingY | $spacing-base | 表格标题垂直内边距 |
| $spacing-table_title-paddingX | 0 | 表格标题水平内边距 |
| $spacing-table_footer-padding | $spacing-base | 表格 footer 内边距 |
| $spacing-table_expand_row-paddingLeft | 16px | 表格展开行左侧内边距 |
| $spacing-table_expand_row-paddingRight | 16px | 表格展开行右侧内边距 |
| $spacing-table_resizable-offset-y | 4px | 可拖动拉伸操作垂直偏移量 |
| $spacing-table_resizable-bottom | 4px | 可拖动拉伸操作底部距离 |
| $spacing-table_row_head-paddingY | $spacing-base-tight | 表头垂直内边距 |
| $spacing-table_row_head-paddingX | $spacing-base | 表头水平内边距 |
| $spacing-table_react_resizable_handle-right | -1px | 可拖动拉伸操作向右偏移量 |
| $spacing-table_tbody_rowCell-padding | $spacing-base | 表格体单元格内边距(默认尺寸) |
| $spacing-table_tbody_rowSelection_rowCell_notSelection-paddingX | 16px | 表格选择状态行(非复选框列)单元格水平内边距 |
| $spacing-table_tbody_rowSelection_rowCell_notSelection-paddingY | 10px | 表格选择状态行(非复选框列)垂直内边距 |
| $spacing-table_column_sorter-marginLeft | $spacing-extra-tight | 表头标题与排序按钮间距 |
| $spacing-table_column_filter-marginLeft | $spacing-extra-tight | 列过滤器按钮左侧外边距 |
| $spacing-table_bordered_titler-paddingLeft | $spacing-base | 带边框表格标题左侧内边距 |
| $spacing-table_bordered_titler-paddingRight | $spacing-base | 带边框表格标题右侧内边距 |
| $spacing-table_expand_icon-marginRight | 8px | 行展开按钮右侧外边距 |
| $spacing-table_panel_operation-paddingX | $spacing-base | 表格操作栏水平内边距 |
| $spacing-table_panel_operation-paddingY | $spacing-tight | 表格操作栏垂直内边距 |
| $width-table_base_border | 1px | 表格单元格分割线宽度 |
| $width-table_header_border | 1px | 表格表头分割线宽度 |
| $width-table_resizer_border | 2px | 表格拉伸列标示线宽度 |
| $width-table_column_selection | 48px | 表格选择列及展开列宽度 |
| $width-table_column_sorter-icon | 16px | 表格排序按钮宽度 |
| $height-table_column_sorter-icon | 16px | 表格排序按钮高度 |
| $width-table_column_filter-icon | 16px | 表格过滤按钮宽度 |
| $height-table_column_filter-icon | 16px | 表格过滤按钮高度 |
| $width-table_cell_fixed_left_last | 1px | 表格左侧固定列右侧分割线宽度 |
| $width-table_cell_fixed_right_first | 1px | 表格右侧固定列左侧分割线宽度 |
| $width-table_react_resizable_handle | 9px | 表格伸缩列调节热区宽度 |
| $height-table_pagination_outer_min | 60px | 表格分页器高度 |
| $height-table_column_filter_dropdown | 290px | 表格过滤筛选列表最大高度 |
| $color-table_panel-bg-default | var(--semi-color-primary) | 操作区域样式默认背景颜色 |
| $color-table_panel-text-default | var(--semi-color-primary-light-active) | 表格操作栏选中项文本颜色 |
| $color-table-bg-default | var(--semi-color-bg-1) | 表格固定列及吸顶表头背景颜色 |
| $color-table-text-default | var(--semi-color-text-0) | 单元格默认文字颜色 |
| $color-table-border-default | var(--semi-color-border) | 表格描边颜色 |
| $color-table_shadow-bg-default | rgb(from var(--semi-black) r g b / 0.08) | 表格滚动后阴影颜色 |
| $color-table_shadow-bg-dark | rgb(from var(--semi-black) r g b / 0.4) | 表格滚动后阴影颜色 - 暗色模式 |
| $color-table_shadow-border-default | var(--semi-color-border) | 表格拟阴影 描边颜色 |
| $color-table_th-bg-default | var(--semi-color-bg-1) | 表头背景底色,须不透明以遮住滚动到固定列和吸顶表头下的内容 |
| $color-table_th-fill-default | var(--semi-color-fill-0) | 表头背景叠加的填充色 |
| $color-table_th-border-default | var(--semi-color-border) | 表头底部分割线颜色 |
| $color-table_th-text-default | var(--semi-color-text-2) | 表头文字颜色 |
| $color-table_th-clickSort-bg-hover | var(--semi-color-fill-1) | 点击表头触发排序背景色 - 悬浮 |
| $color-table_pl-bg-default | transparent | |
| $color-table_body-bg-default | var(--semi-color-bg-1) | 表格行背景颜色 - 默认态 |
| $color-table_body-bg-hover | var(--semi-color-fill-0) | 表格行背景颜色 - 悬浮态 |
| $color-table_footer-bg-default | var(--semi-color-fill-0) | 表格 footer 背景颜色 - 默认 |
| $color-table_row_expanded-bg-default | var(--semi-color-fill-0) | 表格展开行背景颜色 - 默认 |
| $color-table_expanded_icon-default | var(--semi-color-text-2) | 表格展开行图标颜色 - 默认 |
| $color-table_expanded-bg-default | transparent | 表格展开行图标容器背景颜色 - 默认 |
| $color-table_filter_on-text-default | var(--semi-color-primary) | 表格过滤按钮颜色 - 激活态 |
| $color-table_filter-text-default | var(--semi-color-text-2) | 表格过滤按钮颜色 - 默认态 |
| $color-table_sorter_on-text-default | var(--semi-color-primary) | 表格排序按钮颜色 - 激活态 |
| $color-table_sorter-text-default | var(--semi-color-text-2) | 表格排序按钮颜色 - 默认态 |
| $color-table_sorter-text-hover | var(--semi-color-text-2) | 表格排序按钮颜色 - 悬浮态 |
| $color-table_page-text-default | var(--semi-color-text-2) | 表格翻页器文本颜色 |
| $color-table_resizer-bg-default | var(--semi-color-primary) | 表格拉伸标示线颜色 |
| $color-table_selection-bg-default | var(--semi-grey-0) | 表格分组背景色 |
| $color-table_placeholder-text-default | var(--semi-color-text-2) | 表格空数据文本颜色 |
| $color-table_cell-bg-hover | var(--semi-color-bg-0) | 让表格在 hover 时正确显示 $color-table_body-bg-hover 颜色,如无必要不要修改 |
| $font-table_base-fontSize | 14px | 表格默认文本字号 |
| $border-table_base-borderStyle | solid | 表格描边样式 |
| $width-table_fixed_shadow | 6px | 表格固定列滚动阴影宽度 |
| $z-table_fixed_column | $z-table-fixed | fixed列的zIndex值 |
FAQ
-
Clicking the row selection button on the second page will jump to the first page?
After the Table's dataSource is updated, the page number will be reset to the initial state. Please check if the data source changed when the component was rendered.
A component runs only once, so
dataSourceshould be kept in a signal and read in JSX (for exampledataSource={data()}). Replacing the whole array triggers an update; modifying the array in place (for example withpush) does not, so generate a new array and callsetDatainstead. -
The number of filtered data is wrong?
Please check that your filter columns and data sources are configured correctly.
The filter column needs to set an independent
dataIndex, and the dataSource needs to set an independentkey. Please refer to thedataSourceAPI. Otherwise the filtering function will not work properly. -
Why is the table data not updated?
Table updates by where props are read. Reading signals such as
columnsanddataSourcein JSX makes them follow changes. Acolumnsarray defined inside the component is created only once, so column configuration that depends on signals should use getters or be read insiderender. -
Why can't my form line be selected and expanded?
Please specify a rowKey or set a different "key" attribute for each item of the dataSource. All rows related operations in the table need to be used.
-
How to implement custom sorting or pass parameters to the server for sorting when clicking the sort button?
The input parameters of the onChange method include pagination, filters, and sorter. Users can customize the sorting of the dataSource according to the sorter.
-
How to add className to a row?
Use onRow or onHeaderRow.
-
How to style the table cell?
It can be controlled by column.onHeaderCell and column.onCell.
-
Why cache the previously selected keys for the first parameter of
rowSelectiononChange?This is for the scenario where the selected row keys are lost when data is selected on the first page during paging, and then the data is selected on the second page. If you don't want to use the cached keys, you can filter it from the current dataSource or use the second parameter of
rowSelectiononChange. -
Does it support single row selection?
Table currently does not support single-row selection function, and users can implement single selection in a custom way.