Using tree selection
When using a tree grid, a common use-case is to allow users to select nodes, both parent and child nodes.
The
<TreeDataSource> component allows you to specify an initial tree selection, via the defaultTreeSelection prop.When using
defaultTreeSelection or its controlled counterpart treeSelection, if no selectionMode is specified, the selection mode will default to "multi-row".If you enable selection, don't forget to specify which column should render a selection checkbox, by using
renderSelectionCheckBox=true. Example of tree selection value with default selection set to false
import type { TreeSelectionValue } from '@infinite-table/infinite-react';
// Default selection is false, with some selected node paths: ['1'] and ['2', '20']
// however, node ['1', '10'] is deselected
const treeSelectioDefaultDeselected: TreeSelectionValue = {
defaultSelection: false,
selectedPaths: [['1'], ['2', '20']],
deselectedPaths: [['1', '10']],
}; COPY
Example of tree selection value with default selection set to true
// Default selection is true, with some deselected node paths: ['2'] and ['3']
// however, inside ['3'], we have a selected node ['3','30','301']
const treeSelectionDefaultSelected: TreeSelectionValue = {
defaultSelection: true,
deselectedPaths: [['2'], ['3']],
selectedPaths: [['3','30','301']],
}; COPY
View Mode
Fork Forkimport { DataSourceApi, InfiniteTableColumn, TreeDataSource, TreeGrid, TreeSelectionValue, } 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, renderSelectionCheckBox: 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)' }, }; const defaultTreeSelection: TreeSelectionValue = { defaultSelection: true, deselectedPaths: [ ['1', '10'], ['3', '31'], ], selectedPaths: [['3']], }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState<DataSourceApi<FileSystemNode> | null>(); return ( <> <TreeDataSource onReady={setDataSourceApi} nodesKey="children" primaryKey="id" data={dataSource} defaultTreeSelection={defaultTreeSelection} > <div style={{ color: 'var(--infinite-cell-color)', padding: 10, display: 'flex', gap: 10, }} > <button onClick={() => { dataSourceApi!.treeApi.selectAll(); }} > Select all </button> <button onClick={() => { dataSourceApi!.treeApi.deselectAll(); }} > Deselect all </button> </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: '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 uncontrolled tree selection, the
<TreeDataSource /> will manage the selection state internally, and will update it as a result of user actions. If you want to change the selection, you can use the Tree API to do so: selectNode, deselectNode, selectAll, deselectAll, etc.Selecting nodes with the mouse and keyboard#
Users select nodes just like rows in a grouped table - see using your mouse and keyboard to select rows. Parent nodes work like group rows:
- clicking a node selects it, and only it - clicking a parent node selects all its nodes;
cmd/ctrl+ click toggles a node, keeping the rest of the selection;shift+ click selects the range of nodes displayed from the last clicked node to the clicked one. A collapsed parent node in the range is selected with all its nodes, while an expanded one follows its children - only its nodes inside the range are selected. The starting node keeps what its own click selected, so a parent node stays selected with all its nodes;- the spacebar works like a click on the active row, and
shift+ spacebar selects a range.
The
multiRowSelectionWith prop decides whether clicks select nodes while a visible column shows a selection checkbox. With 'checkbox' (the default), users select nodes with the checkboxes (shift + a checkbox selects a range) and the spacebar toggles the active node. With 'click', clicks select nodes as well.Clicking the expand/collapse icon of a parent node doesn't change the selection, and nodes that can't be selected - like read-only parent nodes (see
isNodeReadOnly) - are skipped.With single-row selection, clicking a node - a parent node too - selects it, while
cmd/ctrl + click and the spacebar toggle it.Click
Report.docx, then shift + click unknown.txt - the collapsed Private folder is selected with all its files. Use the buttons to show the selection checkboxes and to choose how users select nodes.View Mode
Fork Forkimport { InfiniteTableColumn, InfiniteTablePropMultiRowSelectionWith, TreeDataSource, TreeExpandStateValue, TreeGrid, TreeSelectionValue, } from '@infinite-table/infinite-react'; import { useMemo, useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; sizeInKB: number; children?: FileSystemNode[]; }; const defaultTreeSelection: TreeSelectionValue = { defaultSelection: false, selectedPaths: [], }; const defaultTreeExpandState: TreeExpandStateValue = { defaultExpanded: true, collapsedPaths: [['1', '10']], }; export default function App() { const [checkbox, setCheckbox] = useState(false); const [selectWith, setSelectWith] = useState<InfiniteTablePropMultiRowSelectionWith>('checkbox'); const [selectedFiles, setSelectedFiles] = useState<string[]>([]); const columns = useMemo<Record<string, InfiniteTableColumn<FileSystemNode>>>( () => ({ name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: checkbox, }, type: { field: 'type', header: 'Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }), [checkbox], ); return ( <> <div style={{ color: 'var(--infinite-cell-color)', padding: 10, display: 'flex', flexWrap: 'wrap', gap: 10, }} > <button onClick={() => setCheckbox(!checkbox)}> {checkbox ? 'Hide' : 'Show'} selection checkboxes </button> <button onClick={() => setSelectWith(selectWith === 'click' ? 'checkbox' : 'click') } > multiRowSelectionWith: {selectWith} </button> </div> <div style={{ color: 'var(--infinite-cell-color)', padding: 10 }}> Selected files: {selectedFiles.join(', ') || 'none'} </div> <TreeDataSource<FileSystemNode> nodesKey="children" primaryKey="id" data={nodes} selectionMode="multi-row" defaultTreeExpandState={defaultTreeExpandState} defaultTreeSelection={defaultTreeSelection} onTreeSelectionChange={(_selection, { treeSelectionState }) => { setSelectedFiles( treeSelectionState .getSelectedLeafNodePaths() .map((path) => names[path[path.length - 1]]), ); }} > <TreeGrid columns={columns} multiRowSelectionWith={selectWith} /> </TreeDataSource> </> ); } const nodes: FileSystemNode[] = [ { id: '1', name: 'Documents', sizeInKB: 1200, type: 'folder', children: [ { id: '11', name: 'Report.docx', sizeInKB: 210, type: 'file' }, { id: '10', name: 'Private', sizeInKB: 100, type: 'folder', children: [ { id: '100', name: 'Vacation.docx', sizeInKB: 120, type: 'file' }, { id: '101', name: 'CV.pdf', sizeInKB: 108, type: 'file' }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file' }, { id: '21', name: 'notes.txt', sizeInKB: 20, type: 'file' }, ], }, { id: '3', name: 'Media', sizeInKB: 1000, type: 'folder', children: [ { id: '31', name: 'Videos', sizeInKB: 5400, type: 'folder', children: [ { id: '310', name: 'Vacation.mp4', sizeInKB: 108, type: 'file' }, ], }, ], }, ]; const names: Record<string, string> = {}; const collectNames = (list: FileSystemNode[]) => list.forEach((node) => { names[node.id] = node.name; if (node.children) { collectNames(node.children); } }); collectNames(nodes);
Selecting nodes when the tree is filtered#
When some nodes are filtered out (see
treeFilterFunction), use the multiRowSelectionTarget prop to decide which nodes the header checkbox, the parent node checkboxes and selectAll/deselectAll act on:'all'(the default) - they act on all the leaf nodes, including the ones that are filtered out. A parent node checkbox is only checked when all its leaf nodes are selected.'visible'- they act only on the leaf nodes that pass the filter. Filtered out nodes keep their current selection. A parent node checkbox is checked when all its visible leaf nodes are selected.
When no nodes are filtered out, both values behave the same.
The tree is filtered to only show
.docx files. Select a folder or click "Select all", then clear the filter to see which files were selected with each multiRowSelectionTarget value.View Mode
Fork Forkimport { DataSourceApi, DataSourcePropMultiRowSelectionTarget, InfiniteTableColumn, TreeDataSource, TreeGrid, } from '@infinite-table/infinite-react'; import { useCallback, useState } from 'react'; type FileSystemNode = { id: string; name: string; type: 'folder' | 'file'; sizeInKB: number; children?: FileSystemNode[]; }; const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = { name: { field: 'name', header: 'Name', renderTreeIcon: true, renderSelectionCheckBox: true, }, type: { field: 'type', header: 'Type' }, size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' }, }; export default function App() { const [dataSourceApi, setDataSourceApi] = useState<DataSourceApi<FileSystemNode> | null>(); const [target, setTarget] = useState<DataSourcePropMultiRowSelectionTarget>('visible'); const [filter, setFilter] = useState('.docx'); const treeFilterFunction = useCallback( ({ data, filterTreeNode, }: { data: FileSystemNode; filterTreeNode: (data: FileSystemNode) => FileSystemNode | boolean; }) => { if (!Array.isArray(data.children)) { return data.name.toLowerCase().includes(filter.toLowerCase()); } return filterTreeNode(data); }, [filter], ); return ( <> <TreeDataSource onReady={setDataSourceApi} nodesKey="children" primaryKey="id" data={dataSource} selectionMode="multi-row" multiRowSelectionTarget={target} treeFilterFunction={filter ? treeFilterFunction : undefined} > <div style={{ color: 'var(--infinite-cell-color)', padding: 10, display: 'flex', gap: 10, alignItems: 'center', }} > <label> multiRowSelectionTarget:{' '} <select value={target} onChange={(e) => setTarget( e.target.value as DataSourcePropMultiRowSelectionTarget, ) } > <option value="all">all</option> <option value="visible">visible</option> </select> </label> <input placeholder="Filter files by name" value={filter} onChange={(e) => setFilter(e.target.value)} /> <button onClick={() => dataSourceApi!.treeApi.selectAll()}> Select all </button> <button onClick={() => dataSourceApi!.treeApi.deselectAll()}> Deselect all </button> </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' }, { id: '101', name: 'Vacation.docx', sizeInKB: 120, type: 'file' }, { id: '102', name: 'CV.pdf', sizeInKB: 108, type: 'file' }, ], }, ], }, { id: '2', name: 'Desktop', sizeInKB: 1000, type: 'folder', children: [ { id: '20', name: 'unknown.txt', sizeInKB: 100, type: 'file' }, { id: '21', name: 'Notes.docx', sizeInKB: 50, type: 'file' }, ], }, ]; return Promise.resolve(nodes); };
Reacting to user actions#
To listen to selection changes, you can use the
onTreeSelectionChange callback.This callback is called both when the user interacts with the grid, and when you use the Tree API to change the selection.
Using controlled tree selection#
When using the controlled
treeSelection prop, you have to make sure you update the tree selection via onTreeSelectionChange.Controlled tree selection also gives you a more declarative way to manage the selection state.
You no longer have to call Tree API methods to change the selection. Simply pass a new tree selection state object to the
treeSelection prop and the tree grid will be updated accordingly.For example, if you want to select all nodes, set the
treeSelection prop to: Tree selection value to show all nodes as selected
{
defaultSelection: true,
deselectedPaths: [],
} COPY
For deselecting all nodes, the value should be:
All nodes as deselected
{
defaultSelection: false,
selectedPaths: [],
} COPY
Using controlled tree selection also gives you an easy way to restore a previously saved tree selection at any point in time.
View Mode
Fork Forkimport { InfiniteTableColumn, TreeDataSource, TreeGrid, TreeSelectionValue, } 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, renderSelectionCheckBox: 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 [treeSelection, setTreeSelection] = useState<TreeSelectionValue>({ defaultSelection: false, selectedPaths: [['1', '10'], ['3']], }); return ( <> <TreeDataSource nodesKey="children" primaryKey="id" data={dataSource} treeSelection={treeSelection} onTreeSelectionChange={setTreeSelection} > <div style={{ color: 'var(--infinite-cell-color)', padding: '10px', }} > <button onClick={() => { setTreeSelection({ defaultSelection: false, selectedPaths: [], }); }} > Deselect all </button> <button onClick={() => { setTreeSelection({ defaultSelection: true, deselectedPaths: [], }); }} > Select all </button> <div style={{ overflow: 'auto', height: '10vh', minHeight: '200px', }} > Current tree selection: <pre>{JSON.stringify(treeSelection, 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: '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); };