Lattice Grid Buy a licence

demo D27

JSON and structured values in htmx

A nested object in a cell, summarised inline and expandable

type: 'json'

This grid holds a nested object in a single cell, summarised inline and expandable to see the full structure. It lets you keep source data in its natural shape, an address or a permissions map, without flattening it into extra columns first.

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="json-values-table" data-lattice-grid>
  <thead>
    <tr>
      <th data-field="hostname">Hostname</th>
      <th data-field="config">Interfaces</th>
      <th data-field="hardware">Hardware</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="1"><td>peering-0001.london.example.net</td><td>{"role":"peering","bgp":{"asn":65280,"peers":5,"md5":false},"snmp":{"version":"2c","community":"redacted"},"interfaces":[{"name":"xe-0/0/0","mtu":1500}]}</td><td>{"vendor":"Cisco","model":"NCS-5501","slots":4}</td></tr>
    <tr data-id="2"><td>leaf-0002.london.example.net</td><td>{"role":"leaf","bgp":{"asn":65175,"peers":3,"md5":true},"snmp":{"version":"3","community":"redacted"},"interfaces":[{"name":"xe-0/0/0","mtu":9000},{"name":"xe-0/0/1","mtu":1500},{"name":"xe-0/0/2","mtu":1500}]}</td><td>{"vendor":"Juniper","model":"MX204","slots":8}</td></tr>
    <tr data-id="3"><td>peering-0003.zurich.example.net</td><td>{"role":"peering","bgp":{"asn":65286,"peers":6,"md5":true},"snmp":{"version":"3","community":"redacted"},"interfaces":[{"name":"xe-0/0/0","mtu":1500},{"name":"xe-0/0/1","mtu":9000}]}</td><td>{"vendor":"Cisco","model":"NCS-5501","slots":4}</td></tr>
    <!-- the rest of the rows, rendered by your server -->
  </tbody>
</table>

<script type="module">
  import { readTable, hydrateTable } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/htmx.esm.min.js';

  // A table cell carries text, so the columns whose values are whole objects
  // travel as JSON and are read back here. Everything else
  // arrives as the number, boolean or string it already looks like.
  const rows = readTable(document.getElementById('json-values-table')).rows.map((row) => ({
    ...row,
    config: JSON.parse(row.config),
    hardware: JSON.parse(row.hardware),
  }));

  // 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('json-values-table'), {
    toolPanel: {
      side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'lattice-demo',
    },
    columns: [
      { field: 'hostname', title: 'Hostname', layout: { width: 260, pin: 'start' } },
      {
        field: 'config', title: 'Config', type: 'json', layout: { flex: 1, min: 260 },
        // Short objects print in full; anything longer collapses to a count,
        // because a cell that wraps six lines of JSON is not a cell. The
        // full document is on the tooltip and in the editor.
        cell: {
          props: { inlineLimit: 44 },
          tooltip: (p) => {
            try { return JSON.stringify(p.value, null, 2); } catch { return ''; }
          },
        },
      },
      {
        field: 'config', id: 'interfaces', title: 'Interfaces', type: 'json', layout: { width: 200 },
        value: { deps: ['config'], pure: true, compute: (d) => d.config?.interfaces ?? null },
      },
      {
        id: 'asn', title: 'BGP ASN', type: 'number', layout: { width: 130 },
        // Pulling a leaf out of the document gives you a real number column:
        // it sorts, it filters as a range, it totals. The object stays whole
        // underneath.
        value: { deps: ['config'], pure: true, compute: (d) => d.config?.bgp?.asn ?? null },
        filter: { type: 'number' },
      },
      {
        id: 'mtu', title: 'Max MTU', type: 'number', layout: { width: 130 },
        value: {
          deps: ['config'], pure: true,
          compute: (d) => {
            const list = d.config?.interfaces ?? [];
            return list.length ? Math.max(...list.map((i) => i.mtu)) : null;
          },
        },
      },
      {
        id: 'snmp', title: 'SNMP', layout: { width: 110 },
        value: { deps: ['config'], pure: true, compute: (d) => d.config?.snmp?.version ?? null },
        filter: { type: 'set' },
      },
      { field: 'hardware', title: 'Hardware', type: 'json', layout: { width: 240 } },
    ],
    rows,
  });
</script>

Rendering nested JSON in a grid cell

Source data is not always flat. An order row might carry a shipping address as a nested object, or a user record might embed a permissions map, and a JavaScript data grid that only accepts scalars forces a flattening step before the data can be displayed at all. Lattice Grid’s type: 'json' column renders that structure directly: the cell shows a compact inline summary of the object, and expands on demand to a formatted, indented view of the full value, so nesting depth is never lost to a stringified blob. A developer reaches for this type wherever an API response is stored as-is rather than normalised into separate columns, which keeps the column definition stable even when the shape of the nested data grows a new key.

Because the summary is generated from the object rather than pre-rendered per row, a json column pays no layout cost until a cell is actually expanded: collapsed rows contribute a single-line label to the row height calculation, so a grid holding thousands of records with deeply nested payloads scrolls at the same frame rate as one holding plain strings. Expansion is keyboard-operable, so the nested view is reachable without a pointer, which matters for any grid built to pass an accessibility audit rather than merely look right under a mouse.

How do you display a JSON object in a data grid cell?

Set the column’s type to 'json'. Lattice Grid shows a condensed one-line summary of the object in the collapsed cell and an indented, formatted view when the cell is expanded, so nested values stay inspectable without a custom renderer or a separate detail panel.