developer guide
Data Grid Inline Editing and Validation
A reader types straight into the cell, and the rules you declared on the column decide whether the value is allowed: required, min and max, length, pattern, one of a list, or a check that reads other fields in the same row. A rejected value never becomes state, and the reason is there to show.
Developer guide › Editing › Data Grid Inline Editing and Validation
Editing
Turning it on
edit: { enabled: true }, // double-click, the default
edit: { enabled: true, start: 'single' },// single click
editBar: true, // a spreadsheet-style input above the grid
columns: [
{ field: 'notes', edit: true },
{ field: 'statusId', edit: { editor: 'select' } },
{ field: 'quality', edit: { editor: 'rating', props: { max: 5, allowHalf: true } } },
{ field: 'circuitId', edit: false },
]
The editor is chosen from the type unless you name one. Enter commits and steps down, Tab commits and steps across, Escape cancels; holding Shift reverses the direction of either (Shift+Enter steps up, Shift+Tab steps back) - the row a step lands on opens its own editor in the same column, so the keyboard never has to re-arm it. A validator can refuse a value:
Validation
{ field: 'capacity', edit: {
validate: (p) => p.value > 0 || 'Capacity must be positive',
}}
An input mask on a text column
A text column’s cell editor can be held to a fixed pattern as it is typed: set
mask in the column’s edit.props (the editor options live there, beside
maxLength and placeholder). It is the same engine and the same rules as the form
input item’s mask: 9 a digit, A a letter,
* a letter or digit, anything else a literal inserted as you type, and the cell stores the masked
text as shown, literals included. An incomplete value is refused on commit through the editor’s normal
invalid-edit path. Paste and fill write through edit.validate and not through the cell editor, so
to have a pasted range refused cell by cell set validate: editors.text.maskValidator(pattern) too.
This is not the display mask of a column format, which only paints a stored value.
const { editors } = await import('../packages/dom/src/editors/index.js');
const { attachInputMask } = await import('../packages/dom/src/input-mask.js');
const { createTestDom, TestEvent } = await import('../packages/dom/src/renderer/testdom.js');
const mask = 'AA-9999';
const { document } = createTestDom();
const input = document.createElement('input');
attachInputMask(input, mask);
input.value = 'ab1234';
input.dispatchEvent(new TestEvent('input'));
const check = editors.text.maskValidator(mask);
const out = [
check({ value: 'ab-1234' }) === true ? 'ok' : 'bad',
input.value,
check({ value: 'ab-12' }),
].join('|');
return out;
The same edit.props also take maxLength, the longest text the editor accepts,
and placeholder; with a mask and no placeholder of your own, the pattern is shown.
const { TextEditor } = await import('../packages/dom/src/editors/index.js');
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { document } = createTestDom();
const open = (props) => {
const editor = new TextEditor();
editor.init({ document, cell: document.createElement('div'), overlay: document.body,
column: { id: 'tag', title: 'Tag', edit: { enabled: true, props } }, value: '' });
return editor.input;
};
return [
open({ maxLength: 5 }).getAttribute('maxlength'),
open({ placeholder: 'Asset tag' }).getAttribute('placeholder'),
open({ mask: 'AA-9999' }).getAttribute('placeholder'),
].join('|');
Declarative validation
The edit.validate function above is the imperative form. Where the rules are simple
and the same across columns, declare them as data on validation instead: required, min/max,
minLength/maxLength, pattern, oneOf, and a
crossField predicate. Each is checked before the write, riding the
cancellable beforeEdit event: a failing value cancels the commit, marks the cell
with the same accessible invalid state an editor rejection uses, and fires
validation:failed. Correcting the value clears the mark and fires
validation:cleared. Every user-origin write is gated, whichever way
it arrives: a typed edit, a clipboard paste, the fill handle and Ctrl+D,
grid.edit.setCells(…, { origin: 'user' }) (and pasteInto,
fill, bulkSet given the same origin) and a chart drag-to-edit.
A refused cell is skipped with the grid's own message, as a typed edit is; the other cells of a
paste still land. A write with any other origin - the default 'api', or
'form' - keeps its behaviour: a host API write is the authority and is never
self-vetoed.
Rules as data
columns: [
{ field: 'name', edit: true, validation: { required: true, minLength: 2 } },
{ field: 'age', type: 'number', edit: true, validation: { min: 0, max: 120 } },
{ field: 'code', edit: true, validation: { pattern: '^[A-Z]{3}$', messages: { pattern: 'Three capitals.' } } },
]
// Why a write was refused, and clearing a mark by hand.
grid.validation.errorFor('r1', 'age'); // { code, message, key, colId } or null
grid.on('validation:failed', (e) => report(e.failures));
grid.on('validation:cleared', () => refreshBanner());
Deleting rows
Set rowDelete: true for the built-in delete gesture: Delete or Backspace on the
selected rows, and a "Delete row" item in the cell menu. It works on a memory grid, not only a
remote one, and every deletion flows through the cancellable beforeDelete event - so a confirm dialog is a handler that calls e.preventDefault(reason). It is off by
default because deleting data on a keystroke is destructive.
Confirm before delete
const grid = createGrid(el, { columns, rows, rowKey: 'id', selection: 'multiple', rowDelete: true });
grid.on('beforeDelete', async (e) => {
const ok = await confirmDialog(`Delete ${e.rows.length} row(s)?`);
if (!ok) e.preventDefault('cancelled'); // the rows stay; delete:cancelled fires
});
// The gesture calls this; you can call it too. Defaults to the selection.
grid.edit.deleteRows();