Lattice Grid Buy a licence

developer guide

Tree data and master-detail: JavaScript Data Grid Guide

A hierarchy from a parent reference or a path, orphans and cycles absorbed rather than dropped, lazy children, and rows that expand into a nested grid.

Developer guide › Tree data and master-detail

Tree data

Rows that sit under one another rather than in a flat list. Two shapes, and they answer different questions about where the hierarchy lives.

The row names its parent

tree: {
  parentKey: 'parentId',        // a field, or a function of the row
  label: 'name',                // what the tree column shows
  orphans: 'Unassigned',        // or 'root', the default
}

The row carries its own ancestry

tree: { path: (row) => row.hierarchy }   // ['EMEA', 'UK', 'Colchester']

Parent-reference is what a join or a document store produces. Every node is a real row. A row whose parent is not in the data (filtered away, not loaded, or simply wrong) is an orphan: it goes to the root by default, or into a named bucket. It is never dropped, because hiding a record over a bad reference loses data the user can see in a flat view.

Path-based rows describe their own place, so intermediate levels may have no row at all: EMEA/UK/Colchester with no EMEA/UK row still needs a UK node to sit under. Those are synthesised, and render as group rows: a heading over the rows beneath it with no record of its own. A real row arriving later for a level already synthesised fills that node rather than appearing beside it.

A parent cycle is not a hierarchy and cannot be walked. It is reported once and cut, with the rows shown at the root: wrong place beats vanished.

The grid generates a tree column to carry the expander and the indent, on the same terms as the auto-group, selection and detail columns: pinned to the start, and absent from columns.visible(), saved views, exports and the tool panel. Its text comes from tree.label; without one it falls back to your first visible column, which then appears twice until you hide it, the grid does not remove a column you did not ask it to remove.

Loading a branch on demand

Children fetched when the node is opened

tree: {
  parentKey: 'parentId',
  hasChildren: (data) => data.childCount > 0,
  loadChildren: (row, signal) => api.children(row.data.id, { signal }),
}

hasChildren lets a row declare children it does not hold, so the expander is there before anything is fetched: without it there is nothing to click and the branch can never load. Such a node reads as closed even though tree nodes are otherwise expanded by default, because an open branch with nothing under it leaves no gesture to load it.

The rows that arrive are added to the data set, so the hierarchy rebuilds through the same pipeline as everything else and the new rows sort, filter and export like any other. A branch is fetched once however often it is toggled; closing it before the rows arrive aborts the request through the signal. A rejection is reported and leaves the branch unloaded, so reopening tries again rather than showing an empty node for good. tree:loading, tree:loaded and tree:loadFailed are on the event bus.

Expansion is the same state group expansion uses, so rows.expand, rows.collapse, expandAll, collapseAll and saved views all work on it. A collapsed branch is skipped rather than hidden, so it costs nothing. Grouping and tree together is not a combination: the grouping wins and says so once, because two expanders in one row would be two hierarchies claiming the same rows.

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.