Master Detail
A panel under any row, drawn by a snippet of your own. Rows without one keep no chevron, so the grid never offers to expand something empty.
Basic Usage
The panel is a full-width row, drawn by the fullWidthRow snippet. It is a real row of the grid, so it sits in the scroll flow and in the keyboard matrix
rather than floating over them.
The grid is holding 8 order rows and 0 open panels. Counted live with isDataRow, which
is what to walk with when a panel must not be counted as data.
<script lang="ts">
import {
createDataGrid, DataGrid, masterDetail, sorting,
type DataGridFullWidthContext
} from '@sv5ui/datagrid';
const money = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
const grid = createDataGrid<Order>({
columns,
data: orders,
getRowId: (order) => order.id,
features: [
sorting<Order>(),
masterDetail<Order>({ hasDetail: (order) => order.lines.length > 0 })
]
});
</script>
{#snippet lines({ row }: DataGridFullWidthContext<Order>)}
<div class="border-s-2 border-primary/40 bg-surface-container/40 px-4 py-3">
<p class="mb-2 text-xs font-medium text-on-surface">
{row.id}
<span class="font-normal text-on-surface/60">
· {row.lines.length} lines
</span>
</p>
<table class="w-full text-xs">
<thead>
<tr class="border-b border-on-surface/15 text-on-surface/60">
<th class="w-1/2 py-1.5 text-start font-medium">SKU</th>
<th class="py-1.5 text-end font-medium">Qty</th>
<th class="py-1.5 text-end font-medium">Unit</th>
<th class="py-1.5 text-end font-medium">Line total</th>
</tr>
</thead>
<tbody>
{#each row.lines as line (line.sku)}
<tr class="border-b border-on-surface/5 last:border-0">
<td class="py-1.5 font-mono">{line.sku}</td>
<td class="py-1.5 text-end tabular-nums">{line.qty}</td>
<td class="py-1.5 text-end tabular-nums">{money.format(line.price)}</td>
<td class="py-1.5 text-end font-medium tabular-nums">
{money.format(line.qty * line.price)}
</td>
</tr>
{/each}
</tbody>
<tfoot>
<tr class="border-t border-on-surface/15">
<td class="py-1.5 text-on-surface/60" colspan="3">Order total</td>
<td class="py-1.5 text-end font-semibold tabular-nums">
{money.format(row.total)}
</td>
</tr>
</tfoot>
</table>
</div>
{/snippet}
<DataGrid {grid} fullWidthRow={lines} />
<!-- row is the master row, not one of its own: a detail node points back at
the row it opened under, so the footer above can read row.total and
match the Total column of the grid row it hangs under. -->The snippet is handed the master row, not a row of its own: a detail node points back at the
row it opened under, so row is the
same object the grid row above it drew.
Rows That Cannot Open
hasDetail decides which rows offer a panel. Three of the orders above have no lines yet, and they carry
no chevron at all rather than opening onto nothing: SO-2100, SO-2103, SO-2106.
masterDetail<Order>({
hasDetail: (order) => order.lines.length > 0
});
const detail = getMasterDetail(grid)!;
detail.hasDetail(orders[0]); // false: no lines, so no chevron
// Without hasDetail every row can open, which is wrong the moment one has
// nothing to show.One Panel at a Time
single: true closes the last panel when the next one opens. Open two rows below and the first shuts on its
own.
masterDetail<Order>({ hasDetail, single: true });
const detail = getMasterDetail(grid)!;
detail.toggle('SO-2101');
detail.toggle('SO-2102'); // SO-2101 closes on its own
detail.closeAll();A Panel That Fetches
A panel is mounted when it opens and unmounted when it closes, so a request belongs inside the snippet rather than beside the grid.
The grid below is that, running. Both halves are requests: the five teams were counted and totalled by the server, and a team's best paid people are not asked for until its row is opened. Watch the log underneath while you open one, then another.
No data
The last few requests
- Nothing yet. Open a team.
{#snippet lines({ row }: DataGridFullWidthContext<Order>)}
<div class="border-s-2 border-primary/40 bg-surface-container/40 px-4 py-3">
{#await loadLines(row.id)}
<p class="text-xs text-on-surface/60">Loading...</p>
{:then lines}
<LineTable {lines} />
{:catch error}
<p class="text-xs text-error">{error.message}</p>
{/await}
</div>
{/snippet}
<!-- A panel is mounted when it opens and unmounted when it closes, so the
fetch belongs in the snippet. Pair it with single: true, or opening five
rows starts five requests and closing them cancels none. -->single there
is at most one in flight. Options
Everything masterDetail() accepts.
| Option | Description |
|---|---|
hasDetail | Which rows can open a panel. A row that answers false keeps no chevron, so the grid never offers to expand something empty |
single | Only one panel open at a time. Worth it when a panel is tall, or fetches its own data |
Reading It Back
What getMasterDetail(grid) exposes. toggle and closeAll are also on grid.api.
| Member | What it answers |
|---|---|
toggle(id) | Open or close the panel under that row |
closeAll() | Close every open panel |
hasDetail(row) | Whether that row can open at all |
isDetailRow(node) | Whether a node is a panel rather than a row |
masterOf(node) | The row id a panel hangs under |
isDetailNode(id) | The same question from a bare row id, with no node and no feature to ask. Exported on its own |
Not Beside a Pivot
A detail panel opens under a row from the data. A pivot row is a total over many of them, so there is no one row for a panel to describe, and the grid refuses to build with masterDetail() and pivot() registered together. What a reader wants from a pivot cell is the rows behind it, which rowsBehind answers.
Driving It From Code
import { getMasterDetail, isDataRow, isDetailNode } from '@sv5ui/datagrid';
const detail = getMasterDetail(grid)!;
detail.toggle('SO-2101');
detail.closeAll();
detail.hasDetail(order); // whether that row can open
detail.isDetailRow(node); // whether a node is a panel
detail.masterOf(node); // the row id a panel hangs under
detail.single;
grid.api.toggleDetail?.('SO-2101');
grid.api.closeAllDetails?.();
isDetailNode(id); // the same question from a bare row id
let dataRows = 0;
for (const node of grid.preWindowNodes) if (isDataRow(node)) dataRows += 1;
// 8 order rows, however many panels are open