demo D92
Undo and redo in htmx
A history stack over edits, fills and pastes
grid.history
This grid keeps a history stack across edits, fills and pastes, so a user can step back out of a mistake and forward again. It gives direct manipulation a safety net, so people edit confidently without reloading data to recover.
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="undo-and-redo-table" data-lattice-grid>
<thead>
<tr>
<th data-field="id">Order</th>
<th data-field="reference">Reference</th>
<th data-field="team">Team</th>
<th data-field="stage">Stage</th>
<th data-field="due">Due</th>
<th data-field="circuits">Circuits</th>
<th data-field="impact">Impact</th>
<th data-field="effort">Effort</th>
<th data-field="budget">Budget</th>
<th data-field="notes">Notes</th>
</tr>
</thead>
<tbody>
<tr data-id="WO-00001"><td>WO-00001</td><td>REQ-9090</td><td>central.birmingham</td><td>done</td><td>2026-01-01</td><td>27</td><td>3</td><td>5</td><td>8580</td><td>Customer asked for an evening window.</td></tr>
<tr data-id="WO-00002"><td>WO-00002</td><td>INC-7104</td><td>south.london</td><td>done</td><td>2026-02-02</td><td>20</td><td>4</td><td>70</td><td>23160</td><td>Second engineer required for the roof section.</td></tr>
<tr data-id="WO-00003"><td>WO-00003</td><td>REQ-7362</td><td>south.london</td><td>blocked</td><td>2026-03-03</td><td>30</td><td>3</td><td>55</td><td>17685</td><td>Awaiting a wayleave from the landlord.</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('undo-and-redo-table'), {
selection: { mode: 'multiple', ranges: true },
// One history stack over every kind of write: a typed cell, a fill, a paste
// and a range clear each land as a single step, so undo reverses the whole
// gesture rather than one cell at a time. Ctrl-Z steps back, Ctrl-Shift-Z
// forward, and the toolbar buttons do the same.
edit: { enabled: true, undoDepth: 100 },
toolPanel: { side: 'left', panels: ['columns'], actions: ['undo', 'redo', 'restore'], exportName: 'work-orders' },
columns: [
{ field: 'id', title: 'Order', layout: { width: 120, pin: 'start' } },
{ field: 'reference', title: 'Reference', layout: { width: 130 } },
{ field: 'team', title: 'Team', filter: { type: 'set' }, edit: true },
{ field: 'stage', title: 'Stage', filter: { type: 'set' },
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: 'due', title: 'Due', type: 'date', layout: { width: 140 }, edit: true },
{ field: 'circuits', title: 'Circuits', type: 'number', layout: { width: 110 }, edit: true },
{ field: 'impact', title: 'Impact', type: 'number', layout: { width: 100 }, edit: true },
{ field: 'effort', title: 'Effort', type: 'number', layout: { width: 100 }, edit: true },
{ field: 'budget', title: 'Budget', type: 'number', layout: { width: 140 },
format: { style: 'currency', currency: 'GBP' }, total: 'sum', edit: true },
{ field: 'notes', title: 'Notes', layout: { flex: 1, min: 220 }, edit: { enabled: true, editor: 'textarea' } },
],
});
</script>
Reversing edits with a grid-level history stack
Undo and redo give a user a way back out of a mistaken edit, fill, or paste without reloading data or hand-correcting cells. A developer reaches for this wherever the grid accepts direct manipulation rather than a form: inline editing, drag-fill, range paste, or a bulk edit. Lattice Grid tracks these mutations through grid.history, which records each committed change as a single entry regardless of how many cells it touched, so a paste covering four hundred cells undoes in one step rather than four hundred. The stack groups by transaction rather than by keystroke, which matters for a JavaScript data grid used for data entry: a user editing one cell across several keypresses gets one undo step for the commit, not one per character. Entries store the affected range and prior values rather than a full grid snapshot, so undoing a change on a hundred-thousand-row dataset costs the same as undoing one on a hundred-row dataset, and the operation replays through the same validation and change-detection path as a live edit, so a reverted cell re-triggers any dependent formula recalculation or row-level validation exactly as the original edit did. Keyboard handling follows platform convention, with Ctrl+Z and Ctrl+Shift+Z (or Cmd on macOS) wired to the stack by default and available as direct calls for a custom toolbar.
How do I add undo and redo to a JavaScript data grid?
Use grid.history, which maintains a stack of committed edits, fills, and pastes and exposes undo() and redo() alongside the default Ctrl+Z / Ctrl+Shift+Z bindings. Each stack entry represents one transaction, so a multi-cell paste or bulk edit reverses in a single step rather than one call per affected cell.