FreeGridv1.15.1한국어

Validation and change tracking

required, unique and validate keep bad values out, validate() checks every row before saving, and getModifiedRows, undo and commitChanges track every edit.

Live demoValidation & editing rules

Locked cells, unique and required values, a time editor, disabled options, row editing, cancellable changes and validate().

Code for this demo
import { createGrid, type ColumnDef, type Row } from '@freegridjs/core';
import { button, separator, text, toggle, tr, type DemoContext } from '../ui';

export default function mount(ctx: DemoContext) {
  const { lang } = ctx;
  const h = (en: string, ko: string) => tr(lang, { en, ko });
  const data: Row[] = Array.from({ length: 30 }, (_, i) => ({
    sku: `SKU-${String(100 + i)}`,
    name: [h('Pen', '펜'), h('Notebook', '노트'), h('Stapler', '스테이플러'), h('Marker', '마커'), h('Tape', '테이프')][i % 5],
    qty: 1 + ((i * 7) % 12),
    price: 2 + ((i * 13) % 40),
    pickup: ['09:30', '13:00', '16:45'][i % 3],
    status: ['open', 'hold', 'closed'][i % 3],
    locked: i % 4 === 3,
  }));
  // a duplicate to find with validate()
  data[5].sku = data[2].sku;

  const columns: ColumnDef[] = [
    { field: 'sku', header: 'SKU', width: 110, editable: true, unique: true, required: true },
    { field: 'name', header: h('Item', '품목'), width: 120, editable: true },
    // locked rows cannot be edited
    { field: 'qty', header: h('Qty', '수량'), type: 'number', width: 90, editable: ({ row }) => !row.locked,
      cellClass: ({ row }) => (row.locked ? 'demo-locked' : undefined) },
    { field: 'price', header: h('Price', '단가'), type: 'number', width: 90, editable: ({ row }) => !row.locked },
    // worked out from the row; not editable
    { id: 'total', header: h('Total', '금액'), type: 'number', format: '#,##0', width: 120, valueGetter: r => r.qty * r.price },
    { field: 'pickup', header: h('Pickup', '수령 시각'), width: 110, editable: true, editor: { type: 'time' } },
    { field: 'status', header: h('Status', '상태'), width: 120, editable: true,
      editor: { type: 'select', options: [
        { value: 'open', label: h('Open', '진행') },
        { value: 'hold', label: h('On hold', '보류') },
        { value: 'closed', label: h('Closed (admin only)', '종료 (관리자 전용)'), disabled: true },
      ] } },
  ];
  // editType 'row': Tab stays in the row, Enter or another row saves it, Escape puts the row back
  const grid = createGrid(ctx.host, { columns, data, locale: lang, editType: 'row' });

  let blockNegative = true;
  grid.on('beforeCellChange', e => {
    if (blockNegative && typeof e.newValue === 'number' && e.newValue < 0) {
      e.cancel();
      ctx.log(h(`beforeCellChange → refused ${e.newValue} in ${e.colId}`, `beforeCellChange → ${e.colId}의 ${e.newValue} 거부`));
    }
  });
  grid.on('cellValueChanged', e => ctx.log(`cellValueChanged → ${e.colId}: ${String(e.oldValue)} → ${String(e.newValue)}`));
  grid.on('validationFailed', e => ctx.log(`validationFailed → ${e.colId}: ${e.message}`));

  text(ctx, h('Rows are edited as a whole: Tab stays in the row, Enter saves it, Escape puts it back. Gray rows are locked. SKU must be unique. Closed is a disabled option.',
    '행 단위로 편집합니다. Tab은 같은 행 안에서 이동하고, Enter는 행 저장, Esc는 행 전체 되돌리기입니다. 회색 행은 수정할 수 없고, SKU는 중복될 수 없습니다. "종료"는 고를 수 없는 옵션입니다.'));
  separator(ctx);
  button(ctx, h('Check all rows (validate)', '전체 검사 (validate)'), () => {
    const bad = grid.validate();
    ctx.log(bad.length ? h(`${bad.length} problem(s):`, `문제 ${bad.length}건:`) : h('all rows are valid', '모든 행이 올바릅니다'));
    for (const b of bad) ctx.log(`  ${h('row', '행')} ${b.rowIndex + 1} · ${b.colId}: ${b.message}`);
  });
  toggle(ctx, h('Refuse negative numbers', '음수 입력 막기'), true, on => { blockNegative = on; });
  grid.on('rowEditStop', e => ctx.log(`rowEditStop → ${e.committed ? h('saved', '저장') : h('put back', '되돌림')} ${e.changes.map(c => c.colId).join(', ')}`));
  return grid;
}

