Skip to content

Tree Data Grid

The TreeData Plugin transforms parent-child relationships data into a hierarchical tree structure and provides the ability to group rows by parentId and is optimized for the best performance, making it suitable for large datasets. It enables features such as expandable rows, and level indicators, making it ideal for applications that handle nested data.

Source code
TypeScript ts
// src/components/tree/Tree.ts

import { defineCustomElements } from '@revolist/revogrid/loader';
import {
  TreeDataPlugin,
  RowOrderPlugin,
  AdvanceFilterPlugin,
  ExportExcelPlugin,
  RowSelectPlugin,
  RowOddPlugin,
  ColumnStretchPlugin,
  StickyCellsPlugin,
  avatarWithTextRenderer,
  type ExportExcelEvent,
  TREE_EXPAND_ALL_EVENT,
  TREE_COLLAPSE_ALL_EVENT,
} from '@revolist/revogrid-pro';
import { ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
defineCustomElements();

import { currentTheme } from '../composables/useRandomData';
import { makeTreeRows } from './tree.avatar';
import { createTreeJobTitleColumn } from './tree.job-title';
import { createTreeSalaryColumn } from './tree.salary';
const { isDark } = currentTheme();

const BTN_CLASS = 'rv-btn';
const TOOLBAR_CLASS = 'flex gap-2 p-2';
const TREE_EXPORT_CONFIG: ExportExcelEvent = {
  sheetName: 'Tree Data',
  workbookName: 'tree-data.xlsx',
};

export function load(parentSelector: string) {
  const container = document.createElement('div');
  container.className = 'flex flex-col gap-2 grow';

  // Buttons
  const actions = document.createElement('div');
  actions.className = TOOLBAR_CLASS;

  const expandBtn = document.createElement('button');
  expandBtn.textContent = 'Expand All';
  expandBtn.className = BTN_CLASS;

  const collapseBtn = document.createElement('button');
  collapseBtn.textContent = 'Collapse All';
  collapseBtn.className = BTN_CLASS;

  const exportBtn = document.createElement('button');
  exportBtn.textContent = 'Export to Excel';
  exportBtn.className = BTN_CLASS;

  const stickyLabel = document.createElement('label');
  stickyLabel.className = 'flex items-center gap-1';
  const stickyInput = document.createElement('input');
  stickyInput.type = 'checkbox';
  stickyInput.checked = true;
  stickyLabel.appendChild(stickyInput);
  stickyLabel.appendChild(document.createTextNode('Sticky rows'));

  actions.appendChild(expandBtn);
  actions.appendChild(collapseBtn);
  actions.appendChild(exportBtn);
  actions.appendChild(stickyLabel);
  container.appendChild(actions);

  // Grid
  const grid = document.createElement('revo-grid');
  grid.range = true;
  grid.resize = true;

  const rows = makeTreeRows();

  grid.columns = [
    {
      name: 'Full Name',
      prop: 'fullName',
      size: 300,
      tree: true,
      rowSelect: true,
      rowDrag: true,
      sortable: true,
      filter: ['selection'],
      avatarProp: 'avatar',
      avatarLabelProp: 'fullName',
      avatarIndexProp: 'id',
      avatarSize: 20,
      cellTemplate: avatarWithTextRenderer,
      cellProperties: ({ model }) => ({
        subRow: !!model.parentId,
      }),
    },
    {
      name: 'Flag',
      prop: 'flag',
      size: 80,
      cellProperties: ({ model }) => ({
        subRow: !!model.parentId,
      }),
    },
    createTreeJobTitleColumn(),
    createTreeSalaryColumn(),
  ];

  grid.plugins = [
    TreeDataPlugin,
    RowOrderPlugin,
    AdvanceFilterPlugin,
    ExportExcelPlugin,
    RowSelectPlugin,
    RowOddPlugin,
    ColumnStretchPlugin,
    StickyCellsPlugin,
  ];
  grid.columnTypes = ECOMMERCE_COLUMNS_TYPES;
  Object.assign(grid, {
    tree: {
      expandedRowIds: new Set([rows[0].id]),
      stickyParents: true,
      animation: true,
    },
  })
  grid.theme = isDark() ? 'darkMaterial' : 'material';
  grid.hideAttribution = true;
  grid.stretch = true;

  expandBtn.onclick = () => grid.dispatchEvent(new CustomEvent(TREE_EXPAND_ALL_EVENT));
  collapseBtn.onclick = () => grid.dispatchEvent(new CustomEvent(TREE_COLLAPSE_ALL_EVENT));
  exportBtn.onclick = async () => {
    exportBtn.textContent = 'Exporting...';
    exportBtn.setAttribute('disabled', '');
    try {
      const plugins = await grid.getPlugins();
      const exportPlugin = plugins.find((plugin) => plugin instanceof ExportExcelPlugin) as ExportExcelPlugin | undefined;
      await exportPlugin?.export(TREE_EXPORT_CONFIG);
    } finally {
      exportBtn.textContent = 'Export to Excel';
      exportBtn.removeAttribute('disabled');
    }
  };
  stickyInput.onchange = () => {
    grid.tree = {
      ...grid.tree,
      stickyParents: stickyInput.checked,
    };
  };

  container.appendChild(grid);
  document.querySelector(parentSelector)?.appendChild(container);
  grid.source = rows;

  return () => container.remove();
}
Vue vue
// src/components/tree/Tree.vue

<template>
  <div class="flex flex-col gap-2 grow">
    <div class="flex gap-2 p-2">
      <button class="rv-btn" @click="expandAll">
        Expand All
      </button>
      <button class="rv-btn" @click="collapseAll">
        Collapse All
      </button>
      <button class="rv-btn" type="button" :disabled="exporting" @click="exportToExcel">
        {{ exporting ? 'Exporting...' : 'Export to Excel' }}
      </button>
      <label class="flex items-center gap-1">
        <input v-model="stickyRows" type="checkbox" />
        Sticky rows
      </label>
    </div>
    <RevoGrid
      ref="gridRef"
      class="border rounded-lg grow"
      range
      :theme="isDark ? 'darkMaterial' : 'material'"
      :columns="columns"
      :source="rows"
      :plugins="plugins"
      :tree="treeConfig"
      :sticky-cells="stickyCellsConfig"
      :column-types="ECOMMERCE_COLUMNS_TYPES"
      :stretch="true"
      hide-attribution
      resize
    />
  </div>
</template>

<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import { currentThemeVue } from '../composables/useRandomData';
import { makeTreeRows } from './tree.avatar';
import RevoGrid, { type ColumnRegular } from '@revolist/vue3-datagrid';
import {
  TreeDataPlugin,
  RowOrderPlugin,
  AdvanceFilterPlugin,
  ExportExcelPlugin,
  RowSelectPlugin,
  RowOddPlugin,
  ColumnStretchPlugin,
  StickyCellsPlugin,
  avatarWithTextRenderer,
  type ExportExcelEvent,
  TREE_EXPAND_ALL_EVENT,
  TREE_COLLAPSE_ALL_EVENT,
} from '@revolist/revogrid-pro';
import { ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
import { createTreeJobTitleColumn } from './tree.job-title';
import { createTreeSalaryColumn } from './tree.salary';

const { isDark } = currentThemeVue();

const gridRef = ref<{ $el: HTMLRevoGridElement } | null>(null);
const exporting = ref(false);
const TREE_EXPORT_CONFIG: ExportExcelEvent = {
  sheetName: 'Tree Data',
  workbookName: 'tree-data.xlsx',
};

const columns = shallowRef<ColumnRegular[]>([
  {
    name: 'Full Name',
    prop: 'fullName',
    size: 300,
    tree: true,
    rowSelect: true,
    rowDrag: true,
    sortable: true,
    filter: ['selection'],
    avatarProp: 'avatar',
    avatarLabelProp: 'fullName',
    avatarIndexProp: 'id',
    avatarSize: 20,
    cellTemplate: avatarWithTextRenderer,
    cellProperties: ({ model }) => ({
      subRow: !!model.parentId,
    })
  },
  {
    name: 'Flag',
    prop: 'flag',
    size: 80,
    cellProperties: ({ model }) => ({
      subRow: !!model.parentId,
    })
  },
  createTreeJobTitleColumn(),
  createTreeSalaryColumn(),
]);

const plugins = shallowRef([
  TreeDataPlugin,
  RowOrderPlugin,
  AdvanceFilterPlugin,
  ExportExcelPlugin,
  RowSelectPlugin,
  RowOddPlugin,
  ColumnStretchPlugin,
  StickyCellsPlugin,
]);

const rows = shallowRef(makeTreeRows());
const stickyRows = ref(true);

const treeConfig = computed(() => ({
  expandedRowIds: new Set([rows.value[0].id]),
  stickyParents: stickyRows.value,
  animation: true,
}));

const stickyCellsConfig = shallowRef({
  maxRows: 2,
});

function expandAll() {
  gridRef.value?.$el.dispatchEvent(new CustomEvent(TREE_EXPAND_ALL_EVENT));
}

function collapseAll() {
  gridRef.value?.$el.dispatchEvent(new CustomEvent(TREE_COLLAPSE_ALL_EVENT));
}

async function exportToExcel() {
  const grid = gridRef.value?.$el;
  if (!grid) {
    return;
  }

  exporting.value = true;
  try {
    const plugins = await grid.getPlugins();
    const exportPlugin = plugins.find(
      (plugin) => plugin instanceof ExportExcelPlugin,
    ) as ExportExcelPlugin | undefined;
    await exportPlugin?.export(TREE_EXPORT_CONFIG);
  } finally {
    exporting.value = false;
  }
}
</script>
React tsx
// src/components/tree/Tree.tsx

import { useState, useMemo, useRef } from 'react';
import { RevoGrid, type DataType, type ColumnRegular } from '@revolist/react-datagrid';
import {
  TreeDataPlugin,
  RowOrderPlugin,
  AdvanceFilterPlugin,
  ExportExcelPlugin,
  RowSelectPlugin,
  RowOddPlugin,
  ColumnStretchPlugin,
  StickyCellsPlugin,
  avatarWithTextRenderer,
  type ExportExcelEvent,
  TREE_EXPAND_ALL_EVENT,
  TREE_COLLAPSE_ALL_EVENT,
} from '@revolist/revogrid-pro';
import { ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
import { currentTheme } from '../composables/useRandomData';
import { makeTreeRows } from './tree.avatar';
import { createTreeJobTitleColumn } from './tree.job-title';
import { createTreeSalaryColumn } from './tree.salary';

const BTN_CLASS = 'rv-btn';
const TOOLBAR_CLASS = 'flex gap-2 p-2';
const TREE_EXPORT_CONFIG: ExportExcelEvent = {
  sheetName: 'Tree Data',
  workbookName: 'tree-data.xlsx',
};

function Tree() {
  const { isDark } = currentTheme();
  const gridRef = useRef<HTMLRevoGridElement>(null);

  const [source] = useState<DataType[]>(makeTreeRows);
  const [stickyRows, setStickyRows] = useState(true);
  const [exporting, setExporting] = useState(false);

  const columns = useMemo<ColumnRegular[]>(
    () => [
      {
        name: 'Full Name',
        prop: 'fullName',
        size: 300,
        tree: true,
        rowSelect: true,
        rowDrag: true,
        sortable: true,
        filter: ['selection'],
        avatarProp: 'avatar',
        avatarLabelProp: 'fullName',
        avatarIndexProp: 'id',
        avatarSize: 20,
        cellTemplate: avatarWithTextRenderer,
        cellProperties: ({ model }) => ({
          subRow: !!model.parentId,
        }),
      },
      {
        name: 'Flag',
        prop: 'flag',
        size: 80,
        cellProperties: ({ model }) => ({
          subRow: !!model.parentId,
        }),
      },
      createTreeJobTitleColumn(),
      createTreeSalaryColumn(),
    ],
    []
  );

  const plugins = useMemo(() => [TreeDataPlugin, RowOrderPlugin, AdvanceFilterPlugin, ExportExcelPlugin, RowSelectPlugin, RowOddPlugin, ColumnStretchPlugin, StickyCellsPlugin], []);

  const tree = useMemo(() => ({
    expandedRowIds: new Set([source[0]?.id]),
    stickyParents: stickyRows,
    animation: true,
  }), [source, stickyRows]);

  const expandAll = () => gridRef.current?.dispatchEvent(new CustomEvent(TREE_EXPAND_ALL_EVENT));
  const collapseAll = () => gridRef.current?.dispatchEvent(new CustomEvent(TREE_COLLAPSE_ALL_EVENT));
  const exportToExcel = async () => {
    if (!gridRef.current) {
      return;
    }

    setExporting(true);
    try {
      const gridPlugins = await gridRef.current.getPlugins();
      const exportPlugin = gridPlugins.find(
        (plugin) => plugin instanceof ExportExcelPlugin,
      ) as ExportExcelPlugin | undefined;
      await exportPlugin?.export(TREE_EXPORT_CONFIG);
    } finally {
      setExporting(false);
    }
  };

  return (
    <div className="flex flex-col gap-2 grow">
      <div className={TOOLBAR_CLASS}>
        <button className={BTN_CLASS} onClick={expandAll}>Expand All</button>
        <button className={BTN_CLASS} onClick={collapseAll}>Collapse All</button>
        <button className={BTN_CLASS} type="button" disabled={exporting} onClick={exportToExcel}>
          {exporting ? 'Exporting...' : 'Export to Excel'}
        </button>
        <label className="flex items-center gap-1">
          <input
            type="checkbox"
            checked={stickyRows}
            onChange={(event) => setStickyRows(event.currentTarget.checked)}
          />
          Sticky rows
        </label>
      </div>
      <RevoGrid
        ref={gridRef}
        className="border rounded-lg grow"
        range
        resize
        theme={isDark() ? 'darkMaterial' : 'material'}
        columns={columns}
        source={source}
        plugins={plugins}
        tree={tree}
        columnTypes={ECOMMERCE_COLUMNS_TYPES}
        stretch
        hideAttribution
      />
    </div>
  );
}

export default Tree;
Angular ts
import { Component, ViewChild, ElementRef, ViewEncapsulation, NO_ERRORS_SCHEMA } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
  TreeDataPlugin,
  RowOrderPlugin,
  AdvanceFilterPlugin,
  ExportExcelPlugin,
  RowSelectPlugin,
  RowOddPlugin,
  ColumnStretchPlugin,
  StickyCellsPlugin,
  avatarWithTextRenderer,
  type ExportExcelEvent,
  TREE_EXPAND_ALL_EVENT,
  TREE_COLLAPSE_ALL_EVENT,
} from '@revolist/revogrid-pro';
import { ECOMMERCE_COLUMNS_TYPES } from '../sys-data/ecommerce.columns';
import { currentTheme } from '../composables/useRandomData';
import { makeTreeRows } from './tree.avatar';
import { createTreeJobTitleColumn } from './tree.job-title';
import { createTreeSalaryColumn } from './tree.salary';

const BTN_CLASS = 'rv-btn';
const TOOLBAR_CLASS = 'flex gap-2 p-2';
const TREE_EXPORT_CONFIG: ExportExcelEvent = {
  sheetName: 'Tree Data',
  workbookName: 'tree-data.xlsx',
};

@Component({
  selector: 'tree-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <div class="flex flex-col gap-2 grow">
      <div [class]="toolbarClass">
        <button [class]="btnClass" (click)="expandAll()">Expand All</button>
        <button [class]="btnClass" (click)="collapseAll()">Collapse All</button>
        <button [class]="btnClass" type="button" [disabled]="exporting" (click)="exportToExcel()">
          {{ exporting ? 'Exporting...' : 'Export to Excel' }}
        </button>
        <label class="flex items-center gap-1">
          <input type="checkbox" [checked]="stickyRows" (change)="setStickyRows($event)" />
          Sticky rows
        </label>
      </div>
      <revo-grid
        #grid
        class="border rounded-lg grow"
        [range]="true"
        [resize]="true"
        [theme]="theme"
        [columns]="columns"
        [source]="rows"
        [plugins]="plugins"
        [tree]="treeConfig"
        [columnTypes]="columnTypes"
        [hideAttribution]="true"
      ></revo-grid>
    </div>
  `,
  encapsulation: ViewEncapsulation.None,
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
})
export class TreeGridComponent {
  @ViewChild('grid', { read: ElementRef }) gridElement!: ElementRef;

  btnClass = BTN_CLASS;
  toolbarClass = TOOLBAR_CLASS;
  theme = currentTheme().isDark() ? 'darkMaterial' : 'material';

  rows = makeTreeRows();

  columns = [
    {
      name: 'Full Name',
      prop: 'fullName',
      size: 300,
      tree: true,
      rowSelect: true,
      rowDrag: true,
      sortable: true,
      filter: ['selection'],
      avatarProp: 'avatar',
      avatarLabelProp: 'fullName',
      avatarIndexProp: 'id',
      avatarSize: 20,
      cellTemplate: avatarWithTextRenderer,
      cellProperties: ({ model }: any) => ({
        subRow: !!model.parentId,
      }),
    },
    {
      name: 'Flag',
      prop: 'flag',
      size: 80,
      cellProperties: ({ model }: any) => ({
        subRow: !!model.parentId,
      }),
    },
    createTreeJobTitleColumn(),
    createTreeSalaryColumn(),
  ];

  plugins = [
    TreeDataPlugin,
    RowOrderPlugin,
    AdvanceFilterPlugin,
    ExportExcelPlugin,
    RowSelectPlugin,
    RowOddPlugin,
    ColumnStretchPlugin,
    StickyCellsPlugin,
  ];
  columnTypes = ECOMMERCE_COLUMNS_TYPES;
  stickyRows = true;
  exporting = false;

  treeConfig = {
    expandedRowIds: new Set([this.rows[0]?.id]),
    stickyParents: true,
    animation: true,
  };

  expandAll() {
    this.gridElement.nativeElement.dispatchEvent(new CustomEvent(TREE_EXPAND_ALL_EVENT));
  }

  collapseAll() {
    this.gridElement.nativeElement.dispatchEvent(new CustomEvent(TREE_COLLAPSE_ALL_EVENT));
  }

  async exportToExcel() {
    this.exporting = true;
    try {
      const plugins = await this.gridElement.nativeElement.getPlugins();
      const exportPlugin = plugins.find(
        (plugin: unknown) => plugin instanceof ExportExcelPlugin,
      ) as ExportExcelPlugin | undefined;
      await exportPlugin?.export(TREE_EXPORT_CONFIG);
    } finally {
      this.exporting = false;
    }
  }

  setStickyRows(event: Event) {
    this.stickyRows = (event.target as HTMLInputElement).checked;
    this.treeConfig = {
      ...this.treeConfig,
      stickyParents: this.stickyRows,
    };
  }
}
Data json
[
  {
    "id": "root-01",
    "parentId": null,
    "avatar": "SM",
    "firstName": "Shaylee",
    "lastName": "MacGyver",
    "fullName": "Shaylee MacGyver"
  },
  {
    "id": "root-01-group-01",
    "parentId": "root-01",
    "avatar": "JB",
    "firstName": "Jettie",
    "lastName": "Bergnaum",
    "fullName": "Jettie Bergnaum"
  },
  {
    "id": "root-01-group-01-team-01",
    "parentId": "root-01-group-01",
    "avatar": "DU",
    "firstName": "Dangelo",
    "lastName": "Upton",
    "fullName": "Dangelo Upton"
  },
  {
    "id": "root-01-group-01-team-01-member-01",
    "parentId": "root-01-group-01-team-01",
    "avatar": "HS",
    "firstName": "Hector",
    "lastName": "Schumm",
    "fullName": "Hector Schumm"
  },
  {
    "id": "root-01-group-01-team-01-member-02",
    "parentId": "root-01-group-01-team-01",
    "avatar": "ER",
    "firstName": "Eleanora",
    "lastName": "Ruecker",
    "fullName": "Eleanora Ruecker"
  },
  {
    "id": "root-01-group-01-team-01-member-03",
    "parentId": "root-01-group-01-team-01",
    "avatar": "SM",
    "firstName": "Shaylee",
    "lastName": "MacGyver",
    "fullName": "Shaylee MacGyver"
  },
  {
    "id": "root-01-group-01-team-01-member-04",
    "parentId": "root-01-group-01-team-01",
    "avatar": "IM",
    "firstName": "Ines",
    "lastName": "Metz",
    "fullName": "Ines Metz"
  },
  {
    "id": "root-01-group-01-team-01-member-05",
    "parentId": "root-01-group-01-team-01",
    "avatar": "JB",
    "firstName": "Jettie",
    "lastName": "Bergnaum",
    "fullName": "Jettie Bergnaum"
  },
  {
    "id": "root-01-group-02",
    "parentId": "root-01",
    "avatar": "LP",
    "firstName": "Liliane",
    "lastName": "Predovic",
    "fullName": "Liliane Predovic"
  },
  {
    "id": "root-01-group-02-team-01",
    "parentId": "root-01-group-02",
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-01-group-02-team-01-member-01",
    "parentId": "root-01-group-02-team-01",
    "avatar": "LP",
    "firstName": "Liliane",
    "lastName": "Predovic",
    "fullName": "Liliane Predovic"
  },
  {
    "id": "root-01-group-02-team-01-member-02",
    "parentId": "root-01-group-02-team-01",
    "avatar": "FK",
    "firstName": "Federico",
    "lastName": "Kertzmann",
    "fullName": "Federico Kertzmann"
  },
  {
    "id": "root-01-group-02-team-01-member-03",
    "parentId": "root-01-group-02-team-01",
    "avatar": "DU",
    "firstName": "Dangelo",
    "lastName": "Upton",
    "fullName": "Dangelo Upton"
  },
  {
    "id": "root-01-group-02-team-01-member-04",
    "parentId": "root-01-group-02-team-01",
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-01-group-02-team-01-member-05",
    "parentId": "root-01-group-02-team-01",
    "avatar": "LL",
    "firstName": "Libbie",
    "lastName": "Lynch",
    "fullName": "Libbie Lynch"
  },
  {
    "id": "root-02",
    "parentId": null,
    "avatar": "LP",
    "firstName": "Liliane",
    "lastName": "Predovic",
    "fullName": "Liliane Predovic"
  },
  {
    "id": "root-02-group-01",
    "parentId": "root-02",
    "avatar": "LP",
    "firstName": "Liliane",
    "lastName": "Predovic",
    "fullName": "Liliane Predovic"
  },
  {
    "id": "root-02-group-01-team-01",
    "parentId": "root-02-group-01",
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-02-group-01-team-01-member-01",
    "parentId": "root-02-group-01-team-01",
    "avatar": "DU",
    "firstName": "Dangelo",
    "lastName": "Upton",
    "fullName": "Dangelo Upton"
  },
  {
    "id": "root-02-group-01-team-01-member-02",
    "parentId": "root-02-group-01-team-01",
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-02-group-01-team-01-member-03",
    "parentId": "root-02-group-01-team-01",
    "avatar": "LL",
    "firstName": "Libbie",
    "lastName": "Lynch",
    "fullName": "Libbie Lynch"
  },
  {
    "id": "root-02-group-01-team-01-member-04",
    "parentId": "root-02-group-01-team-01",
    "avatar": "MH",
    "firstName": "Mervin",
    "lastName": "Howell",
    "fullName": "Mervin Howell"
  },
  {
    "id": "root-02-group-01-team-01-member-05",
    "parentId": "root-02-group-01-team-01",
    "avatar": "WF",
    "firstName": "Wallace",
    "lastName": "Feil",
    "fullName": "Wallace Feil"
  },
  {
    "id": "root-02-group-02",
    "parentId": "root-02",
    "avatar": "FK",
    "firstName": "Federico",
    "lastName": "Kertzmann",
    "fullName": "Federico Kertzmann"
  },
  {
    "id": "root-02-group-02-team-01",
    "parentId": "root-02-group-02",
    "avatar": "LL",
    "firstName": "Libbie",
    "lastName": "Lynch",
    "fullName": "Libbie Lynch"
  },
  {
    "id": "root-02-group-02-team-01-member-01",
    "parentId": "root-02-group-02-team-01",
    "avatar": "NR",
    "firstName": "Nadia",
    "lastName": "Reilly",
    "fullName": "Nadia Reilly"
  },
  {
    "id": "root-02-group-02-team-01-member-02",
    "parentId": "root-02-group-02-team-01",
    "avatar": "AR",
    "firstName": "Alessandro",
    "lastName": "Runolfsson",
    "fullName": "Alessandro Runolfsson"
  },
  {
    "id": "root-02-group-02-team-01-member-03",
    "parentId": "root-02-group-02-team-01",
    "avatar": "CM",
    "firstName": "Camila",
    "lastName": "Mills",
    "fullName": "Camila Mills"
  },
  {
    "id": "root-02-group-02-team-01-member-04",
    "parentId": "root-02-group-02-team-01",
    "avatar": "HS",
    "firstName": "Hector",
    "lastName": "Schumm",
    "fullName": "Hector Schumm"
  },
  {
    "id": "root-02-group-02-team-01-member-05",
    "parentId": "root-02-group-02-team-01",
    "avatar": "ER",
    "firstName": "Eleanora",
    "lastName": "Ruecker",
    "fullName": "Eleanora Ruecker"
  },
  {
    "id": "root-03",
    "parentId": null,
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-03-group-01",
    "parentId": "root-03",
    "avatar": "FK",
    "firstName": "Federico",
    "lastName": "Kertzmann",
    "fullName": "Federico Kertzmann"
  },
  {
    "id": "root-03-group-01-team-01",
    "parentId": "root-03-group-01",
    "avatar": "LL",
    "firstName": "Libbie",
    "lastName": "Lynch",
    "fullName": "Libbie Lynch"
  },
  {
    "id": "root-03-group-01-team-01-member-01",
    "parentId": "root-03-group-01-team-01",
    "avatar": "CM",
    "firstName": "Camila",
    "lastName": "Mills",
    "fullName": "Camila Mills"
  },
  {
    "id": "root-03-group-01-team-01-member-02",
    "parentId": "root-03-group-01-team-01",
    "avatar": "HS",
    "firstName": "Hector",
    "lastName": "Schumm",
    "fullName": "Hector Schumm"
  },
  {
    "id": "root-03-group-01-team-01-member-03",
    "parentId": "root-03-group-01-team-01",
    "avatar": "ER",
    "firstName": "Eleanora",
    "lastName": "Ruecker",
    "fullName": "Eleanora Ruecker"
  },
  {
    "id": "root-03-group-01-team-01-member-04",
    "parentId": "root-03-group-01-team-01",
    "avatar": "SM",
    "firstName": "Shaylee",
    "lastName": "MacGyver",
    "fullName": "Shaylee MacGyver"
  },
  {
    "id": "root-03-group-01-team-01-member-05",
    "parentId": "root-03-group-01-team-01",
    "avatar": "IM",
    "firstName": "Ines",
    "lastName": "Metz",
    "fullName": "Ines Metz"
  },
  {
    "id": "root-03-group-02",
    "parentId": "root-03",
    "avatar": "DU",
    "firstName": "Dangelo",
    "lastName": "Upton",
    "fullName": "Dangelo Upton"
  },
  {
    "id": "root-03-group-02-team-01",
    "parentId": "root-03-group-02",
    "avatar": "MH",
    "firstName": "Mervin",
    "lastName": "Howell",
    "fullName": "Mervin Howell"
  },
  {
    "id": "root-03-group-02-team-01-member-01",
    "parentId": "root-03-group-02-team-01",
    "avatar": "JB",
    "firstName": "Jettie",
    "lastName": "Bergnaum",
    "fullName": "Jettie Bergnaum"
  },
  {
    "id": "root-03-group-02-team-01-member-02",
    "parentId": "root-03-group-02-team-01",
    "avatar": "LP",
    "firstName": "Liliane",
    "lastName": "Predovic",
    "fullName": "Liliane Predovic"
  },
  {
    "id": "root-03-group-02-team-01-member-03",
    "parentId": "root-03-group-02-team-01",
    "avatar": "FK",
    "firstName": "Federico",
    "lastName": "Kertzmann",
    "fullName": "Federico Kertzmann"
  },
  {
    "id": "root-03-group-02-team-01-member-04",
    "parentId": "root-03-group-02-team-01",
    "avatar": "DU",
    "firstName": "Dangelo",
    "lastName": "Upton",
    "fullName": "Dangelo Upton"
  },
  {
    "id": "root-03-group-02-team-01-member-05",
    "parentId": "root-03-group-02-team-01",
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-04",
    "parentId": null,
    "avatar": "WF",
    "firstName": "Wallace",
    "lastName": "Feil",
    "fullName": "Wallace Feil"
  },
  {
    "id": "root-04-group-01",
    "parentId": "root-04",
    "avatar": "DU",
    "firstName": "Dangelo",
    "lastName": "Upton",
    "fullName": "Dangelo Upton"
  },
  {
    "id": "root-04-group-01-team-01",
    "parentId": "root-04-group-01",
    "avatar": "MH",
    "firstName": "Mervin",
    "lastName": "Howell",
    "fullName": "Mervin Howell"
  },
  {
    "id": "root-04-group-01-team-01-member-01",
    "parentId": "root-04-group-01-team-01",
    "avatar": "FK",
    "firstName": "Federico",
    "lastName": "Kertzmann",
    "fullName": "Federico Kertzmann"
  },
  {
    "id": "root-04-group-01-team-01-member-02",
    "parentId": "root-04-group-01-team-01",
    "avatar": "DU",
    "firstName": "Dangelo",
    "lastName": "Upton",
    "fullName": "Dangelo Upton"
  },
  {
    "id": "root-04-group-01-team-01-member-03",
    "parentId": "root-04-group-01-team-01",
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-04-group-01-team-01-member-04",
    "parentId": "root-04-group-01-team-01",
    "avatar": "LL",
    "firstName": "Libbie",
    "lastName": "Lynch",
    "fullName": "Libbie Lynch"
  },
  {
    "id": "root-04-group-01-team-01-member-05",
    "parentId": "root-04-group-01-team-01",
    "avatar": "MH",
    "firstName": "Mervin",
    "lastName": "Howell",
    "fullName": "Mervin Howell"
  },
  {
    "id": "root-04-group-02",
    "parentId": "root-04",
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-04-group-02-team-01",
    "parentId": "root-04-group-02",
    "avatar": "WF",
    "firstName": "Wallace",
    "lastName": "Feil",
    "fullName": "Wallace Feil"
  },
  {
    "id": "root-04-group-02-team-01-member-01",
    "parentId": "root-04-group-02-team-01",
    "avatar": "WF",
    "firstName": "Wallace",
    "lastName": "Feil",
    "fullName": "Wallace Feil"
  },
  {
    "id": "root-04-group-02-team-01-member-02",
    "parentId": "root-04-group-02-team-01",
    "avatar": "NR",
    "firstName": "Nadia",
    "lastName": "Reilly",
    "fullName": "Nadia Reilly"
  },
  {
    "id": "root-04-group-02-team-01-member-03",
    "parentId": "root-04-group-02-team-01",
    "avatar": "AR",
    "firstName": "Alessandro",
    "lastName": "Runolfsson",
    "fullName": "Alessandro Runolfsson"
  },
  {
    "id": "root-04-group-02-team-01-member-04",
    "parentId": "root-04-group-02-team-01",
    "avatar": "CM",
    "firstName": "Camila",
    "lastName": "Mills",
    "fullName": "Camila Mills"
  },
  {
    "id": "root-04-group-02-team-01-member-05",
    "parentId": "root-04-group-02-team-01",
    "avatar": "HS",
    "firstName": "Hector",
    "lastName": "Schumm",
    "fullName": "Hector Schumm"
  },
  {
    "id": "root-05",
    "parentId": null,
    "avatar": "CM",
    "firstName": "Camila",
    "lastName": "Mills",
    "fullName": "Camila Mills"
  },
  {
    "id": "root-05-group-01",
    "parentId": "root-05",
    "avatar": "AL",
    "firstName": "Abbigail",
    "lastName": "Lubowitz",
    "fullName": "Abbigail Lubowitz"
  },
  {
    "id": "root-05-group-01-team-01",
    "parentId": "root-05-group-01",
    "avatar": "WF",
    "firstName": "Wallace",
    "lastName": "Feil",
    "fullName": "Wallace Feil"
  },
  {
    "id": "root-05-group-01-team-01-member-01",
    "parentId": "root-05-group-01-team-01",
    "avatar": "AR",
    "firstName": "Alessandro",
    "lastName": "Runolfsson",
    "fullName": "Alessandro Runolfsson"
  },
  {
    "id": "root-05-group-01-team-01-member-02",
    "parentId": "root-05-group-01-team-01",
    "avatar": "CM",
    "firstName": "Camila",
    "lastName": "Mills",
    "fullName": "Camila Mills"
  },
  {
    "id": "root-05-group-01-team-01-member-03",
    "parentId": "root-05-group-01-team-01",
    "avatar": "HS",
    "firstName": "Hector",
    "lastName": "Schumm",
    "fullName": "Hector Schumm"
  },
  {
    "id": "root-05-group-01-team-01-member-04",
    "parentId": "root-05-group-01-team-01",
    "avatar": "ER",
    "firstName": "Eleanora",
    "lastName": "Ruecker",
    "fullName": "Eleanora Ruecker"
  },
  {
    "id": "root-05-group-01-team-01-member-05",
    "parentId": "root-05-group-01-team-01",
    "avatar": "SM",
    "firstName": "Shaylee",
    "lastName": "MacGyver",
    "fullName": "Shaylee MacGyver"
  },
  {
    "id": "root-05-group-02",
    "parentId": "root-05",
    "avatar": "LL",
    "firstName": "Libbie",
    "lastName": "Lynch",
    "fullName": "Libbie Lynch"
  },
  {
    "id": "root-05-group-02-team-01",
    "parentId": "root-05-group-02",
    "avatar": "NR",
    "firstName": "Nadia",
    "lastName": "Reilly",
    "fullName": "Nadia Reilly"
  },
  {
    "id": "root-05-group-02-team-01-member-01",
    "parentId": "root-05-group-02-team-01",
    "avatar": "IM",
    "firstName": "Ines",
    "lastName": "Metz",
    "fullName": "Ines Metz"
  },
  {
    "id": "root-05-group-02-team-01-member-02",
    "parentId": "root-05-group-02-team-01",
    "avatar": "JB",
    "firstName": "Jettie",
    "lastName": "Bergnaum",
    "fullName": "Jettie Bergnaum"
  },
  {
    "id": "root-05-group-02-team-01-member-03",
    "parentId": "root-05-group-02-team-01",
    "avatar": "LP",
    "firstName": "Liliane",
    "lastName": "Predovic",
    "fullName": "Liliane Predovic"
  },
  {
    "id": "root-05-group-02-team-01-member-04",
    "parentId": "root-05-group-02-team-01",
    "avatar": "FK",
    "firstName": "Federico",
    "lastName": "Kertzmann",
    "fullName": "Federico Kertzmann"
  },
  {
    "id": "root-05-group-02-team-01-member-05",
    "parentId": "root-05-group-02-team-01",
    "avatar": "DU",
    "firstName": "Dangelo",
    "lastName": "Upton",
    "fullName": "Dangelo Upton"
  }
]

RevoGrid has two different ways to show hierarchical rows, and they solve different problems:

  • Key grouping uses the grid’s built-in grouping configuration, for example grid.grouping = { props: ['country', 'city'] }. The grid reads one or more column values, creates synthetic group header rows for each matching key, and places source rows under those generated groups. Use it when the hierarchy should be calculated from repeated values such as category, region, status, or date.
  • TreeDataPlugin uses explicit row relationships from your data, normally id and parentId. The plugin keeps your original rows as the tree nodes, computes tree metadata such as level, expanded state, and visibility, and trims collapsed descendants from the viewport. Use it when the hierarchy already exists in the data, such as folders, tasks and subtasks, organization charts, bill of materials, or nested records.

In short, key grouping answers “which rows share the same values?”, while the tree plugin answers “which row is the parent of this row?”. Key grouping is value-driven and creates group rows; tree data is relationship-driven and renders your existing rows as parent and child nodes.

Key Features

  • Hierarchical Data Support: Automatically organizes flat data into a tree structure based on customizable id, parentId, and level fields.
  • Expandable Rows: Expand and collapse rows dynamically to reveal or hide child rows.
  • Customizable Templates: Supports tree-specific cell templates for better customization.
  • Root Parent Support: Define custom root parent identifiers for flexibility in tree structure.

To build the hierarchical tree structure, each data item must include the following fields:

  • id: A unique identifier for each row.
  • parentId: Links the item to its parent row. Root-level rows should use the rootParentId value (default: 'root').

Without these fields, the plugin cannot establish the necessary parent-child relationships for the tree structure.

const data = [
{ id: '1', parentId: 'root', name: 'Parent 1' },
{ id: '2', parentId: '1', name: 'Child 1.1' },
{ id: '3', parentId: '1', name: 'Child 1.2' },
{ id: '4', parentId: 'root', name: 'Parent 2' },
];

The plugin supports the following options for customization:

  • idField: Defines the field representing unique row identifiers. Default: ‘id’.
  • parentIdField: Specifies the field indicating parent row IDs. Default: ‘parentId’.
  • levelField: Sets the field to store hierarchy levels. Default: ‘level’.
  • rootParentId: Defines the identifier for root-level rows. Default: ‘root’.
  • expandedRowIds: A Set of row IDs to predefine expanded rows.

You can provide these options via revogrid.additionalData?.tree to customize the plugin’s behavior:

grid.additionalData = {
tree: {
idField: 'customId',
parentIdField: 'customParentId',
levelField: 'depth',
rootParentId: null,
expandedRowIds: new Set(['row1', 'row2']), // Pre-expanded rows
},
};

By setting these options, you can adapt the plugin to different data structures and define default states for the tree.

Tree rows can animate while they are trimmed during collapse and restored during expand. Enable tree.animation; TreeDataPlugin registers DimensionAnimationPlugin automatically when it is not already present.

import { TreeDataPlugin } from '@revolist/revogrid-pro';
grid.plugins = [TreeDataPlugin];
grid.tree = {
animation: true,
};
grid.dimensionAnimation = {
duration: 180,
};

When tree.animation is not enabled, tree collapse and expand use the existing immediate trim behavior.