Lattice Grid Buy a licence

demo D88

The editor gallery in htmx

Twenty-two editors in one grid: date, duration, colour, rating, slider

editors/*

This grid puts twenty-two editors side by side in one grid: text, number, date, duration, colour, rating, slider, lookup and more, each wired to the column type it belongs to. It shows the breadth of built-in editing, so most columns edit correctly without a custom control.

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="editor-gallery-table" data-lattice-grid>
  <thead>
    <tr>
      <th data-field="id">Order</th>
      <th data-field="reference">text</th>
      <th data-field="notes">textarea</th>
      <th data-field="budget">number</th>
      <th data-field="due">date</th>
      <th data-field="signedOff">checkbox</th>
      <th data-field="stage">select</th>
      <th data-field="tags">multiSelect</th>
      <th data-field="window">time</th>
      <th data-field="starts">datetime</th>
      <th data-field="elapsed">duration</th>
      <th data-field="host">ipaddress</th>
      <th data-field="token">password</th>
      <th data-field="payload">code</th>
      <th data-field="transfer">unit</th>
      <th data-field="vlanMask">radix</th>
      <th data-field="effort">slider</th>
      <th data-field="impact">rating</th>
      <th data-field="priority">segmented</th>
      <th data-field="team">treeSelect</th>
      <th data-field="owner">objectPicker</th>
      <th data-field="icon">iconPicker</th>
      <th data-field="accent">colour</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="WO-00001"><td>WO-00001</td><td>REQ-9090</td><td>Customer asked for an evening window.</td><td>8580</td><td>2026-01-01</td><td>true</td><td>done</td><td>["fibre"]</td><td>21600000</td><td>2026-01-01T06:00</td><td>18900000</td><td>167896094</td><td>svc_3sg2m9rv</td><td>{"retries":1,"notify":true}</td><td>782702283491</td><td>65309</td><td>5</td><td>3</td><td>low</td><td>central.birmingham</td><td>u2</td><td>search</td><td>#2f9e44</td></tr>
    <tr data-id="WO-00002"><td>WO-00002</td><td>INC-7104</td><td>Second engineer required for the roof section.</td><td>23160</td><td>2026-02-02</td><td>true</td><td>done</td><td>["fibre","copper"]</td><td>26100000</td><td>2026-02-02T07:00</td><td>14220000</td><td>167955202</td><td>svc_bo97py86</td><td>{"retries":4,"notify":true}</td><td>668650328647</td><td>16083</td><td>70</td><td>4</td><td>normal</td><td>south.london</td><td>u3</td><td>search</td><td>#0b7285</td></tr>
    <tr data-id="WO-00003"><td>WO-00003</td><td>REQ-7362</td><td>Awaiting a wayleave from the landlord.</td><td>17685</td><td>2026-03-03</td><td>true</td><td>blocked</td><td>["edge"]</td><td>30600000</td><td>2026-03-03T08:00</td><td>37200000</td><td>168030190</td><td>svc_86yk31g2</td><td>{"retries":1,"notify":false}</td><td>697159621305</td><td>15749</td><td>55</td><td>3</td><td>high</td><td>south.london</td><td>u3</td><td>menu</td><td>#e8590c</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('editor-gallery-table')).rows.map((row) => ({
    ...row,
    tags: JSON.parse(row.tags),
    payload: JSON.parse(row.payload),
  }));

  // 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('editor-gallery-table'), {
    selection: { mode: 'multiple', ranges: true },
    edit: true,
    toolPanel: { side: 'left', panels: ['columns', 'filters'], actions: ['restore'], exportName: 'editors' },
    columns: [
      { field: 'id', title: 'Order', layout: { width: 120, pin: 'start' } },
      { field: 'reference', title: 'text', layout: { width: 130 }, edit: { enabled: true, editor: 'text' } },
      { field: 'notes', title: 'textarea', layout: { width: 220 }, edit: { enabled: true, editor: 'textarea', props: { rows: 4, submitOn: 'ctrlEnter' } } },
      { field: 'budget', title: 'number', type: 'number', layout: { width: 120 }, edit: { enabled: true, editor: 'number', props: { min: 0, max: 100000, step: 500, spinner: true } } },
      { field: 'due', title: 'date', type: 'date', layout: { width: 140 }, edit: { enabled: true, editor: 'date' } },
      { field: 'signedOff', title: 'checkbox', type: 'boolean', layout: { width: 110 }, edit: { enabled: true, editor: 'checkbox' } },
      { field: 'stage', title: 'select', layout: { width: 140 }, lookup: { options: [{ id: 'planned', label: 'Planned' }, { id: 'approved', label: 'Approved' }, { id: 'in-flight', label: 'In-flight' }, { id: 'blocked', label: 'Blocked' }, { id: 'done', label: 'Done' }] }, edit: { enabled: true, editor: 'select', props: { allowClear: true } } },
      { field: 'tags', title: 'multiSelect', layout: { width: 190 }, lookup: { options: [{ id: 'fibre', label: 'fibre' }, { id: 'copper', label: 'copper' }, { id: 'wireless', label: 'wireless' }, { id: 'core', label: 'core' }, { id: 'edge', label: 'edge' }, { id: 'peering', label: 'peering' }], multiple: true }, edit: { enabled: true, editor: 'multiSelect', props: { selectAll: true, chipLimit: 2 } } },
      { field: 'window', title: 'time', type: 'time', layout: { width: 110 }, edit: { enabled: true, editor: 'time', props: { step: 15 } } },
      { field: 'starts', title: 'datetime', type: 'datetime', layout: { width: 180 }, edit: { enabled: true, editor: 'datetime' } },
      { field: 'elapsed', title: 'duration', type: 'duration', layout: { width: 130 }, edit: { enabled: true, editor: 'duration' } },
      { field: 'host', title: 'ipaddress', type: 'ipv4', layout: { width: 150 }, edit: { enabled: true, editor: 'ipaddress' } },
      { field: 'token', title: 'password', type: 'secret', layout: { width: 150 }, edit: { enabled: true, editor: 'password', props: { revealable: true } } },
      { field: 'payload', title: 'code', type: 'json', layout: { width: 220 }, edit: { enabled: true, editor: 'code', props: { language: 'json', rows: 8 } } },
      { field: 'transfer', title: 'unit', type: 'bytes', layout: { width: 130 }, edit: { enabled: true, editor: 'unit' } },
      { field: 'vlanMask', title: 'radix', type: 'hex16', layout: { width: 120 }, edit: { enabled: true, editor: 'radix' } },
      { field: 'effort', title: 'slider', type: 'number', layout: { width: 150 }, edit: { enabled: true, editor: 'slider', props: { min: 0, max: 100, step: 5, marks: [0, 50, 100] } } },
      { field: 'impact', title: 'rating', type: 'number', layout: { width: 130 }, edit: { enabled: true, editor: 'rating', props: { max: 5, allowClear: true } } },
      { field: 'priority', title: 'segmented', layout: { width: 170 }, lookup: { options: [{ id: 'low', label: 'Low' }, { id: 'normal', label: 'Normal' }, { id: 'high', label: 'High' }] }, edit: { enabled: true, editor: 'segmented' } },
      { field: 'team', title: 'treeSelect', layout: { width: 170 }, lookup: { options: [
        { id: 'north.leeds', label: 'Leeds', path: ['North', 'Leeds'] },
        { id: 'north.newcastle', label: 'Newcastle', path: ['North', 'Newcastle'] },
        { id: 'north.glasgow', label: 'Glasgow', path: ['North', 'Glasgow'] },
        { id: 'south.london', label: 'London', path: ['South', 'London'] },
        { id: 'south.bristol', label: 'Bristol', path: ['South', 'Bristol'] },
        { id: 'south.cardiff', label: 'Cardiff', path: ['South', 'Cardiff'] },
        { id: 'central.birmingham', label: 'Birmingham', path: ['Central', 'Birmingham'] },
        { id: 'central.manchester', label: 'Manchester', path: ['Central', 'Manchester'] },
      ] }, edit: { enabled: true, editor: 'treeSelect', props: { defaultExpanded: true } } },
      { field: 'owner', title: 'objectPicker', layout: { width: 180 }, lookup: { options: [
        { id: 'u1', label: 'Priya Raman', role: 'Network engineering' },
        { id: 'u2', label: 'Tom Ellery', role: 'Network engineering' },
        { id: 'u3', label: 'Sara Whitlock', role: 'Field operations' },
        { id: 'u4', label: 'Dan Osei', role: 'Field operations' },
        { id: 'u5', label: 'Marta Kowal', role: 'Capacity planning' },
        { id: 'u6', label: 'Joe Brennan', role: 'Capacity planning' },
      ] }, edit: { enabled: true, editor: 'objectPicker', props: { secondary: 'role' } } },
      { field: 'icon', title: 'iconPicker', layout: { width: 140 }, edit: { enabled: true, editor: 'iconPicker', props: { columns: 8 } } },
      { field: 'accent', title: 'colour', layout: { width: 130 }, edit: { enabled: true, editor: 'colour', props: { swatches: ['#1a6bc7', '#2f9e44', '#e8590c', '#c92a2a', '#7048e8', '#0b7285'] } } },
    ],
    rows,
  });
</script>

Choosing the right cell editor for each column type

The editor gallery is a single grid carrying twenty-two editors side by side: text, number, date, duration, colour, rating, slider, lookup and more, each wired to the column type it belongs to rather than bolted on as an afterthought. A developer reaches for a specific editor whenever the default text input would misrepresent the data, a duration column edited as free text invites a typo a stepper would prevent, and a colour column benefits from a swatch picker rather than a hex string typed by hand. Lattice Grid selects the editor through the editors/* configuration, which maps a column’s declared type to its editing widget, so setting a column to type: 'date' or type: 'duration' brings its editor along with it instead of requiring a second registration step. Each editor opens on the same double-click, Enter or type-to-start gesture regardless of which one it is, so the interaction model stays constant even as the widget underneath changes from a calendar to a slider track. As a JavaScript data grid built for wide, densely typed tables, the gallery also keeps every editor keyboard-operable: a slider responds to arrow keys in fixed increments, a rating editor accepts a typed digit as well as a click, and Tab commits the current cell and advances focus without requiring a pointer at any point in the sequence.

What editor does Lattice Grid use for a date or duration column?

Lattice Grid picks the editor from the column’s type: a date column opens a calendar editor, a duration column opens a stepper for hours and minutes, and so on through the twenty types in the editors/* set. The mapping is automatic once the column type is declared, so no separate editor needs to be registered for standard types.