Examples › Complex usage scenario
Here is a complex usage scenario featuring custom column definitions, asynchronous data loading with TanStack React Query, sorting, pagination, custom cell data rendering, multiple row selection, row expansion, action cells, and row context-menu.
Name | Email | Company | Department | City | State | Age | ||
---|---|---|---|---|---|---|---|---|
...
No records
Since this example is using React Query, we have to wrap everything in a QueryClientProvider
like so:
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export function ComplexUsageExampleWrapper({ children }: React.PropsWithChildren) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
);
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
Here is the actual code:
'use client';
import { ActionIcon, Button, Center, Flex, Group, Image, MantineTheme, Text, TextInput, rem } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import { IconClick, IconEdit, IconMessage, IconTrash, IconTrashX } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { DataTable, DataTableColumn, DataTableProps, DataTableSortStatus } from 'mantine-datatable';
import dayjs from 'dayjs';
import { useContextMenu } from 'mantine-contextmenu';
import { useCallback, useState } from 'react';
import { Employee } from '~/data';
import { getEmployeesAsync } from '~/data/async';
import classes from './ComplexUsageExample.module.css';
const PAGE_SIZE = 100;
export function ComplexUsageExample() {
const { showContextMenu, hideContextMenu } = useContextMenu();
const [page, setPage] = useState(1);
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<Employee>>({
columnAccessor: 'name',
direction: 'asc',
});
const { data, isFetching } = useQuery({
queryKey: ['employees', sortStatus.columnAccessor, sortStatus.direction, page],
queryFn: () => getEmployeesAsync({ recordsPerPage: PAGE_SIZE, page, sortStatus, delay: { min: 300, max: 500 } }),
});
const [selectedRecords, setSelectedRecords] = useState<Employee[]>([]);
const handleSortStatusChange = (status: DataTableSortStatus<Employee>) => {
setPage(1);
setSortStatus(status);
};
const editRecord = useCallback(({ firstName, lastName }: Employee) => {
showNotification({
withBorder: true,
title: 'Editing record',
message: `In a real application we could show a popup to edit ${firstName} ${lastName}, but this is just a demo, so we're not going to do that`,
});
}, []);
const deleteRecord = useCallback(({ firstName, lastName }: Employee) => {
showNotification({
withBorder: true,
color: 'red',
title: 'Deleting record',
message: `Should delete ${firstName} ${lastName}, but we're not going to, because this is just a demo`,
});
}, []);
const deleteSelectedRecords = useCallback(() => {
showNotification({
withBorder: true,
color: 'red',
title: 'Deleting multiple records',
message: `Should delete ${selectedRecords.length} records, but we're not going to do that because deleting data is bad... and this is just a demo anyway`,
});
}, [selectedRecords.length]);
const sendMessage = useCallback(({ firstName, lastName }: Employee) => {
showNotification({
withBorder: true,
title: 'Sending message',
message: `A real application could send a message to ${firstName} ${lastName}, but this is just a demo and we're not going to do that because we don't have a backend`,
color: 'green',
});
}, []);
const renderActions: DataTableColumn<Employee>['render'] = (record) => (
<Group gap={4} justify="right" wrap="nowrap">
<ActionIcon
size="sm"
variant="transparent"
color="green"
onClick={(e) => {
e.stopPropagation(); // 👈 prevent triggering the row click function
openModal({
title: `Send message to ${record.firstName} ${record.lastName}`,
classNames: { header: classes.modalHeader, title: classes.modalTitle },
children: (
<>
<TextInput mt="md" placeholder="Your message..." />
<Group mt="md" gap="sm" justify="flex-end">
<Button variant="transparent" c="dimmed" onClick={() => closeAllModals()}>
Cancel
</Button>
<Button
color="green"
onClick={() => {
sendMessage(record);
closeAllModals();
}}
>
Send
</Button>
</Group>
</>
),
});
}}
>
<IconMessage size={16} />
</ActionIcon>
<ActionIcon
size="sm"
variant="transparent"
onClick={(e) => {
e.stopPropagation(); // 👈 prevent triggering the row click function
editRecord(record);
}}
>
<IconEdit size={16} />
</ActionIcon>
</Group>
);
const rowExpansion: DataTableProps<Employee>['rowExpansion'] = {
allowMultiple: true,
content: ({ record: { id, sex, firstName, lastName, birthDate, department } }) => (
<Flex p="xs" pl={rem(50)} gap="md" align="center">
<Image
radius="sm"
w={50}
h={50}
alt={`${firstName} ${lastName}`}
src={`https://xsgames.co/randomusers/avatar.php?g=${sex}&q=${id}`}
/>
<Text size="sm" fs="italic">
{firstName} {lastName}, born on {dayjs(birthDate).format('MMM D YYYY')}, works in {department.name} department
at {department.company.name}.
<br />
His office address is {department.company.streetAddress}, {department.company.city},{' '}
{department.company.state}.
</Text>
</Flex>
),
};
const handleContextMenu: DataTableProps<Employee>['onRowContextMenu'] = ({ record, event }) =>
showContextMenu([
{
key: 'edit',
icon: <IconEdit size={14} />,
title: `Edit ${record.firstName} ${record.lastName}`,
onClick: () => editRecord(record),
},
{
key: 'delete',
title: `Delete ${record.firstName} ${record.lastName}`,
icon: <IconTrashX size={14} />,
color: 'red',
onClick: () => deleteRecord(record),
},
{ key: 'divider' },
{
key: 'deleteMany',
hidden: selectedRecords.length <= 1 || !selectedRecords.map((r) => r.id).includes(record.id),
title: `Delete ${selectedRecords.length} selected records`,
icon: <IconTrash size={14} />,
color: 'red',
onClick: deleteSelectedRecords,
},
])(event);
const now = dayjs();
const aboveXs = (theme: MantineTheme) => `(min-width: ${theme.breakpoints.xs})`;
const columns: DataTableProps<Employee>['columns'] = [
{
accessor: 'name',
noWrap: true,
sortable: true,
render: ({ firstName, lastName }) => `${firstName} ${lastName}`,
},
{
accessor: 'email',
sortable: true,
},
{
accessor: 'department.company.name',
title: 'Company',
noWrap: true,
sortable: true,
visibleMediaQuery: aboveXs,
},
{
accessor: 'department.name',
title: 'Department',
sortable: true,
visibleMediaQuery: aboveXs,
},
{
accessor: 'department.company.city',
title: 'City',
noWrap: true,
visibleMediaQuery: aboveXs,
},
{
accessor: 'department.company.state',
title: 'State',
visibleMediaQuery: aboveXs,
},
{
accessor: 'age',
width: 80,
textAlign: 'right',
sortable: true,
render: ({ birthDate }) => now.diff(birthDate, 'years'),
visibleMediaQuery: aboveXs,
},
{
accessor: 'actions',
title: (
<Center>
<IconClick size={16} />
</Center>
),
width: '0%', // 👈 use minimal width
render: renderActions,
},
];
return (
<DataTable
height="70dvh"
minHeight={400}
maxHeight={1000}
withTableBorder
highlightOnHover
borderRadius="sm"
withColumnBorders
striped
verticalAlign="top"
pinLastColumn
columns={columns}
fetching={isFetching}
records={data?.employees}
page={page}
onPageChange={setPage}
totalRecords={data?.total}
recordsPerPage={PAGE_SIZE}
sortStatus={sortStatus}
onSortStatusChange={handleSortStatusChange}
selectedRecords={selectedRecords}
onSelectedRecordsChange={setSelectedRecords}
rowExpansion={rowExpansion}
onRowContextMenu={handleContextMenu}
onScroll={hideContextMenu}
/>
);
}
Head over to the next page to see Mantine DataTable type definitions.