Theme

Examples › Nested tables

You can abuse the row expansion feature and make use of noHeader property to create nested tables.
Click on the expandable rows in the table below to see it in action:
Sipes Inc
58
Runolfsdottir - Cummerata
45
Johnston LLC
60
Crist and Sons
68
Schmidt and Sons
28
Nicolas Group
34
Kub and Sons
33
Jakubowski - Rolfson
74
Welch - Tremblay
50
Mueller, Hodkiewicz and Beahan
50
No records
import { Group, Text, createStyles, px } from '@mantine/core';
import { IconBuilding, IconChevronRight, IconUser, IconUsers } from '@tabler/icons-react';
import dayjs from 'dayjs';
import { DataTable } from 'mantine-datatable';
import { useState } from 'react';
import { companies, departments, employees } from '~/data/nested';
const useStyles = createStyles((theme) => ({
expandIcon: {
transition: 'transform 0.2s ease',
},
expandIconRotated: {
transform: 'rotate(90deg)',
},
employeeName: {
marginLeft: px(theme.spacing.xl) * 2,
},
}));
export default function NestedTablesExample() {
const [expandedCompanyIds, setExpandedCompanyIds] = useState<string[]>([]);
const [expandedDepartmentIds, setExpandedDepartmentIds] = useState<string[]>([]);
const { cx, classes } = useStyles();
return (
<DataTable
withBorder
withColumnBorders
highlightOnHover
columns={[
{
accessor: 'name',
title: 'Company › Department › Employee',
render: ({ id, name }) => (
<Group spacing="xs">
<IconChevronRight
size="0.9em"
className={cx(classes.expandIcon, {
[classes.expandIconRotated]: expandedCompanyIds.includes(id),
})}
/>
<IconBuilding size="0.9em" />
<Text>{name}</Text>
</Group>
),
},
{ accessor: 'employees', title: 'Employees › Birth date', textAlignment: 'right', width: 200 },
]}
records={companies}
rowExpansion={{
allowMultiple: true,
expanded: { recordIds: expandedCompanyIds, onRecordIdsChange: setExpandedCompanyIds },
content: (company) => (
<DataTable
noHeader
columns={[
{
accessor: 'name',
render: ({ id, name }) => (
<Group ml="lg" spacing="xs" noWrap>
<IconChevronRight
size="0.9em"
className={cx(classes.expandIcon, {
[classes.expandIconRotated]: expandedDepartmentIds.includes(id),
})}
/>
<IconUsers size="0.9em" />
<Text>{name}</Text>
</Group>
),
},
{ accessor: 'employees', textAlignment: 'right', width: 200 },
]}
records={departments.filter((department) => department.company.id === company.record.id)}
rowExpansion={{
allowMultiple: true,
expanded: { recordIds: expandedDepartmentIds, onRecordIdsChange: setExpandedDepartmentIds },
content: (department) => (
<DataTable
noHeader
columns={[
{
accessor: 'name',
render: ({ firstName, lastName }) => (
<Group spacing="xs" noWrap className={classes.employeeName}>
<IconUser size="0.9em" />
<Text>
{firstName} {lastName}
</Text>
</Group>
),
},
{
accessor: 'birthDate',
render: ({ birthDate }) => dayjs(birthDate).format('DD MMM YYYY'),
textAlignment: 'right',
width: 200,
},
]}
records={employees.filter((employee) => employee.department.id === department.record.id)}
/>
),
}}
/>
),
}}
/>
);
}
import { sortBy } from 'lodash';
import { useEffect, useState } from 'react';
import { DataTableSortStatus } from '~/../package';
import delay from '~/lib/delay';
import useIsMounted from '~/lib/useIsMounted';
import { companies as companyData, departments as departmentData, employees } from './index';
// Departments with employees count
export const departments = departmentData.map((department) => ({
...department,
employees: employees.filter((employee) => employee.department.id === department.id)?.length || 0,
}));
// Companies with employees count
export const companies = companyData.map((company) => ({
...company,
employees: departments
.filter((department) => department.company.id === company.id)
.reduce((sum, department) => sum + department.employees, 0),
}));
// Employees
export { employees };
// Hook simulating async companies fetching
export function useCompaniesAsync({ sortStatus }: { sortStatus: DataTableSortStatus }) {
const isMounted = useIsMounted();
const [records, setRecords] = useState<typeof companies>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (isMounted()) {
(async () => {
setLoading(true);
await delay({ min: 500, max: 800 });
if (isMounted()) {
const newRecords = sortBy(
companies,
sortStatus.columnAccessor === 'details' ? 'employees' : sortStatus.columnAccessor
);
if (sortStatus.direction === 'desc') newRecords.reverse();
setRecords(newRecords);
setLoading(false);
}
})();
}
}, [isMounted, sortStatus]);
return { records, loading };
}
// Hook simulating async departments fetching by company id
export function useDepartmentsAsync({
companyId,
sortStatus,
}: {
companyId: string;
sortStatus?: DataTableSortStatus;
}) {
const isMounted = useIsMounted();
const [records, setRecords] = useState<typeof departments>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (isMounted()) {
(async () => {
setLoading(true);
await delay({ min: 500, max: 800 });
if (isMounted()) {
let newRecords = departments.filter((department) => department.company.id === companyId);
if (sortStatus) {
newRecords = sortBy(
newRecords,
sortStatus.columnAccessor === 'details' ? 'employees' : sortStatus.columnAccessor
);
if (sortStatus.direction === 'desc') newRecords.reverse();
}
setRecords(newRecords);
setLoading(false);
}
})();
}
}, [companyId, isMounted, sortStatus]);
return { records, loading };
}
// Hook simulating async employees fetching by department id
export function useEmployeesAsync({
departmentId,
sortStatus,
}: {
departmentId: string;
sortStatus?: DataTableSortStatus;
}) {
const isMounted = useIsMounted();
const [records, setRecords] = useState<typeof employees>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (isMounted()) {
(async () => {
setLoading(true);
await delay({ min: 500, max: 800 });
if (isMounted()) {
let newRecords = employees.filter((employee) => employee.department.id === departmentId);
if (sortStatus) {
newRecords = sortBy(
newRecords,
sortStatus.columnAccessor === 'name'
? ({ firstName, lastName }) => `${firstName} ${lastName}`
: 'birthDate'
);
if (sortStatus.direction === 'desc') newRecords.reverse();
}
setRecords(newRecords);
setLoading(false);
}
})();
}
}, [departmentId, isMounted, sortStatus]);
return { records, loading };
}
Head over to the next example to see how you could asynchronously load data for nested tables.

Mantine DataTable is trusted by

MIT LicenseSponsor the author
Built by Ionut-Cristian Florescu and these awesome people.
Please sponsor the project if you find it useful.
GitHub StarsNPM Downloads