developer guide
Master-Detail Rows in a JavaScript Data Grid
A row can open into the records behind it, either as a nested grid within the row or as a pane beside the list, with the detail editable and writing back the way the parent does.
Developer guide › Tree data and master-detail › Master-Detail Rows in a JavaScript Data Grid
Master-detail
A master row expands to reveal a detail region: by default a nested grid over whatever
detail.rows(row) returns, which may be a promise. The grid adds an expander
column while the feature is on, on the same terms as the auto-group and selection columns:
pinned to the start, and absent from columns.visible(), saved views, exports and
the tool panel.
Inline, a detail row beneath its master
detail: {
rows: (row) => api.lines(row.data.id), // array or promise
config: { columns: [{ field: 'port' }, { field: 'vlan' }] },
height: 240,
isMaster: (data) => data.lineCount > 0, // default: every data row
cacheLimit: 10,
}
grid.detail.toggle(key);
grid.detail.keys(); // every open master
grid.detail.closeAll();
The detail is a real display row: virtualised, height-managed, and pushing the rows below it
down. Any number of masters can be open at once. height takes a number or a
function of the row.
A detail pane instead of a detail row
Targeted, the list-and-pane layout
detail: {
target: '#detail-pane', // a selector or an element
rows: (row) => api.lines(row.data.id),
config: { columns: [{ field: 'port' }, { field: 'vlan' }] },
}
grid.detail.active(); // the open master, or null
grid.detail.placement(); // 'inline' | 'target' | null
With target the detail renders into an element you own rather than into a row.
No detail row is created, so the grid's row count does not change when a master opens, and
nothing about the list's geometry moves.
Exactly one master is open at a time in this placement. One element cannot
show two details, and stacking them turns a fixed-height pane into a scrolling list of grids
with no rule for how tall each should be. Expanding a second master closes the first;
height is ignored, because the pane's height is yours.
A target selector that matches no element is reported once and leaves the
details unshown: silence there is indistinguishable from a detail that fails to open, and
the cause is not visible from the grid.
The control changes with the placement, because the gesture does. Inline it
is a chevron that turns down when the row expands, carrying aria-expanded: the
ordinary disclosure pattern. Targeted, nothing expands: the row is being chosen and its
detail appears elsewhere, so the control becomes the “opens elsewhere” glyph and
a toggle (aria-pressed) rather than a disclosure. A chevron there would promise
an expansion that never comes. The chosen row is marked with
lat-row--detail-active and aria-current, since with the detail off
to the side nothing else in the grid says which record the pane belongs to.
An editable detail
The detail is a whole grid, so it edits like one: put edit in
detail.config and its cells are editable. The rows it shows are usually a
sub-array of the master's own record, so an edit there changes the master's data directly;
there is nothing to copy back.
One listener on the master covers every detail
detail: {
rows: (row) => row.data.ports, // a sub-array of the record
config: {
columns: [{ field: 'port' }, { field: 'vlan', type: 'number', edit: true }],
edit: { enabled: true },
},
}
grid.on('detail:cell:changed', (e) => {
e.masterKey; // 'C1', the row the detail belongs to
e.path; // 'ports.1.vlan': where it lands on the master's record
e.value; // 999
e.oldValue; // 101
});
A nested grid is created by the grid, not by you, so its own events would otherwise be out of
reach. The edit lifecycle (detail:edit:started,
detail:edit:stopped, detail:cell:changed) is re-emitted on the
master, tagged with the master it came from. You never have to hold the nested grid to hear
about an edit inside it.
path is the dot notation from the master's record to the value that changed, so
a host can persist a detail edit against the master and never think about the nested grid at
all. It is worked out by identity: rows(row) usually returns an array that is
already a property of the record, and that property is the prefix. A detail fetched from a
server is not part of the master's record, so its path is null,
set detail.path to name it yourself when you want one anyway.
For anything the forwarded events do not cover, detail.onCreate(grid, masterRow)
hands you the nested grid itself as it is built.
| Setting | What it does |
|---|---|
rows(row) | The nested grid's rows. May return a promise; the region is built empty and loaded when it settles, so an empty detail and a pending one do not look the same. |
config | The nested grid's configuration. It inherits the licence and module registry from the master by construction. |
render(container, row) | Draw the region yourself instead of a nested grid. Return anything with a destroy() method. |
isMaster(data, row) | Which rows can expand. Group rows and detail rows never can. |
target | A selector or element to render the detail into. Omit for inline. One master open at a time. |
onCreate(grid, row) | The nested grid, as it is created. |
path | The property of the master's record the detail rows live on, when it cannot be worked out by identity. |
cacheLimit | How many regions are retained after closing, so collapse and re-expand does not refetch. Open regions are never evicted, whatever the limit. 0 destroys on collapse. Default 10. |