demo D76
Orphans and cycles in htmx
Data that is not quite a tree. Nothing is dropped
orphans: 'Unassigned'
This grid handles hierarchy data that is not quite a tree: parent references that point nowhere, and chains that loop back on themselves. It keeps every row visible and accounted for, gathering the strays under a named group rather than silently dropping them.
This is the htmx version. A server-rendered table is upgraded in place, so the page is readable before any script runs and gains the full grid when the module loads, which suits a progressively enhanced app.
The configuration
<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">
<!-- A real, server-rendered table: readable with JavaScript off. data-field on
each header names the column that cell becomes, and data-id on each row is
the key the grid carries it under. -->
<table id="orphans-and-cycles-table" data-lattice-grid>
<thead>
<tr>
<th data-field="source">Source</th>
<th data-field="parentId">Names as parent</th>
<th data-field="charge">Monthly charge</th>
</tr>
</thead>
<tbody>
<tr data-id="R-North"><td>inventory</td><td></td><td></td></tr>
<tr data-id="R-North-C0"><td>inventory</td><td>R-North</td><td>854</td></tr>
<tr data-id="R-North-C1"><td>inventory</td><td>R-North</td><td>1147</td></tr>
<!-- the rest of the rows, rendered by your server -->
</tbody>
</table>
<script type="module">
import { hydrateTable } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/htmx.esm.min.js';
// The markup carries the rows; a server-rendered table's column types are
// declared in the config hydrateTable takes. The grid replaces the table
// where it stood.
const grid = hydrateTable(document.getElementById('orphans-and-cycles-table'), {
selection: 'multiple',
grandTotalRow: 'bottom',
toolPanel: {
side: 'left',
panels: ['columns', 'filters', 'views', 'quick'],
actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
exportName: 'lattice-demo',
},
// Rows naming a parent that is not in the extract go to the named bucket,
// and a cycle is cut and its rows shown at the root, with one console
// warning rather than one per row. No row is lost either way.
tree: { parentKey: 'parentId', orphans: 'Unassigned', label: 'name', title: 'Circuit' },
columns: [
{ field: 'source', title: 'Source', filter: { type: 'set' }, layout: { width: 160 } },
{ field: 'parentId', title: 'Names as parent', layout: { width: 220 } },
{ field: 'charge', title: 'Monthly charge', type: 'number', format: { style: 'currency', currency: 'GBP', decimals: 0 }, total: 'sum', layout: { width: 180 } },
],
});
</script>
Handling broken hierarchies: missing parents and cyclic references
Real hierarchy data is rarely a clean tree. A parent ID can point at a row that was filtered out upstream, deleted from the source system, or never existed in the first place, and occasionally a chain of parent references loops back on itself. A developer reaches for this when building tree data from a flat table sourced from a database join or a CSV export, where referential integrity was never guaranteed. Lattice Grid handles both cases without dropping rows: set orphans: 'Unassigned' and any row whose declared parent cannot be found is regrouped under a synthetic bucket with that label, rather than being silently excluded from the grid or, worse, from a total. Cycles are detected during tree construction, so a row that would otherwise reference its own descendant as a parent is broken out into the same fallback grouping instead of causing infinite recursion. Because this is a JavaScript data grid built for incremental updates, the orphan bucket recalculates only for the rows affected by a change, not the whole tree. This matters for accessibility as well as correctness: a screen reader announcing group counts should never encounter a total that silently excludes rows the user cannot otherwise account for.
What happens to rows with a missing or invalid parent ID in a tree grid?
Lattice Grid groups them under a fallback node rather than discarding them. Setting orphans: 'Unassigned' on the tree configuration names that node, so every row whose parent reference cannot be resolved, including rows caught in a circular reference, still appears in the grid, still counts toward totals, and remains visible to filtering and search.