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.

Basic TreeGrid example
View Mode
Fork
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.

Node path vs row id
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.

Using the nodesKey prop to specify where the node children are
<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.

Nested data structure for the TreeDataSource component
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)

Using controlled tree expand state
View Mode
Fork
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[] - when defaultExpanded is true, this is a mandatory prop.
  • expandedPaths: string[] - when defaultExpanded is false, this is a mandatory prop.
Example of treeExpandState with node paths
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.

TreeGrid with horizontal layout
View Mode
Fork
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);
};