Lattice Grid Buy a licence

demo D81

Lazy children

Fetching a branch on expand, with an abort when it closes first

tree.loadChildren

Building…
Loading a live grid…

The configuration

import * as ng from '@angular/core';
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import createLatticeGrid from '@toclocoinc/lattice-grid/modules/angular';

const { LatticeGridComponent } = createLatticeGrid({ ng, createGrid });

const money = { style: 'currency', currency: 'GBP', decimals: 0 };

// Stands in for the server the branch would come from. The delay makes the
// loading state and the abort both visible; the signal is honoured, so a
// branch closed before it lands is aborted and stays openable rather than
// being left empty. Fetched rows join the data set, so they sort, filter and
// export like any other, and a branch is fetched once however often it toggles.
function fetchChildren(node, signal) {
  return new Promise((resolve, reject) => {
    if (signal.aborted) return reject(new DOMException('Aborted', 'AbortError'));
    const leaf = node.kind === 'Site';
    const timer = setTimeout(() => {
      signal.removeEventListener('abort', onAbort);
      resolve(Array.from({ length: node.childCount }, (_, i) => ({
        id: `${node.id}-${i}`,
        parentId: node.id,
        name: leaf ? `ETH-${i}` : `Site ${100 + i}`,
        kind: leaf ? 'Circuit' : 'Site',
        childCount: leaf ? 0 : 3,
        devices: leaf ? 2 : undefined,
        charge: leaf ? 800 : undefined,
      })));
    }, 700);
    function onAbort() { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); }
    signal.addEventListener('abort', onAbort, { once: true });
  });
}

const columns = [
  { field: 'kind', title: 'Level', filter: { type: 'set' }, layout: { width: 120 } },
  { field: 'childCount', title: 'Children', type: 'number', layout: { width: 120 } },
  { field: 'devices', title: 'Devices', type: 'number', total: 'sum', layout: { width: 120 } },
  { field: 'charge', title: 'Monthly charge', type: 'number', format: money, total: 'sum', layout: { width: 180 } },
];

// Eight roots, nothing below them until a branch is opened. Each root names a
// childCount but holds no children yet.
const rows = [/* region roots, each { id, parentId: null, name, kind: 'Region', childCount } */];

const tree = {
  parentKey: 'parentId',
  label: 'name',
  title: 'Region',
  // Without this the roots would have no expander at all: they hold no
  // children, so there would be no gesture left to fetch any. A node that
  // declares children reads as closed until they arrive.
  hasChildren: (data) => data.childCount > 0,
  loadChildren: (row, signal) => fetchChildren(row.data, signal),
};



@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent],
  template: '<lattice-grid [config]="grid" style="display:block;height:540px"></lattice-grid>',
})
export class AppComponent {
  grid = {
    rowKey: 'id',
    selection: 'multiple',
    tree: tree,
    toolPanel: { side: 'left', panels: ['columns', 'filters', 'views', 'quick'],
          actions: ['undo', 'redo', 'export', 'restore', 'maximise'], exportName: 'lattice-demo' },
    columns: columns,
    rows: rows,
  };
}

bootstrapApplication(AppComponent);

Fetching tree branches on demand

Lazy children load a branch of a tree only when a row is expanded, rather than pulling the whole hierarchy up front. A developer reaches for this when the source is a remote API rather than an in-memory array: an org chart where each manager’s direct reports live behind a separate endpoint, a file browser where a folder’s contents are unknown until it is opened, a category tree too large to fetch in one request. Lattice Grid calls tree.loadChildren for the row being expanded, passing its id, and expects a promise resolving to that row’s children; the branch renders once the promise settles, and a distinct loading state occupies the row in between so the grid never presents an empty branch as an empty result. Closing a branch before its fetch resolves aborts the in-flight request through the same signal loadChildren receives, so a fast double-click through several folders does not leave stale fetches racing to populate rows the user has already collapsed. Because this is a JavaScript data grid built around incremental rendering, only the expanded branch pays the network cost; sibling rows and the rest of the tree keep their existing state untouched. The loading row exposes aria-busy while a fetch is pending, so a screen reader announces that a branch is populating rather than reporting silence.

How do I load tree children from an API only when a row expands?

Implement tree.loadChildren, returning a promise that resolves to the array of child rows for the expanded row’s id. Lattice Grid calls it on expand, shows a loading row until the promise resolves, and aborts the request if the branch is collapsed again before the fetch completes, so no children are rendered for a branch the user has already closed.