FreeGridv1.15.1한국어

Nested fields (address.city)

field: 'address.city' reads and writes nested objects: display, sort, filter, edit, paste, undo, change tracking and export all follow the path.

Live demoNested fields

Edit address.city and see the row object change.

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

const CITIES = ['Seoul', 'Busan', 'Incheon', 'Daegu', 'Tokyo', 'Paris'];

export default function mount(ctx: DemoContext) {
  const h = (en: string, ko: string) => tr(ctx.lang, { en, ko });
  // nested rows, as an API sends them; every fifth customer has no address yet
  const data: Row[] = makeRows(300).map((r, i) => ({
    id: r.id,
    customer: { name: r.name, company: r.company, ...(i % 5 ? { address: { city: CITIES[i % CITIES.length], zip: String(10000 + i * 37).slice(0, 5) } } : {}) },
    total: r.q1,
  }));
  const grid = createGrid(ctx.host, {
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 70 },
      { field: 'customer.name', header: h('Customer', '고객'), width: 160 },
      { field: 'customer.company', header: h('Company', '회사'), width: 150 },
      { field: 'customer.address.city', header: h('City', '도시'), width: 120, editable: true, editor: { type: 'combo', options: CITIES } },
      { field: 'customer.address.zip', header: h('ZIP', '우편번호'), width: 100, editable: true },
      { field: 'total', header: h('Total', '합계'), type: 'number', format: '#,##0', width: 110 },
    ],
    data, locale: ctx.lang, getRowId: r => String(r.id), filterRow: true,
  });
  grid.on('cellValueChanged', e => ctx.log(`#${e.row.id} ${e.colId}: ${String(e.oldValue ?? '')} → ${String(e.newValue)}  ·  customer = ${JSON.stringify(e.row.customer)}`));
  text(ctx, h('Edit a city or ZIP: the log shows the nested object. Rows without an address get one. Sort, filter and Ctrl+Z work as usual.',
    '도시나 우편번호를 고쳐 보세요. 로그에 중첩 객체가 보입니다. 주소가 없던 행에는 주소가 생깁니다. 정렬, 필터, Ctrl+Z도 그대로 됩니다.'));
  separator(ctx);
  button(ctx, h('Changes', '변경 내역'), () => ctx.log(JSON.stringify(grid.getChanges().map(c => ({ id: c.row.id, colId: c.colId, old: c.oldValue, new: c.newValue })))));
  button(ctx, h('Revert all', '모두 되돌리기'), () => grid.revertChanges());
  return grid;
}

Data from an API is often nested: an order has a customer object, the customer an address. Write the path to the value as the column's field, with dots between the keys, and the grid reads and writes it there. There is no need to flatten the rows first or to write a valueGetter.

const rows = [
  { id: 1, customer: { name: 'Kim', address: { city: 'Seoul', zip: '04524' } }, total: 1200 },
  { id: 2, customer: { name: 'Lee', address: { city: 'Busan' } }, total: 800 },
  { id: 3, customer: { name: 'Park' }, total: 450 },          // no address yet
];

createGrid(el, {
  getRowId: row => String(row.id),
  columns: [
    { field: 'customer.name', header: 'Customer' },
    { field: 'customer.address.city', header: 'City', editable: true },
    { field: 'customer.address.zip', header: 'ZIP', editable: true },
    { field: 'total', type: 'number' },
  ],
  data: rows,
});

What follows the path

FeatureWith a nested field
Display, sort, filter, quick search, findRead the value at the path; a missing object on the way reads as blank
Editing, paste, fill, DeleteWrite into the nested object; missing objects on the way are created (row 3 above gets customer.address)
Undo, revertChangesPut the old value back; revertChanges also removes objects the edits created
Change trackinggetChanges() gives the path as the column and the old and new value; the original keeps its own copy of nested objects, so it is not changed by later edits
applyTransaction (live data)An update that sends { id, customer: { … } } replaces the object; the nested columns repaint, flash and re-sort
CSV, Excel, PDF, print, importExport the value at the path; an imported file fills the nested objects
Column id, state, filtersThe id is the path ('customer.address.city') unless you give id

Keys that contain a dot

When a row has a key that is the whole path — { 'customer.name': 'Kim' }, as some exports produce — that key wins and is read and written as one flat key. So data that is already flat keeps working.

Nested field or valueGetter?

field: 'a.b'valueGetter
Readyesyes
Edit, paste, fillyesno
Values from several fieldsnoyes (row => row.qty * row.price)
Arrays (items[0].name)'items.0.name' reads the first itemyes

With TypeScript

With typed rows the paths are completed and checked by the editor: ColumnDef<Order> accepts customer.address.city and refuses a typo. See TypeScript.

A path walks plain objects and arrays (items.0.qty). A Date or a class instance at the end is the value itself. Change tracking and undo keep their own copy of plain objects and arrays only: a class instance is shared, so an edit inside one (field: 'owner.name' on a class) is not seen as a change. Keep values you edit as plain objects.

Questions about this page

How do I show a nested property such as address.city in a column?

Write the path as the field: { field: 'address.city' }. It is read, sorted, filtered and edited like any field, and an edit writes back into row.address.city.

Do I still need a valueGetter for nested data?

Not for plain paths. A valueGetter can only read, so the column cannot be edited; a dotted field can. Keep valueGetter for values computed from several fields.