FreeGridv1.6.0English

편집

컬럼에 editable: true를 지정하면 사용자가 셀을 수정할 수 있습니다. 더블클릭하거나 Enter나 F2를 누르거나, 그냥 입력을 시작하면 됩니다. Enter는 저장하고 아래로, Tab은 저장하고 다음 편집 칸으로 이동하며, Esc는 취소합니다. 참/거짓 값은…

실행 예제편집

셀을 바로 수정합니다. 텍스트·숫자·날짜·선택 편집기, 유효성 검사, 실행 취소, 변경 내역까지 지원합니다.

이 데모의 코드
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;
}

실행 예제편집 규칙과 수식

셀 잠금, 시간 편집기, 고를 수 없는 옵션, 중복 값 검증, 셀 수식, 변경 취소 이벤트, 전체 검사.

이 데모의 코드
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;
}

컬럼에 editable: true를 지정하면 사용자가 셀을 수정할 수 있습니다. 더블클릭하거나 Enter나 F2를 누르거나, 그냥 입력을 시작하면 됩니다. Enter는 저장하고 아래로, Tab은 저장하고 다음 편집 칸으로 이동하며, Esc는 취소합니다. 참/거짓 값은 Space나 더블클릭으로 바뀝니다.

편집기는 컬럼 타입을 따릅니다. 텍스트, 숫자(1,234도 입력 가능), 날짜, 참/거짓입니다. 정해진 목록에서 고르게 하려면 select 편집기를 쓰세요. options를 행을 받는 함수로 주면 국가에 따른 도시처럼 종속 선택이 됩니다. required와 validate로 잘못된 값을 막을 수 있고, 값을 고칠 때까지 편집기에 안내 메시지가 남아 있습니다.

편집, 붙여넣기, 행 추가와 삭제는 모두 기록되어 Ctrl+Z로 되돌릴 수 있습니다. 바뀐 셀에는 모서리에 작은 표시가 생깁니다. 저장할 때는 getModifiedRows()나 컬럼 단위의 getChanges()를 서버로 보낸 뒤 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 함수로 셀마다 정할 수 있고, 시간과 날짜·시간 편집기('time', 'datetime')도 있습니다. 선택 목록의 옵션에 disabled: true를 주면 고를 수도, 붙여넣을 수도 없습니다. unique: true는 다른 행에 이미 있는 값을 막고(빈 값은 허용), 저장 전에 validate()를 부르면 규칙에 맞지 않는 셀 목록을 돌려줍니다.

formulas: true 컬럼에는 =qty*price 같은 수식을 쓸 수 있습니다. 같은 행의 필드 이름, 사칙연산, ROUND, MIN, MAX, SUM, AVG, IF를 쓸 수 있고, 화면·정렬·복사·내보내기에는 계산 결과가 쓰입니다. 잘못된 수식은 #ERROR!, 없는 필드는 #REF!로 표시됩니다. beforeEdit, beforeCellChange, beforeSort, beforeFilter, beforePageChange 이벤트에서 cancel()을 부르면 그 변경을 막을 수 있습니다.

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 }]

editType: 'row'를 주면 행 단위로 편집합니다. Tab은 같은 행 안에서만 움직이고, Enter를 누르거나 다른 행으로 가면 그 행이 저장되며(rowEditStop), Esc는 행 전체를 되돌립니다. Ctrl+X는 범위를 복사한 뒤 편집 가능한 칸을 지웁니다.

웹소켓처럼 밖에서 들어오는 변경은 applyTransaction으로 id 기준 추가·수정·삭제만 반영하세요. 바뀐 셀이 잠깐 깜빡이고, 실행 취소나 변경 내역에는 들어가지 않습니다. 자주 들어오면 applyTransactionAsync가 모아서 한 번에 그립니다.

// 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 }] });

이 페이지에 대한 질문

셀을 편집 가능하게 하려면?

컬럼에 editable: true(또는 행마다 판단하는 함수)를 주세요. 텍스트·숫자·날짜·선택·시간·여러 줄 편집기와 유효성 검사, 실행 취소, 저장용 변경 내역이 들어 있습니다.