FreeGridv1.6.0한국어

Editing

Mark a column editable: true and users can edit its cells: double-click, press Enter or F2, or just start typing. Enter saves and moves down, Tab saves…

Live demoEditing

Edit cells in place with text, number, date and select editors, validation, undo and change tracking.

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

const CITIES: Record<string, string[]> = {
  'South Korea': ['Seoul', 'Busan', 'Incheon'],
  Japan: ['Tokyo', 'Osaka', 'Kyoto'],
  Germany: ['Berlin', 'Munich', 'Hamburg'],
  France: ['Paris', 'Lyon', 'Marseille'],
  Spain: ['Madrid', 'Barcelona', 'Valencia'],
  China: ['Beijing', 'Shanghai', 'Shenzhen'],
  'United States': ['New York', 'Seattle', 'Austin'],
  'United Kingdom': ['London', 'Manchester', 'Leeds'],
};

export default function mount(ctx: DemoContext) {
  const { lang } = ctx;
  const h = (en: string, ko: string) => tr(lang, { en, ko });
  const data = makeRows(300).map(r => ({ ...r, city: CITIES[r.country][r.id % 3] }));
  const columns: ColumnDef[] = [
    { field: 'id', header: 'ID', type: 'number', format: '0', width: 70, pinned: 'left' },
    // text editor + required
    { field: 'name', header: h('Name', '이름'), width: 170, editable: true, required: true },
    // select editor
    { field: 'country', header: h('Country', '국가'), width: 150, editable: true,
      editor: { type: 'select', options: Object.keys(CITIES) } },
    // dependent select: options come from the row's country
    { field: 'city', header: h('City', '도시'), width: 130, editable: true,
      editor: { type: 'select', options: (row: Row) => CITIES[row.country] ?? [] } },
    // number editor + validation
    { field: 'q1', header: h('Q1 revenue', '1분기 매출'), type: 'number', format: '#,##0', width: 130, editable: true,
      validate: v => (v == null || (typeof v === 'number' && v >= 0) ? true : h('Must be 0 or more', '0 이상이어야 합니다')) },
    { field: 'joined', header: h('Joined', '가입일'), type: 'date', width: 130, editable: true },
    { field: 'active', header: h('Active', '활성'), type: 'boolean', width: 90, editable: true }, // Space or double-click toggles
    { field: 'email', header: h('Email (read-only)', '이메일 (읽기 전용)'), width: 260 },
  ];
  const grid = createGrid(ctx.host, {
    columns, data, rowSelection: 'multiple', locale: lang,
    summary: { rows: [{ label: h('Total', '합계'), columns: { q1: 'sum', id: 'count' } }] },
  });

  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}`));
  grid.on('rowsAdded', e => ctx.log(`rowsAdded → ${e.rows.length}`));
  grid.on('rowsRemoved', e => ctx.log(`rowsRemoved → ${e.rows.length}`));

  text(ctx, h('Double-click, press Enter or just type. Enter saves, Esc cancels, Tab moves on.',
    '더블클릭하거나 Enter를 누르거나 바로 입력하세요. Enter로 저장, Esc로 취소, Tab으로 다음 칸.'));
  separator(ctx);
  button(ctx, h('Add row', '행 추가'), () => {
    const [row] = grid.addRows([{ id: 1000 + grid.getData().length, name: '', country: 'South Korea', city: 'Seoul', q1: 0, active: true }], 0);
    grid.scrollToRow(0);
    grid.startEdit(0, 'name');
    return row;
  });
  button(ctx, h('Delete selected', '선택 행 삭제'), () => grid.removeSelectedRows());
  button(ctx, h('Undo', '실행 취소'), () => grid.undo());
  button(ctx, h('Redo', '다시 실행'), () => grid.redo());
  separator(ctx);
  button(ctx, h('Show changes', '변경 내역 보기'), () => {
    const m = grid.getModifiedRows();
    ctx.log(h(`created ${m.created.length}, updated ${m.updated.length}, deleted ${m.deleted.length}`,
      `추가 ${m.created.length}, 수정 ${m.updated.length}, 삭제 ${m.deleted.length}`));
    for (const c of grid.getChanges().slice(0, 10)) ctx.log(`  #${c.row.id} ${c.colId}: ${String(c.oldValue)} → ${String(c.newValue)}`);
  });
  button(ctx, h('Save (commit)', '저장 (commit)'), () => {
    grid.commitChanges();
    ctx.log(h('changes committed — this is where you would send them to your server', '변경 확정 — 실제로는 여기서 서버로 보냅니다'));
  });
  button(ctx, h('Revert all', '모두 되돌리기'), () => grid.revertChanges());
  return grid;
}

Live demoEditing rules & formulas

