Editing
Editing is an Enterprise feature. Import EnterpriseDataGrid instead of
DataGrid and pass an editing object:
import { EnterpriseDataGrid } from '@kanunilabs/datagrid-react-enterprise';
<EnterpriseDataGrid
licenseKey={key}
dataSource={rows}
rowKey="id"
columns={columns}
editing={{ mode: 'batch', allowAdding: true, allowDeleting: true }}
/>
Modes
mode decides two things: what the user opens, and when the change is
persisted.
| Mode | The user edits | Saved when |
|---|---|---|
cell | one cell | immediately on commit |
batch (default) | any cells, freely | an explicit Save |
row | the whole row, in place | that row's Save |
form | the row as a form panel | that row's Save |
popup | the row as a modal dialog | that row's Save |
row, form and popup differ only in presentation — they share one row
session underneath, so Save, Cancel and validation behave identically in all
three. Switching between them mid-edit keeps what the user has typed.
Row, form and popup
Double-click a row to open it, or use Edit row… in the row context menu.
row turns every editable cell of that row into an editor and puts Save /
Cancel in the strip above the grid; form and popup lay the fields out with
labels and carry their own buttons.
editing={{
mode: 'popup',
allowAdding: true,
rules: {
product: [{ type: 'required' }, { type: 'stringLength', min: 3 }],
units: [{ type: 'required' }, { type: 'range', min: 1, max: 1000 }],
},
crud: {
update: (key, values, row) =>
fetch(`/api/orders/${key}`, { method: 'PATCH', body: JSON.stringify(values) })
.then((r) => r.json()),
},
}}
Four behaviours are deliberate:
Only that row is saved. A half-filled row elsewhere stays pending. That is
the whole difference from batch.
Nothing is written through until Save. The values live in the change store, so a Cancel genuinely restores the row and a cancelled edit never reached sorting, filtering or an export. Totals still follow the draft — the footer updates as you type, before you save.
A rejected save keeps the row open, holding what the user typed, with the server's message above the fields. Nothing is lost because row 3 of 10 hit a constraint.
Every field is validated on Save, not just the ones that were touched. A required field the user never opened is the common failure on a new row, and per-keystroke validation cannot see it. The first invalid field takes focus.
+ New row in row/form/popup opens the new record instead of posting
a blank one; onInitNewRow seeds it, and Cancel removes it entirely.
Keyboard: Ctrl/⌘+Enter saves, Esc
cancels. In row mode a bare Enter also saves. In a form a bare
Enter is inert on purpose — in a grid it is one keystroke away at
all times, and "I pressed Enter and it saved a half-filled row" is the
complaint that follows.
Wording is translatable:
editing={{
mode: 'popup',
formLabels: { save: 'Kaydet', cancel: 'Vazgeç', editTitle: 'Satırı düzenle' },
}}
Persistence
Two shapes, and the per-row one wins when both are present.
Per row — one call per insert / update / delete, which is what a REST or SQL back end wants:
crud: {
insert: (values) => api.create(values), // return the stored row
update: (key, values, row) => api.patch(key, values),
remove: (key, row) => api.delete(key),
}
values on an update holds the changed columns only. Returning the stored
row makes the grid adopt the server's version of it — its real primary key,
defaults, computed columns. Return nothing and the grid leaves your data alone:
it never mutates your array behind your back.
As a batch — one call with everything:
onSave: (changes) => api.saveAll(changes)
Either way a rejection fails the whole save and leaves the changes pending, so nothing the user typed is lost.
Validation
Rules are per column id, checked as the user types and again on save:
rules: {
email: [{ type: 'required' }, { type: 'pattern', pattern: /@/ }],
qty: [{ type: 'range', min: 1, max: 999 }],
code: [{ type: 'custom', validate: async (v) => (await isFree(v)) || 'Already taken' }],
}
Async rules run on save, not per keystroke.
Permissions
editing={{
allowAdding: true,
allowDeleting: (row) => row.status === 'draft',
allowUpdating: (row, columnId) => columnId !== 'id',
}}
A column the predicate refuses is read-only in every mode — it is skipped by Tab, shown but disabled in a form, and rejected by paste and fill.