Tree Plugin
The Tree plugin transforms your flat grid into a hierarchical tree view with expandable parent-child relationships. Great for file explorers, organizational charts, nested categories, or any data with a natural hierarchy.
Installation
Section titled “Installation”import '@toolbox-web/grid/features/tree';Basic Usage
Section titled “Basic Usage”Just point the feature at the field containing child items (defaults to children) and the grid handles all the expand/collapse behavior, indentation, and icons automatically.
import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }, { field: 'size', header: 'Size' } ], features: { tree: { childrenField: 'children', indentWidth: 24, }, },};
grid.rows = [ { id: 1, name: 'Documents', type: 'folder', children: [ { id: 2, name: 'Work', type: 'folder', children: [ { id: 3, name: 'Report.docx', type: 'file', size: '24 KB' } ]}, { id: 4, name: 'Personal', type: 'folder', children: [] } ] },];import '@toolbox-web/grid-react/features/tree';import { DataGrid } from '@toolbox-web/grid-react';
function FileExplorer({ files }) { return ( <DataGrid rows={files} columns={[ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }, { field: 'size', header: 'Size' } ]} tree={{ childrenField: 'children', indentWidth: 24 }} /> );}<script setup>import '@toolbox-web/grid-vue/features/tree';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
const files = [ { id: 1, name: 'Documents', type: 'folder', children: [ { id: 2, name: 'Work', type: 'folder', children: [{ id: 3, name: 'Report.docx', type: 'file', size: '24 KB' }] }, { id: 4, name: 'Personal', type: 'folder', children: [] }, ], },];</script>
<template> <TbwGrid :rows="files" :tree="{ childrenField: 'children', indentWidth: 24 }"> <TbwGridColumn field="name" header="Name" /> <TbwGridColumn field="type" header="Type" /> <TbwGridColumn field="size" header="Size" /> </TbwGrid></template>// Feature import - enables the [tree] inputimport { GridTreeDirective } from '@toolbox-web/grid-angular/features/tree';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
@Component({ selector: 'app-file-explorer', imports: [Grid, GridTreeDirective], template: ` <tbw-grid [rows]="files" [columns]="columns" [tree]="{ childrenField: 'children', indentWidth: 24 }" style="height: 400px; display: block;"> </tbw-grid> `,})export class FileExplorerComponent { files = [/* hierarchical data */];
columns: ColumnConfig[] = [ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }, { field: 'size', header: 'Size' } ];}Toggle the controls to explore animation, indentation, and expand behavior.
<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/tree';
const fileSystemData = [ { name: 'Documents', type: 'folder', size: '-', children: [ { name: 'Resume.pdf', type: 'file', size: '2.4 MB' }, { name: 'Cover Letter.docx', type: 'file', size: '156 KB' }, { name: 'Projects', type: 'folder', size: '-', children: [ { name: 'Project A', type: 'folder', size: '-', children: [{ name: 'notes.txt', type: 'file', size: '12 KB' }] }, { name: 'Project B', type: 'folder', size: '-' }, ], }, ], }, { name: 'Pictures', type: 'folder', size: '-', children: [ { name: 'vacation.jpg', type: 'file', size: '4.2 MB' }, { name: 'family.png', type: 'file', size: '3.1 MB' }, ], }, { name: 'readme.md', type: 'file', size: '1 KB' },];const columns = [ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type' }, { field: 'size', header: 'Size' },];
const grid = queryGrid('tbw-grid');
function rebuild(opts: Record<string, unknown>) { grid.gridConfig = { columns, features: { tree: { animation: opts.animation === 'false' ? false : (opts.animation) ?? 'slide', childrenField: 'children', defaultExpanded: opts.defaultExpanded ?? false, indentWidth: (opts.indentWidth) ?? 20, showExpandIcons: opts.showExpandIcons ?? true, }, }, }; grid.rows = fileSystemData;}
rebuild({ animation: 'slide', defaultExpanded: false, indentWidth: 20, showExpandIcons: true });With Pinned Columns
Section titled “With Pinned Columns”Use treeColumn to control which column displays the tree toggle when combining tree with pinned (sticky) columns. By default the toggle appears on the first visible column — set treeColumn to explicitly target a wider column like 'name'.
<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/pinned-columns';import '@toolbox-web/grid/features/tree';
const orgData = [ { id: 1, name: 'Engineering', department: 'Tech', headcount: 42, budget: '$2.1M', children: [ { id: 2, name: 'Frontend', department: 'Tech', headcount: 18, budget: '$900K', children: [ { id: 3, name: 'Alice Chen', department: 'Tech', headcount: 1, budget: '$120K' }, { id: 4, name: 'Bob Lee', department: 'Tech', headcount: 1, budget: '$115K' }, { id: 5, name: 'Carol Wu', department: 'Tech', headcount: 1, budget: '$110K' }, ], }, { id: 6, name: 'Backend', department: 'Tech', headcount: 24, budget: '$1.2M', children: [ { id: 7, name: 'Dan Kim', department: 'Tech', headcount: 1, budget: '$130K' }, { id: 8, name: 'Eve Park', department: 'Tech', headcount: 1, budget: '$125K' }, ], }, ], }, { id: 10, name: 'Marketing', department: 'Business', headcount: 15, budget: '$800K', children: [ { id: 11, name: 'Frank Diaz', department: 'Business', headcount: 1, budget: '$95K' }, { id: 12, name: 'Grace Ito', department: 'Business', headcount: 1, budget: '$90K' }, ], }, { id: 20, name: 'Sales', department: 'Business', headcount: 20, budget: '$1.5M', children: [ { id: 21, name: 'Hank Novak', department: 'Business', headcount: 1, budget: '$100K' }, { id: 22, name: 'Ivy Shah', department: 'Business', headcount: 1, budget: '$105K' }, ], },];
const grid = queryGrid('tbw-grid');
function rebuild(opts: Record<string, unknown>) { const treeColumn = opts.treeColumn === '(first)' ? undefined : (opts.treeColumn); grid.gridConfig = { columns: [ { field: 'id', header: 'ID', width: 60, pinned: opts.pinId ? 'left' : undefined }, { field: 'name', header: 'Name', width: 180, pinned: opts.pinName ? 'left' : undefined }, { field: 'department', header: 'Department', width: 120 }, { field: 'headcount', header: 'Headcount', width: 100, type: 'number' }, { field: 'budget', header: 'Budget', width: 100 }, ], features: { tree: { childrenField: 'children', defaultExpanded: true, indentWidth: (opts.indentWidth) ?? 20, treeColumn, }, pinnedColumns: true, }, }; grid.rows = orgData;}
rebuild({ treeColumn: 'name', indentWidth: 20, pinId: true, pinName: true });Server-Side Data
Section titled “Server-Side Data”Requires:
ServerSidePlugin— see Server-Side Plugin
For large hierarchical datasets, use ServerSidePlugin to fetch top-level tree nodes in blocks as the user scrolls. TreePlugin claims the incoming data and renders the hierarchy. Children can be embedded in the response or fetched lazily via getChildRows().
import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/tree';import '@toolbox-web/grid/features/server-side';
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'role', header: 'Role' }, ], features: { serverSide: { pageSize: 50, dataSource: { async getRows(params) { // params: { startNode, endNode, sortModel, filterModel } const res = await fetch(`/api/tree?start=${params.startNode}&end=${params.endNode}`); const data = await res.json(); return { rows: data.nodes, // Top-level nodes with embedded children totalNodeCount: data.total, // Total top-level nodes on server }; }, }, }, tree: { childrenField: 'children' }, },};import '@toolbox-web/grid-react/features/tree';import '@toolbox-web/grid-react/features/server-side';import { DataGrid } from '@toolbox-web/grid-react';
const dataSource = { async getRows(params) { const res = await fetch(`/api/tree?start=${params.startNode}&end=${params.endNode}`); return res.json(); },};
function ServerTree() { return ( <DataGrid columns={[ { field: 'name', header: 'Name' }, { field: 'role', header: 'Role' }, ]} serverSide={{ pageSize: 50, dataSource }} tree={{ childrenField: 'children' }} /> );}<script setup>import '@toolbox-web/grid-vue/features/tree';import '@toolbox-web/grid-vue/features/server-side';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';
const serverSideConfig = { pageSize: 50, dataSource: { async getRows(params) { const res = await fetch(`/api/tree?start=${params.startNode}&end=${params.endNode}`); return res.json(); }, },};</script>
<template> <TbwGrid :server-side="serverSideConfig" :tree="{ childrenField: 'children' }"> <TbwGridColumn field="name" header="Name" /> <TbwGridColumn field="role" header="Role" /> </TbwGrid></template>import { GridTreeDirective } from '@toolbox-web/grid-angular/features/tree';import { GridServerSideDirective } from '@toolbox-web/grid-angular/features/server-side';import { Component } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig } from '@toolbox-web/grid';
@Component({ selector: 'app-server-tree', imports: [Grid, GridTreeDirective, GridServerSideDirective], template: ` <tbw-grid [columns]="columns" [serverSide]="serverSideConfig" [tree]="{ childrenField: 'children' }" style="height: 400px; display: block;"> </tbw-grid> `,})export class ServerTreeComponent { columns: ColumnConfig[] = [ { field: 'name', header: 'Name' }, { field: 'role', header: 'Role' }, ];
serverSideConfig = { pageSize: 50, dataSource: { async getRows(params: any) { const res = await fetch(`/api/tree?start=${params.startNode}&end=${params.endNode}`); return res.json(); }, }, };}Scroll down to load more nodes…
<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/server-side';import '@toolbox-web/grid/features/tree';
const statusEl = document.querySelector('.load-status');
// Generate a large hierarchical dataset on the client to simulate a serverfunction generateTreeData(count: number) { const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance']; const data = []; for (let i = 0; i < count; i++) { const dept = departments[i % departments.length]; const teamSize = 2 + (i % 4); const children = []; for (let j = 0; j < teamSize; j++) { children.push({ id: `${i + 1}-${j + 1}`, name: `${dept} Member ${j + 1}`, role: j === 0 ? 'Lead' : 'Member', department: dept, }); } data.push({ id: `${i + 1}`, name: `${dept} Team ${Math.floor(i / departments.length) + 1}`, role: 'Manager', department: dept, children, }); } return data;}
const allTopLevelNodes = generateTreeData(200);
// Simulated data source — returns a page of top-level nodes with children embeddedfunction createDataSource(pageSize: number) { return { async getRows(params: any) { // Simulate network latency await new Promise((r) => setTimeout(r, 300));
let data = [...allTopLevelNodes]; if (params.sortModel?.length) { const { field, direction } = params.sortModel[0]; data.sort((a: any, b: any) => { const cmp = a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0; return direction === 'asc' ? cmp : -cmp; }); }
const page = data.slice(params.startNode, params.endNode); return { rows: page, totalNodeCount: data.length, }; }, };}
const columns = [ { field: 'name', header: 'Name', sortable: true }, { field: 'role', header: 'Role' }, { field: 'department', header: 'Department', sortable: true },];
const grid = queryGrid('tbw-grid');
function rebuild(opts: Record<string, unknown>) { const pageSize = (opts.pageSize) ?? 20; grid.gridConfig = { columns, features: { serverSide: { pageSize }, tree: { childrenField: 'children', animation: opts.animation === 'false' ? false : (opts.animation) ?? 'slide', defaultExpanded: opts.defaultExpanded ?? false, }, }, };
grid.ready().then(() => { const serverSide = grid.getPluginByName('serverSide'); serverSide?.setDataSource(createDataSource(pageSize)); statusEl.textContent = 'Scroll down to load more nodes…'; });}
rebuild({ pageSize: 20, animation: 'slide', defaultExpanded: false });
grid.on('datasource:loading', (e: any) => { if (e.detail?.loading) { statusEl.textContent = 'Loading…'; } else { const serverSide = grid.getPluginByName('serverSide'); if (serverSide) { const tree = grid.getPluginByName('tree'); const loaded = tree?.getFlattenedRows?.()?.length ?? '?'; statusEl.textContent = `Loaded ${loaded} rows (${serverSide.getTotalRowCount() ?? '?'} top-level nodes total)`; } }});Data flow:
- ServerSide fetches a block of top-level nodes → broadcasts
datasource:data - TreePlugin claims the data and flattens it into the row model
- Expand/collapse works locally for nodes with embedded children
- For lazy children: TreePlugin queries
datasource:fetch-children→ ServerSide callsgetChildRows()→ broadcastsdatasource:children→ TreePlugin renders children
Configuration Options
Section titled “Configuration Options”See TreeConfig for the full list of options and defaults.
Animation Options
Section titled “Animation Options”The animation option controls how child nodes appear/disappear:
// Slide animation (default)features: { tree: { animation: 'slide', ... } }
// Fade animationfeatures: { tree: { animation: 'fade', ... } }
// No animationfeatures: { tree: { animation: false, ... } }Tree Column
Section titled “Tree Column”By default, the tree toggle and indentation appear on the first visible column. Use treeColumn to target a specific column — useful when combining with pinned columns or when the first column is narrow:
features: { tree: { treeColumn: 'name', // Show tree toggle on the 'name' column childrenField: 'children', }, pinnedColumns: true,}Programmatic API
Section titled “Programmatic API”const plugin = grid.getPluginByName('tree');
plugin.expand(key); // Expand a node by keyplugin.collapse(key); // Collapse a node by keyplugin.toggle(key); // Toggle a node's expanded stateplugin.expandAll(); // Expand all nodesplugin.collapseAll(); // Collapse all nodesplugin.expandToKey(key); // Expand all ancestors to reveal a nodeconst keys = plugin.getExpandedKeys(); // Get currently expanded node keysconst expanded = plugin.isExpanded(key); // Check if a node is expandedconst rows = plugin.getFlattenedRows(); // Get flattened tree rows with metadataconst row = plugin.getRowByKey(key); // Find a row by its keyEvents
Section titled “Events”<tbw-grid style="height: 300px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/tree';
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'type', header: 'Type', width: 100 }, ], features: { tree: { childrenField: 'children' }, },};
grid.rows = [ { name: 'Engineering', type: 'Dept', children: [ { name: 'Alice Johnson', type: 'Employee' }, { name: 'Bob Smith', type: 'Employee' }, ], }, { name: 'Marketing', type: 'Dept', children: [ { name: 'Carol White', type: 'Employee' }, ], }, { name: 'Sales', type: 'Dept', children: [ { name: 'David Brown', type: 'Employee' }, { name: 'Eve Davis', type: 'Employee' }, ], },];
const log = document.querySelector('#tree-events-log');const clearBtn = document.querySelector('#clear-tree-events-log');
function addLog(type: string, detail: string) { if (!log) return; const msg = document.createElement('div'); msg.innerHTML = `<span class="event-type">[${type}]</span> ${detail}`; log.insertBefore(msg, log.firstChild); while (log.children.length > 15) log.lastChild?.remove();}
clearBtn?.addEventListener('click', () => { if (log) log.innerHTML = ''; });
grid.on('tree-expand', (d) => { addLog('tree-expand', `row ${d.rowIndex}, expanded: ${d.expanded}`);});tree-expand
Section titled “tree-expand”Fired when a node is expanded or collapsed (via click, keyboard, toggle(), expandAll(), or collapseAll()):
grid.on('tree-expand', ({ key, row, expanded, depth, expandedKeys }) => { console.log(`${expanded ? 'Expanded' : 'Collapsed'} node ${key} at depth ${depth}`); console.log(`${expandedKeys?.length ?? 0} total nodes expanded`);});See TreeExpandDetail for the full event payload type.
Styling
Section titled “Styling”The tree plugin supports CSS custom properties for theming. Override these on tbw-grid or a parent container:
CSS Custom Properties
Section titled “CSS Custom Properties”| Property | Default | Description |
|---|---|---|
--tbw-tree-toggle-size | 1.25em (~20px) | Toggle icon and spacer width |
--tbw-tree-indent-width | var(--tbw-tree-toggle-size) | Indentation per level (must be ≥ toggle size) |
--tbw-tree-accent | var(--tbw-color-accent) | Toggle icon hover color |
--tbw-animation-duration | 200ms | Expand/collapse animation |
--tbw-animation-easing | ease-out | Animation curve |
Example
Section titled “Example”tbw-grid { /* Custom tree styling */ --tbw-tree-toggle-size: 1.5em; /* Larger toggle icons */ --tbw-tree-indent-width: 1.75em; /* Slightly wider indent */ --tbw-tree-accent: #10b981; --tbw-animation-duration: 150ms;}CSS Classes
Section titled “CSS Classes”The tree plugin uses these class names:
| Class | Element |
|---|---|
.tree-cell-wrapper | Cell content wrapper with indentation |
.tree-expander | Expander cell container |
.tree-toggle | Expand/collapse icon |
.tree-spacer | Indent spacer element |
.tbw-tree-slide-in | Child row slide animation |
.tbw-tree-fade-in | Child row fade animation |
Plugin Compatibility
Section titled “Plugin Compatibility”A development-mode warning is shown if incompatible plugins are loaded together.
See Also
Section titled “See Also”- Master-Detail — Expand rows to show nested detail grids
- Server-Side Plugin — Lazy-load child nodes from the server