Table

A table is a specific pattern for comparing datasets in a very direct and analytical way.

installyarn add @clayui/core
versionNPM Version
useimport {Body, Cell, Head, Row, Table} from '@clayui/core';

Example

import React, {useCallback, useState} from 'react';
import {Body, Cell, Text, Head, Row, Table, Provider} from '@clayui/core';

import '@clayui/css/lib/css/atlas.css';

export default function App() {
	const [sort, setSort] = useState(null);
	const [items, setItems] = useState([
		{files: 22, id: 1, name: 'Games', type: 'File folder'},
		{files: 7, id: 2, name: 'Program Files', type: 'File folder'},
	]);

	const onSortChange = useCallback((sort) => {
		if (sort) {
			setItems((items) =>
				items.sort((a, b) => {
					let cmp = new Intl.Collator('en', {numeric: true}).compare(
						a[sort.column],
						b[sort.column]
					);

					if (sort.direction === 'descending') {
						cmp *= -1;
					}

					return cmp;
				})
			);
		}

		setSort(sort);
	}, []);

	return (
		<Provider spritemap="/public/icons.svg">
			<div className="p-4">
				<Table onSortChange={onSortChange} sort={sort}>
					<Head
						items={[
							{
								id: 'name',
								name: 'Name',
							},
							{
								id: 'files',
								name: 'Files',
							},
							{
								id: 'type',
								name: 'Type',
							},
						]}
					>
						{(column) => (
							<Cell key={column.id} sortable>
								{column.name}
							</Cell>
						)}
					</Head>

					<Body defaultItems={items}>
						{(row) => (
							<Row>
								<Cell>
									<Text size={3} weight="semi-bold">
										{row['name']}
									</Text>
								</Cell>
								<Cell>{row['files']}</Cell>
								<Cell>{row['type']}</Cell>
							</Row>
						)}
					</Body>
				</Table>
			</div>
		</Provider>
	);
}

Introduction

Table allows the rendering of static and dynamic content for data-oriented and data-agnostic tables to prevent the developer from having to transform their data to be able to render in a table. Composition is the central point, as is the use of the render props pattern to allow this so that the component can offer OOTB features, such as sorting and nested row.

The component covers the W3C accessibility patterns for the simplest table implementation and also the treegrid pattern for when nested row is used without requiring the developer to have to configure something extremely complex.

Warning To use the new Table implementation it is necessary to consume the component using the package @clayui/core.

Content

Content rendered in the <Table /> Menu can be done in two different ways, static and dynamic content, the choice depends on the use case.

Static

Static content is when the <Table /> options do not change during the lifecycle of the application or are hardcoded options.

<Table>
	<Head>
		<Cell key="name">Name</Cell>
		<Cell key="type">Type</Cell>
	</Head>

	<Body>
		<Row>
			<Cell>Games</Cell>
			<Cell>File Folder</Cell>
		</Row>
		<Row>
			<Cell>Program Files</Cell>
			<Cell>File Folder</Cell>
		</Row>
	</Body>
</Table>

Dynamic

Unlike static content, dynamic content is when the options can change during the lifecycle of the application or when the data comes from a service. Dynamic content rendering is data agnostic, this allows you to configure how to render the component options regardless of the chosen data structure.

<Table>
	<Head
		items={[
			{
				id: '1',
				name: 'Name',
			},
			{
				id: '2',
				name: 'Type',
			},
		]}
	>
		{(column) => <Cell key={column.id}>{column.name}</Cell>}
	</Head>

	<Body
		defaultItems={[
			{id: 1, name: 'Games', type: 'File folder'},
			{id: 2, name: 'Program Files', type: 'File folder'},
		]}
	>
		{(row) => (
			<Row>
				<Cell>{row.name}</Cell>
				<Cell>{row.type}</Cell>
			</Row>
		)}
	</Body>
</Table>

Icons

Warning When implementing ClayTable from a React application, the icons may not render. The Application Provider method will make the icons available for use.

<Provider spritemap={spritemap}>
	<Table>
		<Head
			items={[
				{
					id: '1',
					name: 'Name',
				},
				{
					id: '2',
					name: 'Type',
				},
			]}
		>
			{(column) => <Cell key={column.id}>{column.name}</Cell>}
		</Head>

		<Body
			defaultItems={[
				{id: 1, name: 'Games', type: 'File folder'},
				{id: 2, name: 'Program Files', type: 'File folder'},
			]}
		>
			{(row) => (
				<Row>
					<Cell>{row.name}</Cell>
					<Cell>{row.type}</Cell>
				</Row>
			)}
		</Body>
	</Table>
</Provider>

Sorting

Column sorting is implemented OOTB so the developer doesn’t need to worry about implementing the UI details but the developer still needs to add their filter layer since the component is data-agnostic and allows you to do this asynchronously, it is important, especially when your data is paged, that the filter must happen in the backend.

It is also possible to implement your own logic on the client side when your data is predictable, check out the pseudocode.

Info Column sorting is only enabled for columns that contain the sortable API defined.

