Lattice Grid Buy a licence

demo D145

The row types in htmx

Leaf, group, footer, grand total, detail, synthesised and pinned, in one grid

Row.group · detail · grandTotal

This grid shows all the row kinds in one place: leaf, group, footer, grand total, detail, synthesised and pinned. It shows that a dataset rarely fits one row shape, and how the grid draws each kind so a hierarchy, its totals and its detail all read together.

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="row-types-table" data-lattice-grid>
  <thead>
    <tr>
      <th data-field="account">Account</th>
      <th data-field="unit">Unit</th>
      <th data-field="service">Service</th>
      <th data-field="region">Region</th>
      <th data-field="cost">Monthly cost</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="alloc-0"><td>platform-stage</td><td>Network</td><td>metrics</td><td>eu-west-2</td><td>1103.5</td></tr>
    <tr data-id="alloc-1"><td>platform-stage</td><td>Observability</td><td>traces</td><td>eu-west-2</td><td>1354</td></tr>
    <tr data-id="alloc-2"><td>platform-prod</td><td>Compute</td><td>metrics</td><td>eu-west-1</td><td>1665.5</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('row-types-table'), {
    selection: 'multiple',
    toolPanel: {
      side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'lattice-demo',
    },
    // One table holds several kinds of row at once: group headers, leaf rows, a
    // footer under each group and a grand total, plus a detail row per leaf.
    groupFooter: true,
    grandTotalRow: true,
    columns: [
      { field: 'account', title: 'Account', filter: { type: 'set' }, group: { enabled: true, index: 0 } },
      { field: 'unit', title: 'Unit', filter: { type: 'set' }, layout: { width: 150 } },
      { field: 'service', title: 'Service', filter: { type: 'set' }, layout: { width: 150 } },
      { field: 'region', title: 'Region', filter: { type: 'set' }, layout: { width: 140 } },
      { field: 'cost', title: 'Monthly cost', type: 'number',
        format: { style: 'currency', currency: 'USD', decimals: 2 }, total: 'sum', layout: { width: 180 } },
    ],
    detail: {
      rows: (row) => row.data.charges,
      height: 160,
      config: {
        rowKey: 'line',
        columns: [
          { field: 'line', title: 'Charge line', layout: { width: 200 } },
          { field: 'quantity', title: 'Qty', type: 'number', layout: { width: 100 } },
          { field: 'amount', title: 'Amount', type: 'number',
            format: { style: 'currency', currency: 'USD', decimals: 2 }, total: 'sum' },
        ],
      },
    },
  });

  // Open the first leaf's detail row on arrival, so the detail kind is visible
  // without a click.
  for (let i = 0; i < grid.rows.count(); i++) {
    const row = grid.rows.get(i);
    if (row && !row.group && !row.detail) { grid.detail.open(row.key); break; }
  }
</script>

A single dataset rarely fits one row shape. Leaf rows carry the raw records, group rows summarise the leaves beneath them, footer and grand total rows roll figures up to a subtotal or a final sum, detail rows expand a leaf into a nested view, and synthesised rows exist only in the grid to carry a computed value that has no backing record. Pinned rows sit outside the scrollable body entirely, fixed above or below it. A developer reaches for this mix when a report needs subtotals per group alongside a running total, or when a master row needs an inline detail panel without navigating away. Row.group, Row.detail and Row.grandTotal mark a row’s kind so the grid can render, sort and export each one according to its role rather than treating every row as an equal leaf. Because row kind is a property of the row rather than a separate rendering path, virtual scrolling treats leaf, group and footer rows as one continuous list, so scroll position and row recycling work the same regardless of how many kinds are mixed into the current view. Each row kind keeps its correct role and aria-level in the accessibility tree, so a screen reader announces a group heading as a heading and a leaf as a data row, not as indistinguishable table cells.

How do you show subtotals and a grand total in a JavaScript data grid?

Mark the rows that should aggregate using Row.group for a per-group subtotal and Row.grandTotal for the overall figure, and the grid computes and renders those values from the leaf rows beneath them. Pinned rows can hold a grand total at the top or bottom of the viewport so it stays visible while the body scrolls.