Documentation

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.

ModeThe user editsSaved when
cellone cellimmediately on commit
batch (default)any cells, freelyan explicit Save
rowthe whole row, in placethat row's Save
formthe row as a form panelthat row's Save
popupthe row as a modal dialogthat 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.