Layout
Table
A table displays large quantities of items or data in a list format. Filtering features, date picker, collapsible rows and actions may be added.
1import {2BlankSlate,3Box,4Button,5ColumnDef,6createColumnHelper,7DateRangePickerPreset,8onTableChangeEvent,9Table,10Title,11useTable,12} from '@coveord/plasma-mantine';13import {EditSize16Px} from '@coveord/plasma-react-icons';14import dayjs from 'dayjs';15import {FunctionComponent, useState} from 'react';1617interface IExampleRowData {18userId: number;19id: number;20title: string;21body: string;22}2324const columnHelper = createColumnHelper<IExampleRowData>();2526/**27* Define your columns outside the component rendering the table28* (or memoize them) to avoid unnecessary render loops29*/30const columns: Array<ColumnDef<IExampleRowData>> = [31columnHelper.accessor('userId', {32header: 'User ID',33cell: (info) => info.row.original.userId,34}),35columnHelper.accessor('id', {36header: 'Post ID',37cell: (info) => info.row.original.id,38}),39columnHelper.accessor('title', {40header: 'Title',41cell: (info) => info.row.original.title,42}),43Table.CollapsibleColumn as ColumnDef<IExampleRowData>,44// or if you prefer an accordion behaviour45// Table.AccordionColumn as ColumnDef<IExampleRowData>,46];4748const Demo = () => {49const [data, setData] = useState(null);50const [loading, setLoading] = useState(true);51const [pages, setPages] = useState(1);5253const fetchData: onTableChangeEvent<IExampleRowData> = async (state) => {54setLoading(true);55const searchParams = new URLSearchParams({56_sort: state.sorting?.[0]?.id ?? 'userId',57_order: state.sorting?.[0]?.desc ? 'desc' : 'asc',58_page: (state.pagination.pageIndex + 1).toString(),59_limit: state.pagination.pageSize.toString(),60userId: state.predicates.user,61title_like: state.globalFilter,62});63if (state.predicates.user === '') {64searchParams.delete('userId');65}66if (!state.globalFilter) {67searchParams.delete('title_like');68}69try {70const response = await fetch(`https://jsonplaceholder.typicode.com/posts?${searchParams.toString()}`);71const body = await response.json();72setData(body);73setPages(Math.ceil(Number(response.headers.get('x-total-count')) / state.pagination?.pageSize));74} catch (e) {75console.error(e);76} finally {77setLoading(false);78}79};8081return (82<Table83data={data}84getRowId={({id}) => id.toString()}85columns={columns}86noDataChildren={<NoData />}87onMount={fetchData}88onChange={fetchData}89loading={loading}90initialState={{dateRange: [previousDay, today], predicates: {user: ''}}}91getExpandChildren={(datum) => <Box py="xs">{datum.body}</Box>}92>93{/* you can override background color with: sx={{backgroundColor: 'white'}} for Header and Footer */}94<Table.Header>95<Table.Actions>{(datum: IExampleRowData) => <TableActions datum={datum} />}</Table.Actions>96<UserPredicate />97<Table.Filter placeholder="Search posts by title" />98<Table.DateRangePicker99rangeCalendarProps={{maxDate: dayjs().endOf('day').toDate()}}100presets={DatePickerPresets}101/>102</Table.Header>103<Table.Footer>104<Table.PerPage />105<Table.Pagination totalPages={pages} />106</Table.Footer>107</Table>108);109};110export default Demo;111112const NoData: FunctionComponent = () => {113const {clearFilters, isFiltered} = useTable();114115return isFiltered ? (116<BlankSlate>117<Title order={4}>No data found for those filters</Title>118<Button onClick={clearFilters}>Clear filters</Button>119</BlankSlate>120) : (121<BlankSlate>122<Title order={4}>No Data</Title>123</BlankSlate>124);125};126127const today: Date = dayjs().startOf('day').toDate();128const previousDay: Date = dayjs().subtract(1, 'day').endOf('day').toDate();129const previousWeek: Date = dayjs().subtract(1, 'week').endOf('day').toDate();130131const DatePickerPresets: Record<string, DateRangePickerPreset> = {132lastDay: {label: 'Last 24 hours', range: [previousDay, today]},133lastWeek: {label: 'Last week', range: [previousWeek, today]},134};135136const TableActions: FunctionComponent<{datum: IExampleRowData}> = ({datum}) => {137const actionCondition = datum.id % 2 === 0 ? true : false;138const pressedAction = () => alert('Edit action is triggered!');139return (140<>141{actionCondition ? (142<Button variant="subtle" onClick={pressedAction} leftIcon={<EditSize16Px height={16} />}>143Edit144</Button>145) : null}146</>147);148};149150const UserPredicate: FunctionComponent = () => (151<Table.Predicate152id="user"153label="User"154data={[155{156value: '',157label: 'All',158},159{value: '1', label: '1'},160{value: '2', label: '2'},161{value: '3', label: '3'},162{value: '4', label: '4'},163{value: '5', label: '5'},164{value: '6', label: '6'},165{value: '7', label: '7'},166{value: '8', label: '8'},167{value: '9', label: '9'},168{value: '10', label: '10'},169]}170/>171);
Props
Name | Type | Default | Description |
---|---|---|---|
columns required | ColumnDef<unknown>[] | Columns to display in the table. | |
data required | unknown[] | Data to display in the table | |
additionalRootNodes | HTMLElement[] | Nodes that are considered inside the table.
Rows normally get unselected when clicking outside the table, but sometimes it has difficulties guessing what is inside or outside, for example when using modals.
You can use this prop to force the table to consider some nodes to be inside the table. | |
children | ReactNode | Childrens to display in the table. They need to be wrap in either `Table.Header` or `Table.Footer` | |
disableRowSelection | boolean | false | Whether row selection is enabled or not |
doubleClickAction | (datum: unknown) => void | Action passed when user double clicks on a row | |
getExpandChildren | (datum: unknown) => ReactNode | Function that generates the expandable content of a row
Return null for rows that don't need to be expandable
| |
getRowId | (originalRow: unknown, index: number, parent?: Row<unknown>) => string | Defines how each row is uniquely identified. It is highly recommended that you specify this prop to an ID that makes sense. | |
initialState | InitialTableState<unknown> | Initial state of the table | |
layouts | TableLayout[] | [Table.Layouts.Rows] | Available layouts |
loading | boolean | false | Whether the table is loading or not |
multiRowSelectionEnabled | boolean | false | Whether the user can select multiple rows in order to perform actions in bulk |
noDataChildren | ReactNode | React children to show when the table has no rows to show. You can leverage useTable to get the state of the table | |
onChange | onTableChangeEvent<unknown> | Function called when the table should update
| |
onMount | onTableChangeEvent<unknown> | Function called when the table mounts
| |
onRowSelectionChange | (selectedRows: unknown[]) => void | Function called whenever the row selection changes
| |
options | Omit<Partial<TableOptions<unknown>>, "getRowId" | "data" | "initialState" | "columns" | "getRowCanExpand" | "manualPagination" | "enableRowSelection" | "enableMultiRowSelection" | "onRowSelectionChange"> | Additional options that can be passed to the table |
Examples
Table with bulk selection of rows
1import {2BlankSlate,3Button,4ColumnDef,5createColumnHelper,6onTableChangeEvent,7Table,8Title,9useTable,10} from '@coveord/plasma-mantine';11import {DeleteSize16Px, EditSize16Px} from '@coveord/plasma-react-icons';12import {FunctionComponent, useState} from 'react';1314interface IExampleRowData {15userId: number;16id: number;17title: string;18body: string;19}2021const columnHelper = createColumnHelper<IExampleRowData>();22const columns: Array<ColumnDef<IExampleRowData>> = [23columnHelper.accessor('userId', {24header: 'User ID',25cell: (info) => info.row.original.userId,26enableSorting: false,27}),28columnHelper.accessor('title', {29header: 'Title',30cell: (info) => info.row.original.title,31enableSorting: false,32}),33];3435const Demo = () => {36const [data, setData] = useState(null);37const [loading, setLoading] = useState(true);38const [pages, setPages] = useState(1);3940const fetchData: onTableChangeEvent<IExampleRowData> = async (state) => {41setLoading(true);42const searchParams = new URLSearchParams({43_page: (state.pagination.pageIndex + 1).toString(),44_limit: state.pagination.pageSize.toString(),45title_like: state.globalFilter,46});47if (!state.globalFilter) {48searchParams.delete('title_like');49}50try {51const response = await fetch(`https://jsonplaceholder.typicode.com/posts?${searchParams.toString()}`);52const body = await response.json();53setData(body);54setPages(Math.ceil(Number(response.headers.get('x-total-count')) / state.pagination?.pageSize));55} catch (e) {56console.error(e);57} finally {58setLoading(false);59}60};6162return (63<Table<IExampleRowData>64data={data}65getRowId={({id}) => id.toString()}66columns={columns}67noDataChildren={<NoData />}68onMount={fetchData}69onChange={fetchData}70loading={loading}71onRowSelectionChange={(selectedRows) =>72console.info(`Row selection changed, selected rows: ${selectedRows.map(({id}) => id).join(', ')}`)73}74multiRowSelectionEnabled75initialState={{76rowSelection: {77'1': {78userId: 1,79id: 1,80title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',81body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',82},83'2': {84userId: 1,85id: 2,86title: 'qui est esse',87body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla',88},89},90}}91>92<Table.Header>93<Table.Actions>94{(selectedRows: IExampleRowData[]) => <TableActions data={selectedRows} />}95</Table.Actions>96<Table.Filter placeholder="Search posts by title" />97</Table.Header>98<Table.Footer>99<Table.PerPage />100<Table.Pagination totalPages={pages} />101</Table.Footer>102</Table>103);104};105export default Demo;106107const NoData: FunctionComponent = () => {108const {isFiltered, clearFilters} = useTable();109110return isFiltered ? (111<BlankSlate>112<Title order={4}>No data found for those filters</Title>113<Button onClick={clearFilters}>Clear filters</Button>114</BlankSlate>115) : (116<BlankSlate>117<Title order={4}>No Data</Title>118</BlankSlate>119);120};121122const TableActions: FunctionComponent<{data: IExampleRowData[]}> = ({data}) => {123if (data.length === 1) {124return (125<Button126variant="subtle"127onClick={() => alert(`Action triggered on a single row: ${data[0].id}`)}128leftIcon={<EditSize16Px height={16} />}129>130Single row action131</Button>132);133}134if (data.length > 1) {135return (136<Button137variant="subtle"138onClick={() => alert(`Bulk action triggered on multiple rows: ${data.map(({id}) => id).join(', ')}`)}139leftIcon={<DeleteSize16Px height={16} />}140>141Bulk action142</Button>143);144}145146return null;147};
Table with disabled row selection
1import {2BlankSlate,3Button,4ColumnDef,5createColumnHelper,6onTableChangeEvent,7Table,8Title,9useTable,10} from '@coveord/plasma-mantine';11import {FunctionComponent, useState} from 'react';1213interface IExampleRowData {14userId: number;15id: number;16title: string;17body: string;18}1920const columnHelper = createColumnHelper<IExampleRowData>();21const columns: Array<ColumnDef<IExampleRowData>> = [22columnHelper.accessor('userId', {23header: 'User ID',24cell: (info) => info.row.original.userId,25enableSorting: false,26}),27columnHelper.accessor('title', {28header: 'Title',29cell: (info) => info.row.original.title,30enableSorting: false,31}),32];3334const Demo = () => {35const [data, setData] = useState(null);36const [loading, setLoading] = useState(true);37const [pages, setPages] = useState(1);3839const fetchData: onTableChangeEvent<IExampleRowData> = async (state) => {40setLoading(true);41const searchParams = new URLSearchParams({42_page: (state.pagination.pageIndex + 1).toString(),43_limit: state.pagination.pageSize.toString(),44title_like: state.globalFilter,45});46if (!state.globalFilter) {47searchParams.delete('title_like');48}49try {50const response = await fetch(`https://jsonplaceholder.typicode.com/posts?${searchParams.toString()}`);51const body = await response.json();52setData(body);53setPages(Math.ceil(Number(response.headers.get('x-total-count')) / state.pagination?.pageSize));54} catch (e) {55console.error(e);56} finally {57setLoading(false);58}59};6061return (62<Table<IExampleRowData>63data={data}64getRowId={({id}) => id.toString()}65columns={columns}66noDataChildren={<NoData />}67onMount={fetchData}68onChange={fetchData}69loading={loading}70onRowSelectionChange={(selectedRows) =>71console.info(`Row selection changed, selected rows: ${selectedRows.map(({id}) => id).join(', ')}`)72}73multiRowSelectionEnabled74disableRowSelection75initialState={{76rowSelection: {77'1': {78userId: 1,79id: 1,80title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',81body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',82},83'2': {84userId: 1,85id: 2,86title: 'qui est esse',87body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla',88},89},90}}91>92<Table.Header>93<Table.Filter placeholder="Search posts by title" />94</Table.Header>95<Table.Footer>96<Table.PerPage />97<Table.Pagination totalPages={pages} />98</Table.Footer>99</Table>100);101};102export default Demo;103104const NoData: FunctionComponent = () => {105const {isFiltered, clearFilters} = useTable();106107return isFiltered ? (108<BlankSlate>109<Title order={4}>No data found for those filters</Title>110<Button onClick={clearFilters}>Clear filters</Button>111</BlankSlate>112) : (113<BlankSlate>114<Title order={4}>No Data</Title>115</BlankSlate>116);117};
Table with client side pagination, sorting, and filtering
Moshe | Abbott | 10 |
Talia | Klocko | 17 |
Savion | Schmidt | 2 |
Jamir | Gulgowski | 3 |
Neal | Beier | 13 |
Results per page
1import {2ColumnDef,3createColumnHelper,4FilterFn,5getFilteredRowModel,6getPaginationRowModel,7getSortedRowModel,8Table,9TableProps,10} from '@coveord/plasma-mantine';11import {faker} from '@faker-js/faker';12import {rankItem} from '@tanstack/match-sorter-utils';13import {useMemo} from 'react';1415export type Person = {16id: string;17firstName: string;18lastName: string;19age: number;20};2122const makeData = (len: number): Person[] =>23Array(len)24.fill(0)25.map(() => ({26id: faker.datatype.uuid(),27firstName: faker.name.firstName(),28lastName: faker.name.lastName(),29age: faker.datatype.number(40),30}));3132const fuzzyFilter: FilterFn<Person> = (row, columnId, value) => rankItem(row.getValue(columnId), value).passed;3334const columnHelper = createColumnHelper<Person>();3536const columns: Array<ColumnDef<Person>> = [37columnHelper.accessor('firstName', {38header: 'First name',39cell: (info) => info.row.original.firstName,40}),41columnHelper.accessor('lastName', {42header: 'Last name',43cell: (info) => info.row.original.lastName,44}),45columnHelper.accessor('age', {46header: 'Age',47cell: (info) => info.row.original.age,48}),49];5051const options: TableProps<Person>['options'] = {52globalFilterFn: fuzzyFilter,53getFilteredRowModel: getFilteredRowModel(),54getPaginationRowModel: getPaginationRowModel(),55getSortedRowModel: getSortedRowModel(),56};5758const Demo = () => {59const data = useMemo(() => makeData(45), []);60return (61<Table62data={data}63columns={columns}64options={options}65initialState={{pagination: {pageSize: 5}}}66getRowId={({id}) => id}67>68<Table.Header>69<Table.Filter placeholder="Search" />70</Table.Header>71<Table.Footer>72<Table.PerPage values={[5, 10, 25]} />73<Table.Pagination totalPages={null} />74</Table.Footer>75</Table>76);77};78export default Demo;
Table with empty states
No data found for filter "foo" |
1import {BlankSlate, Button, ColumnDef, createColumnHelper, Table, Title, useTable} from '@coveord/plasma-mantine';2import {NoContentSize32Px} from '@coveord/plasma-react-icons';34export type Person = {5firstName: string;6lastName: string;7age: number;8};910const NoData = () => {11const {isFiltered, clearFilters, state} = useTable();1213return isFiltered ? (14<BlankSlate>15<Title order={4}>No data found for filter "{state.globalFilter}"</Title>16<Button onClick={clearFilters}>Clear filter</Button>17</BlankSlate>18) : (19<BlankSlate withBorder={false}>20<NoContentSize32Px height={64} />21<Title order={4}>Hello Empty State</Title>22</BlankSlate>23);24};2526const Demo = () => (27<Table28data={[]}29columns={columns}30noDataChildren={<NoData />}31initialState={{globalFilter: 'foo', pagination: {pageSize: 5}}}32>33<Table.Header>34<Table.Filter placeholder="Search" />35</Table.Header>36<Table.Footer>37<Table.PerPage values={[5, 10, 25]} />38<Table.Pagination totalPages={null} />39</Table.Footer>40</Table>41);42export default Demo;4344const columnHelper = createColumnHelper<Person>();4546const columns: Array<ColumnDef<Person>> = [47columnHelper.accessor('firstName', {48header: 'First name',49cell: (info) => info.row.original.firstName,50}),51columnHelper.accessor('lastName', {52header: 'Last name',53cell: (info) => info.row.original.lastName,54}),55columnHelper.accessor('age', {56header: 'Age',57cell: (info) => info.row.original.age,58}),59];
Table with a child component using the hook to re-fetch
1import {ColumnDef, createColumnHelper, onTableChangeEvent, Table, useTable} from '@coveord/plasma-mantine';2import {FunctionComponent, useEffect, useState} from 'react';34interface PostData {5id: number;6title: string;7}89const columnHelper = createColumnHelper<PostData>();10const columns: Array<ColumnDef<PostData>> = [11columnHelper.accessor('id', {12header: 'Post ID',13enableSorting: false,14}),15columnHelper.accessor('title', {16header: 'Title',17enableSorting: false,18}),19];2021const Demo = () => {22const [data, setData] = useState(null);23const [loading, setLoading] = useState(true);2425const fetchData: onTableChangeEvent<PostData> = async () => {26setLoading(true);27try {28const response = await fetch('https://jsonplaceholder.typicode.com/posts');29const body = await response.json();3031// slow down the fetch, to make the refresh more obvious32await new Promise((resolve) => setTimeout(resolve, 1000));3334setData(body);35} catch (e) {36console.error(e);37} finally {38setLoading(false);39}40};4142return (43<Table<PostData>44data={data}45getRowId={({id}) => id.toString()}46columns={columns}47onMount={fetchData}48onChange={fetchData}49loading={loading}50>51<Table.Consumer>52{/* Refresh the component every 10s, look at your network tab to validate it works */}53<IntervalRefresh every={10 * 1000} />54</Table.Consumer>55<Table.LastUpdated />56</Table>57);58};59export default Demo;6061const IntervalRefresh: FunctionComponent<{every: number}> = ({every}) => {62const {onChange} = useTable();63useEffect(() => {64const timer = setInterval(() => onChange(), every);65return () => clearInterval(timer);66}, [every]);6768return null;69};
No guidelines exist for Table yet.