Lattice Grid

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();