Tree data
Show org charts, folders and bills of materials as a tree with getParentId, load children from a server on first expand, and check parents with their children.
Live demoTree data
An org chart as a tree: rows under their parent, children loaded on expand, parents checked with their children.
Code for this demo
import { createGrid, type Row } from '@freegridjs/core';
import { button, separator, text, tr, type DemoContext } from '../ui';
export default function mount(ctx: DemoContext) {
const { lang } = ctx;
const h = (en: string, ko: string) => tr(lang, { en, ko });
// an org chart: departments contain teams, teams contain people
const org: Row[] = [
{ id: 'hq', parent: null, name: h('Head office', '본사'), role: '', budget: 1200 },
{ id: 'dev', parent: 'hq', name: h('Development', '개발본부'), role: '', budget: 640 },
{ id: 'web', parent: 'dev', name: h('Web team', '웹팀'), role: '', budget: 260 },
{ id: 'p1', parent: 'web', name: '김민준', role: h('Lead', '팀장'), budget: 0 },
{ id: 'p2', parent: 'web', name: '이서연', role: h('Engineer', '개발자'), budget: 0 },
{ id: 'app', parent: 'dev', name: h('App team', '앱팀'), role: '', budget: 380 },
{ id: 'p3', parent: 'app', name: '박지호', role: h('Engineer', '개발자'), budget: 0 },
{ id: 'biz', parent: 'hq', name: h('Business', '사업본부'), role: '', budget: 560 },
{ id: 'p4', parent: 'biz', name: '최유나', role: h('Manager', '매니저'), budget: 0 },
{ id: 'p5', parent: 'biz', name: '정하준', role: h('Sales', '영업'), budget: 0 },
// its children come from the "server" on first expand
{ id: 'lab', parent: 'hq', name: h('Research lab (loads on expand)', '연구소 (펼칠 때 불러옴)'), role: '', budget: 300, lazy: true },
];
const grid = createGrid(ctx.host, {
columns: [
{ field: 'name', header: h('Organization', '조직'), width: 280 },
{ field: 'role', header: h('Role', '직책'), width: 140 },
{ field: 'budget', header: h('Budget (M)', '예산(백만)'), type: 'number', format: '#,##0', width: 140 },
],
data: org,
locale: lang,
getRowId: r => r.id,
rowSelection: 'multiple',
groupSelectsChildren: true,
groupDefaultExpanded: 1,
treeData: {
getParentId: r => r.parent,
hasChildren: r => !!r.lazy,
// pretend server: answers after 600 ms
loadChildren: row => new Promise<Row[]>(resolve => setTimeout(() => resolve([
{ id: 'r1', parent: row.id, name: '한지민', role: h('Researcher', '연구원'), budget: 0 },
{ id: 'r2', parent: row.id, name: '오세훈', role: h('Researcher', '연구원'), budget: 0 },
]), 600)),
},
});
grid.on('groupToggle', e => ctx.log(`groupToggle → ${e.key}: ${e.expanded}`));
grid.on('childrenLoaded', e => ctx.log(`childrenLoaded → ${e.rows.length}`));
text(ctx, h('Open the arrows and check a department: its people are checked too. Search keeps the parents of every match.',
'화살표로 펼치고 부서를 체크해 보세요. 소속 직원도 함께 체크됩니다. 검색하면 찾은 행의 상위 조직이 함께 남습니다.'));
separator(ctx);
button(ctx, h('Expand all', '모두 펼치기'), () => grid.expandAll());
button(ctx, h('Collapse all', '모두 접기'), () => grid.collapseAll());
button(ctx, h('Search "team"', '"팀" 검색'), () => grid.setQuickFilter(h('team', '팀')));
button(ctx, h('Clear search', '검색 지우기'), () => grid.setQuickFilter(''));
return grid;
}Data that already forms a hierarchy, such as an org chart, a bill of materials or a folder list, shows as a tree. Each row points at its parent with getParentId, and the grid nests it under that row with an arrow to open and close it. Rows need a stable id from getRowId so parents can be found. The order inside each level follows your data or the current sort.
Filtering and the quick search keep the parents of every match, so a result is always shown in its place in the tree. With rowSelection: 'multiple' and groupSelectsChildren: true, checking a department checks everyone in it. Open and close rows with the arrow, or with the right and left arrow keys in the first column; screen readers hear a treegrid.
createGrid(el, {
columns: [{ field: 'name', header: 'Organization' }, { field: 'budget', type: 'number' }],
data: [
{ id: 'hq', parent: null, name: 'Head office', budget: 1200 },
{ id: 'dev', parent: 'hq', name: 'Development', budget: 640 },
{ id: 'web', parent: 'dev', name: 'Web team', budget: 260 },
],
getRowId: r => r.id, // required: parents are found by id
treeData: { getParentId: r => r.parent }, // null or undefined = top level
groupDefaultExpanded: 1, // 0 = collapsed, -1 = everything open
rowSelection: 'multiple',
groupSelectsChildren: true, // checking a parent checks its children
});
grid.expandAll();
grid.collapseAll();
grid.setGroupExpanded('dev', true); // a tree row's key is its id
grid.on('groupToggle', e => console.log(e.key, e.expanded));Children from a server
When the children live on a server, mark the rows that have them with hasChildren and fetch them in loadChildren, which runs the first time a row opens. The row shows its arrow before anything is loaded, and the children must carry the row's id as their parent id.
// folders whose children live on the server: loaded on first expand
createGrid(el, {
columns, data: roots, getRowId: r => r.id,
treeData: {
getParentId: r => r.parent,
hasChildren: r => r.folder, // shows the arrow before anything is loaded
loadChildren: r => fetch(`/api/files?parent=${r.id}`).then(res => res.json()),
},
});
grid.on('childrenLoadFailed', e => console.error(e.row.id, e.error));To let people move rows to another parent by dragging, see Row drag.
Options, methods and events
| Name | Type | Description |
|---|---|---|
treeData.getParentId(row) | unknown | The parent's id, or null / undefined for a top-level row. Needs getRowId. |
treeData.setParentId(row, parentId, parent) | void | Store a new parent on the row; needed to drag rows to another parent (see Row drag). |
treeData.hasChildren / loadChildren | (row) => boolean / (row) => Promise<Row[]> | Row[] | Rows whose children are not loaded yet, and the function that fetches them on first expand. The children must carry the row id as their parent id. |
groupDefaultExpanded | number | Levels open at start: 0 none (default), n levels, -1 all. |
groupSelectsChildren | boolean | Checking a parent also checks (or unchecks) all of its children. |
expandAll() / collapseAll() / setGroupExpanded(id, open) | void | Open or close tree rows; a tree row's key is its id. |
groupToggle / childrenLoaded / childrenLoadFailed | events | A row opened or closed; loadChildren finished or failed. |
Questions about this page
How do I show hierarchical data as a tree?
Give every row an id with getRowId and set treeData: { getParentId: row => row.parent }. hasChildren and loadChildren fetch children from a server on first expand.