Cell Editing
Double-click any editable cell to edit it in-place. Built-in editors: text, number, date, select, checkbox. Press Enter or Tab to commit, Escape to cancel. Ctrl+Z / Ctrl+Y to undo/redo.
Editor types
One column per built-in editor, plus a custom one built from a template. Double-click a cell to open it; Enter commits, Escape restores the old value, Tab commits and moves to the next editable cell.
One column per built-in editor. text is the default, so a column that only sets
editable: true gets it without naming it:
'text'— auto-focused and pre-selected, so typing replaces.'number'— the committed value is cast to a number, not left as a string.editorOptionscarriesmin,maxandstep.'date'— a calendar; commits an ISO date string (2021-03-15) while showing a readable one (Mar 15, 2021). Give the column a matchingvalueFormatteror the value appears to change as the editor opens —editorsays how a column is edited, never how it is displayed.'select'— needseditorOptions.options.'checkbox'— commits the moment it is toggled. There is nothing to press Enter on, and Escape has nothing to restore.- A
TemplateRef— the Role column. Anything you can render, withcommit(value)andcancel()handed to you.
ID declares no editable, so double-clicking it does nothing at all —
read-only is the default, not something to switch on.
Two more editors, on a licence
'large-text' (a popup textarea) and 'rich-select' (a searchable list
with custom option rendering) are built in too. They are the one part of editing that is not
free — set either without a licence and the cell edits as plain text instead.
Conditional editability
editable takes a function, so the answer can differ per row. Everyone marked Active can be edited; the rest are read-only, and the grid stops offering the editor rather than rejecting the commit afterwards.
editable accepts a function, evaluated per row:
{ key: 'salary', prop: 'salary', editor: 'number',
editable: (row) => row.active }
The four rows marked Active can be edited; the rest cannot. The grid asks
before it offers, so a locked cell never opens an editor — there is no rejected commit to
explain and no error state to design. It shows in the DOM as well as on screen: an editable cell
carries gc-cell--editable, and a locked one does not.
Active itself stays editable on every row, deliberately. Locking the switch that unlocks the row is a trap worth avoiding: toggle a row on and its Name and Salary become editable immediately, because the function is re-evaluated, not cached.
Custom valueSetter
The default write is a shallow spread, which cannot reach a nested field. City lives under location, so it declares a valueSetter — and the object it returns is a new one, which is what keeps change detection honest.
Reading a nested field costs nothing — prop: 'location.city' is a dot path and the
grid walks it. Writing one is different: the default write is a shallow spread,
{ ...row, ['location.city']: value }, which creates a stray top-level key
and leaves the real field untouched.
valueSetter: (row, value) => ({
...row,
location: { ...row.location, city: String(value) },
})
Both columns above show the same field. Edit City and it changes. Edit City (no setter) and it snaps back — the write happened, just not where anyone was looking. That column is on the page precisely so the failure is visible once instead of surprising you later.
Return a new object
The setter must return a new row, not a mutated one. The grid compares references to
decide what to repaint, so mutating row in place writes the value and paints
nothing — the same symptom as having no setter at all, from the opposite cause.
How editing works
Double-click an editable cell — or single-click, with
config.editTrigger: 'click' — and the editor opens in place. Committing updates the
row in the grid immediately and fires (cellEditCommit) with the new row, the old
value and the column, which is where you persist to your API.
The readout under the first example is that event, printed. Nothing is sent anywhere on this page: every example edits an in-memory copy of its own, so an edit in one grid never moves another.
Editing is free, and so is its undo history. The two popup editors —
editor: 'large-text' and editor: 'rich-select' — are the paid part; set
one without a licence and the cell falls back to the plain text editor rather than going quiet.
One combination is not available on the free tier at all: editing and
scrollMode: 'virtual-paging', because virtual paging is itself a paid feature.
Undo / Redo
Ctrl+Z reverts the last committed edit and Ctrl+Y (or Ctrl+Shift+Z) re-applies it. The history is per grid and lives until the component is destroyed.
config.trackBy is required for it to work. Every commit builds a new
row object ({ ...row, [prop]: value }), so row identity cannot come from the
object reference — without trackBy the undo cannot find the row it is supposed to put
back. All three examples set trackBy: 'id'.
Keyboard reference
| Key | Effect |
|---|---|
| Double-click | Open the editor for the cell |
| Enter | Commit the value and close |
| Tab | Commit and move to the next editable cell |
| Esc | Cancel — the original value is restored |
| Ctrl+Z / Ctrl+Y | Undo / redo committed edits |
Every commit flows through the same edit path — clipboard paste, the fill handle and kanban card drags all create undoable entries, so Ctrl+Z reverses any of them.
Editing options
| Property | Type | Default | Description |
|---|---|---|---|
| column.editable | boolean | (row) => boolean | false | Opt a column into editing. A function is evaluated per row, and a locked cell never opens an editor. |
| column.editor | 'text' | 'number' | 'date' | 'checkbox' | 'select' | 'large-text' | 'rich-select' | TemplateRef | 'text' | Which editor opens. A TemplateRef gets an EditorContext with commit and cancel. The last two — large-text and rich-select — need a licence; without one they fall back to the text editor. |
| column.editorOptions | object | — | Editor extras — placeholder, options, min, max, step, textarea size. |
| column.valueSetter | (row, value) => T | shallow prop spread | Custom write path, needed for nested props. Must return a NEW row object. |
| column.validate | rule | rule[] | — | Validation run at commit time on every mutation route. See Edit Validation. |
| config.editTrigger | 'dblclick' | 'click' | 'dblclick' | What opens the editor. |
| config.trackBy | keyof T | ((i, row) => any) | — | Required for undo/redo — commits create new row objects, so identity must come from a field. |
| (cellEditCommit) | EventEmitter<CellEditEvent<T>> | — | Fires after each commit with the updated row, the column key, and the old and new values. |
| EditorContext.commit / cancel | (value) => void / () => void | — | The two functions a custom editor template is handed to end its session. |