Skip to content

Validation and lifecycle

Validation answers whether a proposed value or record is acceptable; lifecycle answers whether the grid can present data at all. Keeping those axes separate produces precise errors and avoids using an empty grid as a catch-all for loading or failure.

Focused usage

ts
import type { GridColumn } from '@jsvision/datagrid';

const quantity: GridColumn<Line> = {
  id: 'quantity',
  title: 'Quantity',
  value: (row) => row.quantity,
  validate: (value) => (value >= 0 ? undefined : 'Quantity cannot be negative'),
};

Validation gates

Cell validation handles type and local constraints. Row validation checks relationships across fields. Before-save validation checks application policy immediately before persistence. Report which gate rejected the operation and retain the user's value for correction.

GateRuns whenUse it for
Column validateA parsed editor value is about to commitType and single-field rules
beforeSaveAfter the optimistic write, before onCommitApplication policy and authorization hints
validateRowAn edited row is about to lose the cursorCross-field rules that need several committed values

Client validation improves correction flow; it is not an authorization boundary. Repeat policy and data-integrity checks in the authoritative persistence layer.

Recover a trapped row

A cross-field rule cannot reject every temporarily invalid cell commit: changing an interval often requires editing Start and End separately. validateRow therefore lets each valid cell commit, then blocks row-leave when the combined record is invalid. The committed cells form one bounded row-edit session that Escape can restore to its earliest values.

Set Start to 9, press Tab then Down to trap the row, and press Escape to restore it. Arm Alt+V first to see a persistence veto retain the edits for another Escape retry.

The laboratory starts maximized so the grid, actions, status text, and complete keyboard guidance remain visible together. Restore and maximize the dialog to check the same workflow at compact and wide sizes. Its text status distinguishes restored and vetoed outcomes without relying on color.

Which Escape owns the key?

Focus stateEscape behavior
A cell editor is openCancels that editor's uncommitted text
The grid body owns a trapped, edited rowStarts the atomic row-revert transaction
The row is untouched, valid, or already releasedFalls through; no row rollback starts
A row revert is pendingIs consumed with other grid input until settlement

Correcting the row and leaving successfully releases the session. Sorting, filtering, or republishing the same key-and-row-object collection may keep the session alive, but replacing or deleting that row, publishing a collection that omits it, or losing ownership invalidates the session. A late asynchronous result cannot attach to a replacement row.

Persist an atomic row revert

Use onRevertRow when accepted cell commits also reach host persistence:

ts
import type { OnRevertRow } from '@jsvision/datagrid';

const persistRevert: OnRevertRow<Line> = async ({ rowKey, row, cells }) => {
  return saveOriginalCells(rowKey, row, cells);
};

The callback receives the row after all captured baselines have been applied, plus an immutable cells array in first-commit order. Each cell describes its column, original value, and committed previous value. Freeze or copy any additional application state before awaiting; the supplied payload itself is already frozen.

Returning false, throwing, or rejecting compensates the in-memory row back to its committed values, keeps the trap retryable, and shows a bounded failure message. While the callback is pending, editing, navigation, filters, selection, resize, and other grid mutations remain inert. Dispose or row replacement invalidates the presentation; settlement still belongs only to the captured transaction.

Without onRevertRow, local rollback is available only when the grid has no beforeSave or onCommit persistence hook. A persisted grid deliberately refuses rollback without the atomic row seam, avoiding a UI value that disagrees with storage.

Lifecycle states

Loading, ready, source-empty, filtered-empty, and error require different messages and actions. A retry belongs to error; clearing criteria belongs to filtered-empty; adding the first record belongs to source-empty.

Cycle through loading, ready, empty, filtered-empty, and error presentations without replacing the host grid.

Limits and practices

  • Keep invalid cells navigable and readable; do not hide them behind a generic toast.
  • Tell users that body Escape restores committed row edits, while editor Escape cancels only the open editor.
  • Persist every reverted cell as one row transaction; per-cell rollback can leave storage partially restored.
  • Keep veto and failure feedback textual as well as colored so retry state remains visible in monochrome.
  • Cancel stale asynchronous results when the source changes or the host closes.
  • Preserve the last usable data only when the application explicitly supports stale display.
  • Reset validation and lifecycle fixtures deterministically in documentation and tests.