demo D28
Lookups in htmx
Store an id, show a label, sort on the label and edit by picking one
lookup: { options }
This grid stores a compact code in each row but shows a readable label, sorts by that label, and lets a user edit by picking one. It is the pattern behind status fields and any foreign key, so the data stays clean while the display stays legible.
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="lookups-table" data-lattice-grid>
<thead>
<tr>
<th data-field="hostname">Hostname</th>
<th data-field="site">Stored</th>
<th data-field="managed">Managed</th>
<th data-field="ports">Ports</th>
<th data-field="commissioned">Commissioned</th>
</tr>
</thead>
<tbody>
<tr data-id="1"><td>peering-0001.london.example.net</td><td>3</td><td>true</td><td>8</td><td>2020-03-01</td></tr>
<tr data-id="2"><td>leaf-0002.london.example.net</td><td>3</td><td>true</td><td>12</td><td>2020-09-27</td></tr>
<tr data-id="3"><td>peering-0003.zurich.example.net</td><td>6</td><td>true</td><td>8</td><td>2025-04-05</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';
// Each row stores a compact id; the grid shows a friendly label for it, and a
// value with no match reads as the fallback rather than as a bare code.
const SITES = [
{ id: 1, label: 'London', group: 'South' },
{ id: 2, label: 'Bristol', group: 'South' },
{ id: 3, label: 'Leeds', group: 'North' },
{ id: 4, label: 'Glasgow', group: 'North' },
{ id: 5, label: 'Cardiff', group: 'West', disabled: true },
];
// 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('lookups-table'), {
toolPanel: { side: 'left', panels: ['columns', 'filters', 'views', 'quick'], actions: ['restore'], exportName: 'lattice-demo' },
columns: [
{ field: 'hostname', title: 'Hostname', layout: { width: 280, pin: 'start' } },
// The same field with no lookup, so the integer the row actually holds is
// visible beside the label it is shown as.
{ field: 'site', id: 'stored', title: 'Stored', type: 'number', layout: { width: 110 } },
// Sorting is on the label by default, which is what someone reading the
// column expects.
{ field: 'site', title: 'Site', type: 'lookup', layout: { width: 200 },
lookup: { options: SITES, unknownLabel: 'decommissioned', sortBy: 'label' } },
// The same lookup, sorted on the stored id instead.
{ field: 'site', id: 'byValue', title: 'Site, sorted by id', type: 'lookup', layout: { width: 200 },
lookup: { options: SITES, unknownLabel: 'decommissioned', sortBy: 'value' } },
// groupKey sections a long list in the select and the set filter. A
// disabled option still reads on rows that hold it, and cannot be chosen
// for new ones.
{ field: 'site', id: 'grouped', title: 'Site, grouped options', type: 'lookup', layout: { width: 200 },
lookup: { options: SITES, unknownLabel: 'decommissioned', groupKey: 'group' } },
{ field: 'managed', title: 'Managed', type: 'boolean', layout: { width: 120 } },
{ field: 'ports', title: 'Ports', type: 'number', layout: { width: 110 } },
{ field: 'commissioned', title: 'Commissioned', type: 'dateString', layout: { width: 160 } },
],
state: { sort: [{ col: 'site', dir: 'asc' }] },
});
</script>
Mapping stored codes to display labels with lookup columns
A lookup column stores a compact code in each row but shows a reader something legible, and sorts by that label rather than the underlying code. This is the pattern behind status fields, currency codes and any foreign key drawn from a small fixed list, where the raw value on the row is an id such as 2 or "ACT" and the value a reader needs is “Active” or “Pending review”. Lattice Grid handles this with lookup: { options } on the column definition: options supplies the code-to-label pairs once, at column setup, and every cell resolves against that same map rather than repeating the label in the row data. Sorting and filtering operate on the resolved label by default, so a status column orders “Active” before “Pending” as a reader expects, not by the numeric order of the codes.
A developer reaches for lookup instead of a plain text column whenever the stored value and the displayed value diverge, keeping row data small while the grid does the translation at render time. The map builds once from options rather than being looked up per cell, so resolving a label costs a single property read, and a lookup column in a large JavaScript data grid carries no more per-row overhead than a plain text cell.
A lookup column edits the same way it reads. Add edit: { enabled: true, editor: 'select' } and the cell opens a picker over the same options: a reader chooses the label they see, and the grid writes back the underlying id, so the stored value stays compact while the person editing never has to know the code behind it.
How do you show a label for a coded value in a JavaScript data grid?
Set type: 'lookup' on the column and pass lookup: { options }, where options maps each stored code to its display label. Lattice Grid renders the label in the cell and sorts by the label rather than the code, so a status or category column reads naturally without changing what is stored in the row.