export function App() {
	const [sort, setSort] = (useState < Sorting) | (null > null);
	const [items, setItems] = useState([
		{files: 22, id: 1, name: 'Games', type: 'File folder'},
		{files: 7, id: 2, name: 'Program Files', type: 'File folder'},
	]);

	const filteredItems = useMemo(() => {
		if (!sort) {
			return;
		}

		return items.sort((a, b) => {
			let cmp = new Intl.Collator('en', {numeric: true}).compare(
				a[sort.column],
				b[sort.column]
			);

			if (sort.direction === 'descending') {
				cmp *= -1;
			}

			return cmp;
		});
	}, [sort, items]);

	return (
		<Table onSortChange={setSort} sort={sort}>
			<Head>
				<Cell key="name" sortable>
					Name
				</Cell>
				<Cell key="files" sortable>
					Files
				</Cell>
				<Cell key="type" sortable>
					Type
				</Cell>
			</Head>

			<Body defaultItems={filteredItems}>...</Body>
		</Table>
	);
}

Asynchronous

Most tables with sorting can have a lot of data and are paged so the sorting must happen on the server side instead of implementing the logic on the client side. You can achieve this level of implementation by composing using the useResource hook.

function App() {
	const {
		resource: items,
		sort,
		sortChange,
	} = useResource({
		fetch: (link, init, sort) => {
			const url = new URL(link);

			if (sort) {
				url.searchParams.append('column', String(sort.column));
				url.searchParams.append('direction', sort.direction);
			}

			return fetch(url, init);
		},
		link: 'https://example.com/api/items',
	});

	return (
		<Table onSortChange={setSort} sort={sort}>
			<Head>
				<Cell key="name" sortable>
					Name
				</Cell>
				<Cell key="files" sortable>
					Files
				</Cell>
				<Cell key="type">Type</Cell>
			</Head>

			<Body defaultItems={items}>...</Body>
		</Table>
	);
}

Nested row

Implementing nested row allows you to render a table as a tree view. It is not necessary that you have to change your composition to render in tree view but just configure the nestedKey property to inform which nested key and the composition can continue in the same way.

When using the nested row pattern, Clay automatically changes the accessibility behavior to use the treegrid recommendation instead of the default behavior.

Limitation The unique id of a row does not work properly when configured via key in the Row component property to deal with the nodes expandability, it is necessary that the id key is defined in your row data to use as unique id.

import React from 'react';
import {Body, Cell, Head, Row, Table, Provider} from '@clayui/core';

import '@clayui/css/lib/css/atlas.css';

export default function App() {
	const columns = [
		{
			id: 'name',
			name: 'Name',
		},
		{
			id: 'type',
			name: 'Type',
		},
	];

	return (
		<Provider spritemap="/public/icons.svg">
			<div className="p-4">
				<Table aria-label="Drive" nestedKey="children">
					<Head items={columns}>
						{(column) => (
							<Cell
								className="table-cell-minw-300"
								key={column.id}
							>
								{column.name}
							</Cell>
						)}
					</Head>

					<Body
						defaultItems={[
							{
								children: [
									{id: 10, name: 'WoW', type: 'MMORPG'},
								],
								id: 1,
								name: 'Games',
								type: 'File folder',
							},
							{
								id: 2,
								name: 'Program Files',
								type: 'File folder',
							},
						]}
					>
						{(row) => (
							<Row items={columns}>
								{(column) => (
									<Cell key={row.id + ':' + column.id}>
										{row[column.id]}
									</Cell>
								)}
							</Row>
						)}
					</Body>
				</Table>
			</div>
		</Provider>
	);
}

Expandable

Expanding nodes is done OOTB in the component but it is also possible to control the state to modify behaviors if necessary or use it to save a session to improve the user experience.

const [expandedKeys, setExpandedKeys] = useState(new Set());

<Table expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys}>
	<Head items={columns}>
		{(column) => <Cell key={column.id}>{column.name}</Cell>}
	</Head>

	<Body defaultItems={rows}>
		{(row) => (
			<Row items={columns}>
				{(column) => <Cell>{row[column.id]}</Cell>}
			</Row>
		)}
	</Body>
</Table>;

Asynchronous Item

When the tree is very large with a lot of data on a single node, loading the data asynchronously is essential to reduce the initial data payload and memory space. Table supports asynchronous node loading when the user expands a node.

  • When returning void , null or undefined the Table will do nothing.
  • When returning the items will add to the tree.

Warning If you have an error in the asynchronous call of the onLoadMore method, only the suppression is done and an error is thrown on the console.

When adding a new asynchronous item to the tree, the onItemsChange method is respectively called to update the tree with a new value if the items prop is controlled.

<Table
	onLoadMore={async (item) => {
		return await fetch(`example.com/tree/item?parent_id=${item.id}`);
	}}
>
	<Head items={columns}>
		{(column) => <Cell key={column.id}>{column.name}</Cell>}
	</Head>

	<Body
		defaultItems={[
			{id: 1, name: 'Games', type: 'File folder'},
			{id: 2, name: 'Program Files', type: 'File folder'},
		]}
	>
		{(row) => (
			<Row>
				<Cell>{row.name}</Cell>
				<Cell>{row.type}</Cell>
			</Row>
		)}
	</Body>
</Table>