Rules on a column keep bad values out of the grid. required rejects blanks, unique rejects a value another row already has (blanks are fine), and validate(value, row) returns a message to reject anything else, such as a negative quantity or an e-mail address without @. A rejected edit leaves the editor open with the message until the value is fixed or Escape cancels it; the editor fires validationFailed. A pasted value that breaks a rule skips its cell and is counted in the paste event's skipped.

Data that was loaded, imported or changed in code can break the rules too. Call validate() before saving: it checks every editable cell and returns each one that fails, with its row, column and message, so you can scroll to the first problem or show a list.

const grid = createGrid(el, {
  columns: [
    { field: 'code', editable: true, unique: true, required: true },
    { field: 'name', editable: true, required: true },
    { field: 'qty', type: 'number', editable: true, validate: v => v >= 0 || 'Must be 0 or more' },
    { field: 'email', editable: true, validate: (v, row) => !v || v.includes('@') || 'Not an e-mail address' },
  ],
  data,
});
grid.on('validationFailed', e => toast(e.message));

// before saving: every cell that breaks a rule
const problems = grid.validate(); // [{ row, rowIndex, colId, message }]
if (problems.length) grid.scrollToRow(problems[0].rowIndex);

To check only part of the grid, such as the rows about to be saved, see Checking only some rows.

Change tracking and undo

Every edit, paste, insert and removal is recorded. Changed cells get a small corner mark, Ctrl+Z and Ctrl+Y undo and redo, and the grid knows what to send to your server: getModifiedRows() returns the created, updated and deleted rows, and getChanges() each changed cell with its old and new value. After the server has saved them, commitChanges() makes the current rows the new baseline and clears the marks; revertChanges() puts everything back instead.

grid.addRows([{ name: 'New' }], 0);   // insert at the top
grid.removeSelectedRows();
grid.undo(); grid.redo();              // Ctrl+Z / Ctrl+Y
grid.hasChanges();                     // true

// send only what changed
const { created, updated, deleted } = grid.getModifiedRows();
await save({ created, updated, deleted, cells: grid.getChanges() });
grid.commitChanges();                  // the saved state is the new baseline
// grid.revertChanges();               // or put everything back

Changes from applyTransaction (live data) are not edits: they are not tracked and cannot be undone. For a ready-made "search, edit, save" screen, saveChanges() posts the changes and commits them when the server accepts them; see Server examples.

Options, methods and events

NameTypeDescription
required / uniquebooleanColumn: reject blanks; reject a value another row already has (blanks are allowed).
validate(value, row)true | stringColumn: return a message to reject the value; the editor stays open and shows it.
grid.validateCell(row, colId)string | nullWhy one cell breaks its rules, or null.
grid.validate({ rows?, colIds?, changedOnly? }){ row, rowIndex, colId, message }[]Check every editable cell against the rules; empty when all rows are valid.
validationFailed{ rowIndex, colId, value, message } (event)An edited value was rejected (pasted values that break a rule are counted in the paste event instead).
getModifiedRows() / getChanges() / hasChanges()objectCreated, updated and deleted rows; every changed cell with its old and new value; whether anything changed.
commitChanges() / revertChanges()voidMake the current rows the new baseline (after saving), or put every change back.
addRows(rows, index?) / removeRows(rows | fn) / removeSelectedRows()Row[]Insert or remove rows; both are tracked and can be undone.
undo() / redo() / canUndo() / canRedo()void / booleanUndo history for edits, pastes and row changes (Ctrl+Z / Ctrl+Y).

Questions about this page

How do I check values before saving?

Put required, unique or validate on the columns; edits that break a rule are refused, and grid.validate() lists every invalid cell before you save.