Master-Detail

Nest a fully interactive sub-grid, or any custom component, under an expandable parent row.

Master-detail renders a custom component — commonly a nested <Grid> — directly under an expandable parent row. Enable it with the detail option — height sizes the detail rows (a number or a function of the row) and isMaster chooses which rows can open one — and supply the content with detailRowRenderer.

import { useCallback, useMemo } from 'react';
import { Grid, type ColumnDef, type VisualRow, type GridApi, type CellRendererProps, type GridOptions } from '@witbybit/wit-grid-react';

interface OrderRow {
	id: string;
	customerName: string;
	totalAmount: number;
}

interface OrderItemRow {
	id: string;
	itemName: string;
	price: number;
	quantity: number;
}

const DetailToggleRenderer = ({ rowId, api }: CellRendererProps<OrderRow>) => (
	<button onClick={() => void api.runCommand(api.isDetailOpen(rowId) ? 'closeDetail' : 'openDetail', { rowId })}>
		{api.isDetailOpen(rowId) ? '▼' : '▶'}
	</button>
);

const NestedItemsGrid = ({ visualRow }: { visualRow: VisualRow<OrderRow> }) => {
	if (visualRow.kind !== 'detail') return null;

	const items: OrderItemRow[] = [
		{ id: 'ITM-01', itemName: 'Widget A', price: 25, quantity: 4 },
		{ id: 'ITM-02', itemName: 'Widget B', price: 12.5, quantity: 2 },
	];

	const detailColumns = useMemo<ColumnDef<OrderItemRow>[]>(
		() => [
			{ field: 'itemName', header: 'Item', width: 220 },
			{ field: 'price', header: 'Price', width: 100 },
			{ field: 'quantity', header: 'Qty', width: 80 },
		],
		[]
	);

	return (
		<div style={{ height: '100%', padding: '8px 8px 8px 40px' }}>
			<Grid rows={items} columns={detailColumns} getRowId={(row) => row.id} navigation />
		</div>
	);
};

// The `detail` option, defined once (an option given anew applies again).
const DETAIL: GridOptions<OrderRow>['detail'] = { height: 220 };

export function MasterOrdersGrid({ orders }: { orders: OrderRow[] }) {
	const masterColumns = useMemo<ColumnDef<OrderRow>[]>(
		() => [
			{ field: 'toggle', header: '', width: 45, renderer: { kind: 'react', component: DetailToggleRenderer } },
			{ field: 'id', header: 'Order ID', width: 120 },
			{ field: 'customerName', header: 'Customer', width: 220 },
			{ field: 'totalAmount', header: 'Total', width: 120 },
		],
		[]
	);

	const detailRowRenderer = useCallback(
		({ visualRow }: { visualRow: VisualRow<OrderRow>; api: GridApi<OrderRow> }) => <NestedItemsGrid visualRow={visualRow} />,
		[]
	);

	return <Grid rows={orders} columns={masterColumns} getRowId={(row) => row.id} detail={DETAIL} detailRowRenderer={detailRowRenderer} />;
}

Nested grids are independent instances

The grid rendered inside detailRowRenderer is a completely separate <Grid> instance with its own state, selection, and API handle — it isn't a view into the parent's data. Fetch or derive its rows from visualRow.parentId (the master row's id) as needed.

Detail content is as wide as the grid's visible area and stays in view when the grid scrolls horizontally. Detail rows entering during a scroll are drawn in that frame, not after the scroll ends.

Set detail.height: 'auto' to size each detail row to its content: the grid measures it (starting from detail.estimatedHeight, default 200) and keeps the row in step when the content changes.

Instead of the adapter's detailRowRenderer, detail.renderer takes a renderer spec: { kind: 'dom', renderer } is mounted by the grid itself, synchronously, with { row, masterData, api } — mount(container, params) returns { update?, destroy? } — and { kind: 'react', component } is rendered by the React adapter with { visualRow, api }. grouping.rowRenderer does the same for full-width group rows (grouping.display: 'row'), but with a GroupRenderContext instead (see Row grouping).

Open and close detail rows with the openDetail / closeDetail commands (api.runCommand('openDetail', { rowId })), or set them all as state (api.setState({ expansion: { ...api.getState().expansion, details } })); api.isDetailOpen(rowId) reads one. detailRowRenderer receives { visualRow, api } where api is the master grid's GridApi — use it to read sibling row data or coordinate with the parent grid.

Edit on GitHub

Last updated on