Using Tree Data
Starting with version 6.0.0
, Infinite Table has support for displaying tree data.
Note
To show tree data, you have to use:
- the
<TreeDataSource />
instead of<DataSource />
component - the
<TreeGrid />
instead of<InfiniteTable />
component.
Under the hood, those specialized components have better typing support for tree data, which will make it easier to work with them.
Note
To specify which column will have the expand/collapse icon, set the columns.renderTreeIcon
prop to true
for that column.
import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = { name: { field: 'name', renderTreeIcon: true, header: 'Name' }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { return ( <TreeDataSource nodesKey="children" primaryKey="id" data={dataSource}> <TreeGrid columns={columns} /> </TreeDataSource> ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', mimeType: 'text/plain', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); };
Throughout the docs for the TreeGrid, we will use an example data source that illustrates file system data, as that will be familiar to most people.
Terminology
When referring to rows in the TreeGrid, we'll prefer to use the term "node"
instead of "row". So whenever you see "node"
in the docs, you should know that it refers to a TreeGrid configuration of Infinite Table.
Also in the context of the TreeGrid, we'll use the term "node path"
instead of row id. The "node path"
is the array with the ids of all the parent nodes leading down to the current node. The node path includes the id of the current node.
const data = [
{ id: '1', name: 'Documents', // path: ['1']
children: [
{ id: '10', name: 'Private', // path: ['1', '10']
children: [
{ id: '100', name: 'Report.docx' }, // path: ['1', '10', '100']
{ id: '101', name: 'Vacation.docx' },// path: ['1', '10', '101']
],
},
]
},
{
id: '2',
name: 'Downloads', // path: ['2']
children: [
{
id: '20',
name: 'cat.jpg', // path: ['2', '20']
},
],
},
];
It's important to understand node paths, as that will be the primary way you'll interact with the TreeGrid/TreeDataSource.
Note
For the initial version of the TreeGrid, it's safer if your node ids are unique globally, but as we refine the TreeGrid, it will be safe to use ids unique only within a node children (so unique relative to siblings).
Parent vs leaf nodes
Nodes with an array for their nodesKey
property (defaults to "children"
) are considered parent nodes. All other nodes are leaf nodes.
When using the InfiniteTableRowInfo
type, you can check for isTreeNode
to determine if you're in a tree scenario. Also use the isParentNode
property to check if a node is a parent node or not.
Data format for the TreeDataSource
When using the <TreeDataSource />
component, the data you specify in your <DPropLink name="dataSource" />
should resolve to a nested array - with the nodesKey
containing the child items for each tree node.
<TreeDataSource
nodesKey="children"
primaryKey="id"
data={dataSource}
/>
With the nodesKey
set to "children"
, the <TreeDataSource />
will look for the children
property on each item in the data array, and use that to determine the child nodes for each tree node. Nodes without a "children"
property are assumed to be leaf nodes.
const dataSource = [
{
id: '1',
name: 'Documents',
children: [
{
id: '10',
name: 'Private',
children: [
{
id: '100',
name: 'Report.docx',
},
{
id: '101',
name: 'Vacation.docx',
},
],
},
],
},
{
id: '2',
name: 'Downloads',
children: [] // will be a parent node, with no children
},
];
Tree collapse and expand state
The <TreeDataSource />
component allows you to fully configure & control the collapse and expand state of the tree nodes, via the treeExpandState
/defaultTreeExpandState
props.
By default, if no expand state is specified, the tree will be rendered as fully expanded.
However, you can choose to specify the expand state with a default value and then with specific values for node paths (or node ids)
import { InfiniteTableColumn, TreeDataSource, TreeExpandStateValue, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = { name: { field: 'name', header: 'Name', renderTreeIcon: true }, type: { field: 'type', header: 'Type' }, extension: { field: 'extension', header: 'Extension' }, mimeType: { field: 'mimeType', header: 'Mime Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [treeExpandState, setTreeExpandState] = useState<TreeExpandStateValue>({ defaultExpanded: true, collapsedPaths: [['1', '10'], ['3']], }); return ( <> <TreeDataSource nodesKey="children" primaryKey="id" data={dataSource} treeExpandState={treeExpandState} onTreeExpandStateChange={setTreeExpandState} > <div style={{ color: 'var(--infinite-cell-color)', padding: '10px', }} > <button onClick={() => { setTreeExpandState({ defaultExpanded: true, collapsedPaths: [] }); }} > Expand all </button> <button onClick={() => { setTreeExpandState({ defaultExpanded: false, expandedPaths: [], }); }} > Collapse all </button> <div> Current tree expand state: <pre>{JSON.stringify(treeExpandState, null, 2)}</pre> </div> </div> <TreeGrid columns={columns} /> </TreeDataSource> </> ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); };
When using node paths for treeExpandState
, the object should have the following properties:
defaultExpanded
:boolean
- whether the tree nodes are expanded by default or not.collapsedPaths
:string[]
- whendefaultExpanded
istrue
, this is a mandatory prop.expandedPaths
:string[]
- whendefaultExpanded
isfalse
, this is a mandatory prop.
const treeExpandState = {
defaultExpanded: true,
collapsedPaths: [
['1', '10'],
['2', '20'],
['5']
],
expandedPaths: [
['1', '4'],
['5','nested node in 5'],
],
};
Note
As seen above, you can have a node specifically collapsed while other child nodes specifically expanded. So you can combine the expanded/collapsed paths to achieve very complex tree layouts, which can be restored later.
Working with horizontal layout
The wrapRowsHorizontally
prop can be used to enable horizontal layout, just like non-tree DataGrids.
import { InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; extension?: string; mimeType?: string; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = { name: { field: 'name', renderTreeIcon: true, header: 'Name' }, type: { field: 'type', header: 'Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size' }, }; export default function App() { const [wrapRowsHorizontally, setWrapRowsHorizontally] = useState(false); return ( <> <div style={{ color: 'var(--infinite-cell-color)', padding: '10px', }} > <button onClick={() => setWrapRowsHorizontally(!wrapRowsHorizontally)}> Toggle horizontal layout </button> </div> <TreeDataSource nodesKey="children" primaryKey="id" data={dataSource}> <TreeGrid columns={columns} wrapRowsHorizontally={wrapRowsHorizontally} columnDefaultWidth={100} /> </TreeDataSource> </> ); } const dataSource = () => { const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Report.docx', sizeInKB: 210, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file', extension: 'docx', mimeType: 'application/msword', }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file', extension: 'pdf', mimeType: 'application/pdf', }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file', extension: 'txt', mimeType: 'text/plain', }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '30', name: 'Music - empty', sizeInKB: 0, type: 'folder', children: [], }, { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, { id: '311', name: 'WinterVacation.mp4', sizeInKB: 245, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, { id: '312', name: 'SummerVacation.mp4', sizeInKB: 1259, type: 'file', extension: 'mp4', mimeType: 'video/mp4', }, ], }, ], }, ]; return Promise.resolve(nodes); };