Skip to content

Undo/Redo Plugin

The Undo/Redo plugin tracks all cell edits and lets users revert or replay changes with familiar keyboard shortcuts (Ctrl+Z / Ctrl+Y). It maintains an in-memory history stack with configurable depth—perfect for data entry workflows where mistakes happen.

⚠️ Required Dependency: This plugin requires EditingPlugin to be loaded first. UndoRedo tracks the edit history that EditingPlugin creates.

import '@toolbox-web/grid/features/editing';
import '@toolbox-web/grid/features/undo-redo';

Enable both features and history tracking is automatic—every cell edit is recorded, and users can navigate back and forth through the edit history.

import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');
grid.gridConfig = {
columns: [
{ field: 'name', header: 'Name', editable: true },
{ field: 'price', header: 'Price', type: 'number', editable: true },
{ field: 'quantity', header: 'Quantity', type: 'number', editable: true },
],
features: {
editing: 'dblclick',
undoRedo: { maxHistorySize: 50 },
},
};

Edit cells by double-clicking, then use Ctrl+Z / Ctrl+Y to undo/redo. Adjust the max history size with the control.

Max history sizeMaximum undo steps to keep

See UndoRedoConfig for the full list of options and defaults.

ShortcutAction
Ctrl+Z / Cmd+ZUndo
Ctrl+Y / Cmd+Y / Cmd+Shift+ZRedo
const plugin = grid.getPluginByName('undoRedo');
plugin.undo();
plugin.redo();
const canUndo = plugin.canUndo();
const canRedo = plugin.canRedo();
plugin.clearHistory();

Each recorded edit stores the row’s ID (from getRowId or row.id) alongside its display index. Undo and redo find the row by that ID, so they still target the right row after the grid is sorted or filtered, even when the row is filtered out of view. If a row has no ID, the plugin falls back to the display index captured when the edit was made. That index goes stale once the view changes, so configure getRowId when you combine undo/redo with sorting or filtering.

recordEdit(rowIndex, field, oldValue, newValue, rowId?) reads the ID from the row currently shown at rowIndex, so call it while that index still points at the edited row. If the row may not be in view, pass rowId yourself. For example, a cell-commit raised by updateRow() on a filtered-out row reports rowIndex: -1, but its detail.rowId is still set.

After an undo or redo, focus moves to the restored cell once the grid has re-rendered, so it lands on the row’s new position after any re-sort or re-filter. If the row is filtered out, focus stays where it was. Undoing an edit on a row that has since been deleted does nothing.

When a user edit cascades programmatic changes to other fields, group them into a single undo step so Ctrl+Z reverts everything at once:

grid.on('cell-commit', ({ row, rowId, rowIndex }) => {
const undoRedo = grid.getPluginByName('undoRedo');
undoRedo.beginTransaction();
// Record cascaded updates manually (pass rowId so off-screen rows are targeted correctly)
const oldTotal = row.total;
undoRedo.recordEdit(rowIndex, 'total', oldTotal, newTotal, rowId);
grid.updateRow(rowId, { total: newTotal });
// End after the auto-recorded original edit is captured
queueMicrotask(() => undoRedo.endTransaction());
});
MethodDescription
beginTransaction()Start buffering edits into a compound group
endTransaction()Finalize and push the compound as a single undo step
recordEdit()Manually record a cell edit (buffered during txn)
Double-click a cell to edit, then undo/redo.
Event Log:

Fired after an undo or redo action is applied, whether it came from the keyboard or from plugin.undo() / plugin.redo(). If your app keeps rows in its own state, replay the action from these events so a toolbar button and Ctrl+Z update that state the same way:

grid.on('undo', ({ action, type }) => {
// `action` is a single 'cell-edit' or a 'compound' group (see Compound Actions above)
if (action.type === 'cell-edit') {
console.log(`${type}: reverted ${action.field} on row ${action.rowId ?? action.rowIndex}`);
}
});
grid.on('redo', ({ action, type }) => {
if (action.type === 'cell-edit') {
console.log(`${type}: reapplied ${action.field} on row ${action.rowId ?? action.rowIndex}`);
}
});

See UndoRedoDetail for the full event payload type.

Fired whenever the history changes: an edit is recorded outside a transaction, a non-empty transaction ends, an undo or redo runs, or clearHistory() empties a non-empty history. Bind your toolbar buttons to it:

grid.on('history-change', ({ canUndo, canRedo }) => {
undoButton.disabled = !canUndo;
redoButton.disabled = !canRedo;
});

See HistoryChangeDetail for the payload type. In the framework adapters, onHistoryChange (React), @history-change (Vue) and (historyChange) (Angular) expose the same event; useGridUndoRedo() / injectGridUndoRedo() already refresh canUndo() / canRedo() from it.