Two ways to show a row's detail
You have a list of orders and each one has ports. Or invoices with lines, or shipments with parcels, or tickets with replies. The child records will not fit as columns and they are the reason anybody opened the screen, so they have to be reachable without a route change and without losing the list.
There are two answers and they are not interchangeable, which is the part most component libraries leave you to work out for yourself.
The row opens, or the pane does
Expansion is the familiar one. Click the chevron, the row grows, a nested grid appears underneath it inside the master’s own scroller. Its virtue is that several can be open at once, so it is the right answer when the job is comparison: three orders open together, ports beside ports, scroll between them. Its cost is geometry. Every open detail changes the height of the list, so row positions move under the pointer, and a scrollbar that meant something a moment ago now means something else.
The pane is the other one, and it is what the grid above is doing. Give the detail a target element and nothing expands:
detail: {
target: pane,
rows: (row) => row.data.ports,
config: { rowKey: 'port', autoHeight: true, columns: PORT_COLUMNS },
}
The master list’s geometry is now fixed. No row grows, the row count never changes, and the scrollbar means the same thing all afternoon. Exactly one master is open at a time, so opening a second closes the first, and the expander control changes shape to say so: it becomes an “opens elsewhere” toggle rather than a chevron. That is not decoration. A chevron promises the detail will appear where the chevron is, and here it will not.
Open an order in the grid above, then the next one, then the one after. The list does not move. That is the whole argument for the pane, and it is the argument for a queue: somebody working down twelve thousand provisioning orders wants the next row to be exactly where the next row was.
So: expansion when the reader is comparing several records, the pane when the
reader is processing one at a time. Both are the same detail block with one
field different, which means you can put the decision in a user preference
rather than in a rewrite.
The pane is yours, and that has consequences
detail.target takes an element, and the grid does not create it, because the
grid has no idea what your layout is. The nested grid measures its container the
same way the outer one does, so give the pane a real height before the grid is
created. The demo above builds its pane with flex: 1 1 20rem and an
explicit height, then hands it over.
The rest of the block is worth reading as a set of decisions rather than
options. rows is a function of the master row, so the children can be a
property already on the record, or a slice you compute, or anything you can
return synchronously. config is a whole grid configuration, which means the
nested grid gets the same columns, types, formatters and totals machinery as the
outer one rather than a cut-down table. If you use expansion instead, height
sizes the expanded region and cacheLimit bounds what is retained after
closing, which is a different question from how many may be open: several can be
open at once and the limit applies to closed ones being kept warm.
Every order in this data has at least one port, which is the shape a provisioning queue actually has.