Home/Editing/Cell Editing
Editing

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.

Full Name
Salary ($)
Started
Department
Active
Role
ID
Alice Smith
92000
Mar 15, 2021
Engineering
Yes
Engineer
1
Bob Johnson
88000
Nov 4, 2019
Engineering
Yes
Engineer
2
Carol Williams
105000
Jun 22, 2018
Marketing
Yes
Manager
3
David Brown
78000
Jan 10, 2022
Engineering
No
Engineer
4
Emma Jones
82000
Sep 1, 2020
Design
Yes
Designer
5
Frank Miller
115000
Feb 28, 2017
Engineering
Yes
Engineer
6
Grace Davis
65000
May 18, 2023
Sales
No
Manager
7
Henry Wilson
95000
Aug 30, 2021
Marketing
No
Designer
8
All records loaded
8 / 8 records
Nothing committed yet — double-click a 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. editorOptions carries min, max and step.
  • 'date' — a calendar; commits an ISO date string (2021-03-15) while showing a readable one (Mar 15, 2021). Give the column a matching valueFormatter or the value appears to change as the editor opens — editor says how a column is edited, never how it is displayed.
  • 'select' — needs editorOptions.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, with commit(value) and cancel() 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.

Full Name
Salary ($)
Active
Department
ID
Alice Smith
92000
Yes
Engineering
1
Bob Johnson
88000
Yes
Engineering
2
Carol Williams
105000
Yes
Marketing
3
David Brown
78000
No
Engineering
4
Emma Jones
82000
Yes
Design
5
Frank Miller
115000
Yes
Engineering
6
Grace Davis
65000
No
Sales
7
Henry Wilson
95000
No
Marketing
8
All records loaded
8 / 8 records

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.

Full Name
City
City (no setter)
Department
ID
Alice Smith
Berlin
Berlin
Engineering
1
Bob Johnson
Lisbon
Lisbon
Engineering
2
Carol Williams
Dublin
Dublin
Marketing
3
David Brown
Kraków
Kraków
Engineering
4
Emma Jones
Madrid
Madrid
Design
5
Frank Miller
Oslo
Oslo
Engineering
6
Grace Davis
Athens
Athens
Sales
7
Henry Wilson
Porto
Porto
Marketing
8
All records loaded
8 / 8 records

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

KeyEffect
Double-clickOpen the editor for the cell
EnterCommit the value and close
TabCommit and move to the next editable cell
EscCancel — the original value is restored
Ctrl+Z / Ctrl+YUndo / 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

PropertyTypeDefaultDescription
column.editableboolean | (row) => booleanfalseOpt 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.editorOptionsobject—Editor extras — placeholder, options, min, max, step, textarea size.
column.valueSetter(row, value) => Tshallow prop spreadCustom write path, needed for nested props. Must return a NEW row object.
column.validaterule | rule[]—Validation run at commit time on every mutation route. See Edit Validation.
config.editTrigger'dblclick' | 'click''dblclick'What opens the editor.
config.trackBykeyof 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.