Lattice Grid Buy a licence

demo D78

Master-detail in htmx

A row expanding into a nested grid, several open at once

detail: { rows, config }

This grid expands a row into its own nested grid, an order onto its line items, with several detail views open at once. It shows a parent and its related records together, each detail grid with its own columns, sorting and totals.

Building…
Loading a live grid…

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="master-detail-table" data-lattice-grid>
  <thead>
    <tr>
      <th data-field="circuit">Circuit</th>
      <th data-field="customer">Customer</th>
      <th data-field="region">Region</th>
      <th data-field="portCount">Ports</th>
      <th data-field="charge">Monthly charge</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="C0"><td>ETH-00000</td><td>Verity Bank</td><td>North</td><td>6</td><td>1197</td></tr>
    <tr data-id="C1"><td>ETH-00001</td><td>Northgate Care</td><td>Midlands</td><td>2</td><td>509</td></tr>
    <tr data-id="C2"><td>ETH-00002</td><td>Tamar Energy</td><td>South East</td><td>2</td><td>3444</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('master-detail-table'), {
    selection: 'multiple',
    toolPanel: {
      side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'lattice-demo',
    },
    // Each row opens a nested grid of its own ports, drawn inline beneath it.
    // cacheLimit bounds how many closed details are kept ready, not how many may
    // be open: expand as many as you like and they all stay open.
    detail: {
      rows: (row) => row.data.ports,
      height: 260,
      cacheLimit: 10,
      config: {
        rowKey: 'port',
        columns: [
          { field: 'port', title: 'Port', layout: { width: 140 } },
          { field: 'vlan', title: 'VLAN', type: 'number', layout: { width: 100 } },
          { field: 'speed', title: 'Speed', type: 'number', format: { suffix: ' Gb' }, layout: { width: 110 } },
          { field: 'state', title: 'State', layout: { width: 130 },
            cell: { decoration: 'dot', variant: { map: { up: 'success', down: 'danger', 'admin down': 'neutral' } } } },
          { field: 'peer', title: 'Peer AS' },
        ],
      },
    },
    columns: [
      { field: 'circuit', title: 'Circuit', layout: { width: 160 } },
      { field: 'customer', title: 'Customer', filter: { type: 'set' } },
      { field: 'region', title: 'Region', filter: { type: 'set' } },
      { field: 'status', title: 'Status', filter: { type: 'set' }, layout: { width: 140 },
        cell: { decoration: 'pill', variant: { map: { live: 'success', provisioning: 'info', ceased: 'neutral' } } } },
      { field: 'portCount', title: 'Ports', type: 'number', layout: { width: 100 } },
      { field: 'charge', title: 'Monthly charge', type: 'number',
        format: { style: 'currency', currency: 'GBP', decimals: 0 }, total: 'sum', layout: { width: 180 } },
    ],
  });
</script>

Nesting a grid inside a row with master-detail

Master-detail expands a row into its own grid rather than a flat sub-list: an order row opens onto its line items, a shipment row onto its parcels, each with its own columns, sorting and totals. A developer reaches for it when the child records do not fit as extra columns on the parent and are too structured for a plain text summary, and when several of those child grids might need to stay open at once for comparison. Lattice Grid controls this per row through the detail: { rows, config } option, passing the child rows and a full grid configuration for the detail, so the nested grid is not a cut-down preview but a complete instance with its own column definitions. Because this is a JavaScript data grid built on incremental rendering, opening a detail row inserts only that row’s grid into the layout rather than recalculating the rows around it, so several master rows can sit expanded together without the outer grid’s scroll position or row heights being recomputed from scratch. The detail area also participates in keyboard navigation, so focus moves from a master row into its expanded grid and back out again without a pointer. See the step-by-step how to add an expandable detail row guide for a runnable example and the source.

How do I show a nested grid when a row is expanded?

Set detail: { rows, config } on the row: rows supplies the child records and config is a standard grid configuration describing their columns. Lattice Grid renders that configuration inside the expanded row as an independent grid, and any number of master rows can be expanded at the same time, each keeping its own detail state.