Locked cells, time editor, disabled options, unique values, cell formulas, 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),
    // a formula: the cell shows the result
    total: i % 3 === 0 ? '=ROUND(qty*price*1.1, 0)' : '=qty*price',
    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 },
    { field: 'total', header: h('Total (formula)', '금액 (수식)'), type: 'number', format: '#,##0', width: 140, editable: true, formulas: true },
    { 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. Total holds formulas like =qty*price. Closed is a disabled option.',
    '행 단위로 편집합니다. Tab은 같은 행 안에서 이동하고, Enter는 행 저장, Esc는 행 전체 되돌리기입니다. 회색 행은 수정할 수 없고, 금액 칸에는 =qty*price 같은 수식이 들어 있습니다. "종료"는 고를 수 없는 옵션입니다.'));
  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;
}

Mark a column editable: true and users can edit its cells: double-click, press Enter or F2, or just start typing. Enter saves and moves down, Tab saves and moves to the next editable cell, Esc cancels. Booleans toggle with Space or a double-click.

The editor follows the column type: text, numbers (1,234 is accepted), dates and booleans. Use a select editor for a fixed list. When its options is a function of the row, you get dependent selects such as cities by country. required and validate keep bad values out; the editor stays open with a message until the value is fixed.

Every edit, paste, insert and removal is tracked and can be undone with Ctrl+Z. Changed cells get a small corner mark. When you're ready to save, read getModifiedRows() or the column-level getChanges(), send them to your server, then call commitChanges().

const COUNTRIES = { KR: ['Seoul', 'Busan'], JP: ['Tokyo', 'Osaka'] };

const grid = createGrid(el, {
  columns: [
    { field: 'id', type: 'number' },                                   // read-only
    { field: 'name', editable: true, required: true },
    { field: 'qty', type: 'number', editable: true, validate: v => v >= 0 || 'Must be 0 or more' },
    { field: 'country', editable: true, editor: { type: 'select', options: Object.keys(COUNTRIES) } },
    { field: 'city', editable: true, editor: { type: 'select', options: row => COUNTRIES[row.country] } },
    { field: 'memo', editable: true, editor: { type: 'textarea' } },
    { field: 'active', type: 'boolean', editable: true },             // Space / double-click toggles
  ],
  data,
});

grid.addRows([{ name: 'New' }], 0);
grid.removeSelectedRows();
grid.undo(); grid.redo();

// send only what changed
const { created, updated, deleted } = grid.getModifiedRows();
await save({ created, updated, deleted, cells: grid.getChanges() });
grid.commitChanges(); // or grid.revertChanges()

editable can be a function to decide per cell, and there are time and date-time editors ('time', 'datetime'). A select option with disabled: true can be neither chosen nor pasted. unique: true rejects a value another row already has (blanks are fine), and validate() lists every cell that breaks a rule before you save.

Columns with formulas: true accept formulas like =qty*price: same-row field names, arithmetic, ROUND, MIN, MAX, SUM, AVG and IF. The result is what you see, sort, copy and export; a bad formula shows #ERROR! and an unknown field #REF!. Call cancel() in beforeEdit, beforeCellChange, beforeSort, beforeFilter or beforePageChange to stop a change.

const grid = createGrid(el, {
  columns: [
    { field: 'code', editable: true, unique: true, required: true },
    // editable per cell
    { field: 'qty', type: 'number', editable: ({ row }) => !row.locked },
    { field: 'at', editable: true, editor: { type: 'time' } },
    { field: 'status', editable: true, editor: { type: 'select', options: [
      { value: 'open' }, { value: 'closed', disabled: true } ] } },
    // '=qty*price' in a cell shows the result
    { field: 'total', type: 'number', editable: true, formulas: true },
  ],
  data,
});

// stop a change before it happens
grid.on('beforeCellChange', e => { if (e.newValue < 0) e.cancel(); });
grid.on('beforeSort', e => { e.cancel(); loadSortedFromServer(e.sort); });

// before saving: every invalid cell
const problems = grid.validate(); // [{ row, rowIndex, colId, message }]

With editType: 'row' a row is edited as a whole: Tab stays in the row, Enter or moving to another row saves it (rowEditStop), and Escape puts the whole row back. Ctrl+X copies the range and clears its editable cells.

For changes that come from outside, such as a websocket, use applyTransaction to add, update and remove rows by id. Changed cells flash, and nothing goes into undo or the change log. When updates arrive often, applyTransactionAsync merges them into one redraw.

// prices pushed from a websocket: changed cells flash
socket.onmessage = e => grid.applyTransactionAsync({ update: JSON.parse(e.data) });

// add, update and remove by id
grid.applyTransaction({ add: [newOrder], addIndex: 0, update: [{ id: 7, status: 'done' }], remove: [{ id: 3 }] });

Questions about this page

How do I make cells editable in FreeGrid?

Set editable: true on a column (or a function per row). Text, number, date, select, time and textarea editors are built in, with validation, undo and change tracking for saving.