Lattice Grid Buy a licence

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 guideTree 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.

SettingWhat 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.
configThe 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.
targetA 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.
pathThe property of the master's record the detail rows live on, when it cannot be worked out by identity.
cacheLimitHow 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.