---
title: "SSRM Tree Data"
enterprise: true
framework: javascript
version: "36.1.0"
---

# SSRM Tree Data

This section shows how Tree Data can be used with the Server-Side Row Model.

## Tree Data

Tree Data is defined as data that has parent / child relationships where the parent / child relationships are provided as part of the data. This is in contrast to Row Grouping where the parent / child relationships are the result of grouping. When working with Tree Data, there are no columns getting grouped.

An example of a Tree Data JSON structure is shown below:

```json
[{
    "employeeId": 101,
    "employeeName": "Erica Rogers",
    "jobTitle": "CEO",
    "employmentType": "Permanent",
    "children": [{
        "employeeId": 102,
        "employeeName": "Malcolm Barrett",
        "jobTitle": "Exec. Vice President",
        "employmentType": "Permanent",
        "children": [
            {
                "employeeId": 103,
                "employeeName": "Leah Flowers",
                "jobTitle": "Parts Technician",
                "employmentType": "Contract"
            },
            {
                "employeeId": 104,
                "employeeName": "Tammy Sutton",
                "jobTitle": "Service Technician",
                "employmentType": "Contract"
            }
        ]
    }]
}]
```

It is expected that the data set will be too large to send over the network, hence the SSRM is used to lazy-load child row as the parent rows are expanded.

## Tree Data Mode

In order to set the grid to work with Tree Data, simply enable Tree Data mode via the Grid Options using: `gridOptions.treeData = true`.

## Supplying Tree Data

Tree Data is supplied via the [Server-Side Datasource](https://www.ag-grid.com/javascript-data-grid/server-side-model-datasource/) just like flat data, however there are two additional gridOptions callbacks: `isServerSideGroup(dataItem)` and `getServerSideGroupKey(dataItem)`.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `isServerSideGroup` | `IsServerSideGroup` |  |  | SSRM Tree Data: Allows specifying which rows are expandable. Module: [`ServerSideRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |
| `getServerSideGroupKey` | `GetServerSideGroupKey` |  |  | SSRM Tree Data: Allows specifying group keys. Module: [`ServerSideRowModelModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |

The following code snippet shows the relevant `gridOptions` entries for configuring tree data with the server-side row model:

```js
const gridOptions = {
    // choose Server-Side Row Model
    rowModelType: 'serverSide',
    // enable Tree Data
    treeData: true,
    // indicate if row is a group node
    isServerSideGroup: dataItem => {
        return dataItem.group;
    },
    // specify which group key to use
    getServerSideGroupKey: dataItem => {
        return dataItem.employeeId;
    },

    // other grid options ...
}
```

> **Note**
>
> Be careful not to get mixed up with the [Client-Side Tree Data](https://www.ag-grid.com/javascript-data-grid/tree-data/) configurations by mistake.

The example below shows this in action where the following can be noted:

- Tree Data is enabled with the Server-Side Row Model using `gridOptions.treeData = true`.
- Group nodes are determined using the callback `gridOptions.isServerSideGroup(dataItem)`.
- Group keys are returned from the callback `gridOptions.getServerSideGroupKey(dataItem)`.

#### Tree Data

```ts
import {
  ColDef,
  GridApi,
  GridOptions,
  ICellRendererParams,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
]);

const columnDefs: ColDef[] = [
  { field: "employeeId", hide: true },
  { field: "employeeName", hide: true },
  { field: "jobTitle" },
  { field: "employmentType" },
];

let gridApi: GridApi;

const gridOptions: GridOptions = {
  defaultColDef: {
    width: 240,
    flex: 1,
    sortable: false,
  },
  autoGroupColumnDef: {
    field: "employeeName",
    cellRendererParams: {
      innerRenderer: (params: ICellRendererParams) => {
        // display employeeName rather than group key (employeeId)
        return params.data.employeeName;
      },
    },
  },
  rowModelType: "serverSide",
  treeData: true,
  columnDefs: columnDefs,
  isServerSideGroupOpenByDefault: (
    params: IsServerSideGroupOpenByDefaultParams,
  ) => {
    // open first two levels by default
    return params.rowNode.level < 2;
  },
  isServerSideGroup: (dataItem: any) => {
    // indicate if node is a group
    return dataItem.group;
  },
  getServerSideGroupKey: (dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeId;
  },
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/small-tree-data.json")
  .then((response) => response.json())
  .then(function (data) {
    const fakeServer = createFakeServer(data);
    const datasource = createServerSideDatasource(fakeServer);
    gridApi!.setGridOption("serverSideDatasource", datasource);
  });

function createFakeServer(fakeServerData: any[]) {
  const fakeServer = {
    data: fakeServerData,
    getData: function (request: IServerSideGetRowsRequest) {
      function extractRowsFromData(groupKeys: string[], data: any[]): any {
        if (groupKeys.length === 0) {
          return data.map(function (d) {
            return {
              group: !!d.children,
              employeeId: d.employeeId,
              employeeName: d.employeeName,
              employmentType: d.employmentType,
              jobTitle: d.jobTitle,
            };
          });
        }

        const key = groupKeys[0];
        for (let i = 0; i < data.length; i++) {
          if (data[i].employeeId === key) {
            return extractRowsFromData(
              groupKeys.slice(1),
              data[i].children.slice(),
            );
          }
        }
      }

      return extractRowsFromData(request.groupKeys, this.data);
    },
  };

  return fakeServer;
}

function createServerSideDatasource(fakeServer: any) {
  const dataSource: IServerSideDatasource = {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("ServerSideDatasource.getRows: params = ", params);

      const allRows = fakeServer.getData(params.request);

      const request = params.request;
      const doingInfinite = request.startRow != null && request.endRow != null;
      const result = doingInfinite
        ? {
            rowData: allRows.slice(request.startRow, request.endRow),
            rowCount: allRows.length,
          }
        : { rowData: allRows };
      console.log("getRows: result = ", result);
      setTimeout(() => {
        params.success(result);
      }, 200);
    },
  };

  return dataSource;
}
```

[Live example: Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/tree-data/typescript)

## Refreshing Tree Data

Tree Data can be refreshed in the same way as groups are refreshed when not using Tree Data. This is explained in the [SSRM Refresh](https://www.ag-grid.com/javascript-data-grid/server-side-model-updating-refresh/).

The example below shows this in action where the following can be noted:

- Click **Refresh Everything** to clear all caches by calling `api.refreshServerSide({ route: [], purge: true })`.
- Click **Refresh ['Kathryn Powers','Mabel Ward']** to clear a single cache by calling `api.refreshServerSide({ route: ['Kathryn Powers','Mabel Ward'], purge: true })`.

#### Purging Tree Data

```ts
import {
  ColDef,
  GridApi,
  GridOptions,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
  ServerSideRowModelApiModule,
]);

const columnDefs: ColDef[] = [
  { field: "employeeId", hide: true },
  { field: "employeeName", hide: true },
  { field: "employmentType" },
  { field: "startDate" },
];

let gridApi: GridApi;

const gridOptions: GridOptions = {
  defaultColDef: {
    width: 235,
    flex: 1,
    sortable: false,
  },
  autoGroupColumnDef: {
    field: "employeeName",
  },
  rowModelType: "serverSide",
  treeData: true,
  columnDefs: columnDefs,
  cacheBlockSize: 10,
  isServerSideGroupOpenByDefault: (
    params: IsServerSideGroupOpenByDefaultParams,
  ) => {
    const isKathrynPowers =
      params.rowNode.level == 0 && params.data.employeeName == "Kathryn Powers";
    const isMabelWard =
      params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
    return isKathrynPowers || isMabelWard;
  },
  isServerSideGroup: (dataItem: any) => {
    // indicate if node is a group
    return dataItem.group;
  },
  getServerSideGroupKey: (dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeName;
  },
};

function refreshCache(route: string[]) {
  gridApi!.refreshServerSide({ route: route, purge: true });
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/tree-data.json")
  .then((response) => response.json())
  .then(function (data) {
    const fakeServer = createFakeServer(data);
    const datasource = createServerSideDatasource(fakeServer);
    gridApi!.setGridOption("serverSideDatasource", datasource);
  });

function createFakeServer(fakeServerData: any[]) {
  const fakeServer = {
    getData: (request: IServerSideGetRowsRequest) => {
      function extractRowsFromData(groupKeys: string[], data: any[]): any {
        if (groupKeys.length === 0) {
          return data.map(function (d) {
            return {
              group: !!d.underlings,
              employeeId: d.employeeId + "",
              employeeName: d.employeeName,
              employmentType: d.employmentType,
              startDate: d.startDate,
            };
          });
        }

        const key = groupKeys[0];
        for (let i = 0; i < data.length; i++) {
          if (data[i].employeeName === key) {
            return extractRowsFromData(
              groupKeys.slice(1),
              data[i].underlings.slice(),
            );
          }
        }
      }

      return extractRowsFromData(request.groupKeys, fakeServerData);
    },
  };
  return fakeServer;
}

function createServerSideDatasource(fakeServer: any) {
  const dataSource: IServerSideDatasource = {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("ServerSideDatasource.getRows: params = ", params);
      const request = params.request;
      const allRows = fakeServer.getData(request);
      const doingInfinite = request.startRow != null && request.endRow != null;
      const result = doingInfinite
        ? {
            rowData: allRows.slice(request.startRow, request.endRow),
            rowCount: allRows.length,
          }
        : { rowData: allRows };
      console.log("getRows: result = ", result);
      setTimeout(() => {
        params.success(result);
      }, 500);
    },
  };

  return dataSource;
}

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).refreshCache = refreshCache;
}
```

[Live example: Purging Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/purging-tree-data/typescript)

## Transactions with Tree Data

Tree Data can have transactions applied in the same way as row groups. This is explained in the [SSRM Transactions](https://www.ag-grid.com/javascript-data-grid/server-side-model-updating-transactions/) section.

The example below demonstrates transactions with Tree Data. Note the following:

- **Add Child to Selected** adds a child under the selected row, even if it wasn't originally a group.
- **Update Selected** changes the selected row's `Employment Type`.
- **Delete Selected** removes the selected row, and all of its child rows.
- **Move Selected to Robert Peterson** moves the selected row and its children directly under `Robert Peterson`.

#### Transactions with Tree Data

```ts
import {
  ColDef,
  GetRowIdParams,
  GridApi,
  GridOptions,
  IRowNode,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  TextFilterModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  TextFilterModule,
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
  ServerSideRowModelApiModule,
]);

let fakeServer: {
  getData: (request: IServerSideGetRowsRequest) => void;
  addChildRow: (route: string[], newRow: any) => void;
  toggleEmployment: (route: string[]) => void;
  removeEmployee: (route: string[]) => void;
  moveEmployee: (from: string[], to: string[]) => void;
};

const columnDefs: ColDef[] = [
  { field: "employeeId", hide: true },
  { field: "employeeName", hide: true },
  { field: "employmentType" },
  { field: "startDate" },
];

let gridApi: GridApi;

const gridOptions: GridOptions = {
  defaultColDef: {
    width: 235,
    flex: 1,
    sortable: false,
  },
  autoGroupColumnDef: {
    field: "employeeName",
  },
  rowModelType: "serverSide",
  treeData: true,
  columnDefs,
  cacheBlockSize: 10,
  rowSelection: { mode: "singleRow" },

  isServerSideGroupOpenByDefault: (
    params: IsServerSideGroupOpenByDefaultParams,
  ) => {
    const isKathrynPowers =
      params.rowNode.level == 0 && params.data.employeeName == "Kathryn Powers";
    const isMabelWard =
      params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
    return isKathrynPowers || isMabelWard;
  },
  getRowId: (row: GetRowIdParams) => String(row.data.employeeId),
  isServerSideGroup: (dataItem: any) => dataItem.group,
  getServerSideGroupKey: (dataItem: any) => dataItem.employeeName,
};

function getRouteToNode(rowNode: IRowNode): string[] {
  if (!rowNode.parent) {
    return [];
  }
  return [
    ...getRouteToNode(rowNode.parent),
    rowNode.key ? rowNode.key : rowNode.data.employeeName,
  ];
}

let latestId = 100000;
function addToSelected() {
  const selected = gridApi!.getSelectedNodes()[0];
  if (!selected) {
    console.log("No row was selected.");
    return;
  }

  const route = getRouteToNode(selected);
  const newRow = {
    employeeId: ++latestId,
    employeeName: "Bertrand Parker " + latestId,
    employmentType: "Permanent",
    startDate: "20/01/1999",
  };
  fakeServer.addChildRow(route, newRow);
}

function updateSelected() {
  const selected = gridApi!.getSelectedNodes()[0];
  if (!selected) {
    console.log("No row was selected.");
    return;
  }

  const route = getRouteToNode(selected);
  fakeServer.toggleEmployment(route);
}

function deleteSelected() {
  const selected = gridApi!.getSelectedNodes()[0];
  if (!selected) {
    console.log("No row was selected.");
    return;
  }

  const route = getRouteToNode(selected);
  fakeServer.removeEmployee(route);
}

function moveSelected() {
  const selected = gridApi!.getSelectedNodes()[0];
  if (!selected) {
    console.log("No row was selected.");
    return;
  }

  const route = getRouteToNode(selected);
  fakeServer.moveEmployee(route, ["Robert Peterson"]);
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/tree-data.json")
  .then((response) => response.json())
  .then(function (data) {
    const adjustedData = [
      {
        employeeId: -1,
        employeeName: "Robert Peterson",
        employmentType: "Founder",
        startDate: "24/01/1990",
      },
      ...data,
    ];
    const fakeServer = createFakeServer(adjustedData, gridApi!);
    const datasource = createServerSideDatasource(fakeServer);
    gridApi!.setGridOption("serverSideDatasource", datasource);
  });

function createFakeServer(fakeServerData: any[], api: GridApi) {
  const getDataAtRoute = (route: string[]) => {
    let mutableRoute = [...route];
    let target: any = { underlings: fakeServerData };
    while (mutableRoute.length) {
      const nextRoute = mutableRoute[0];
      mutableRoute = mutableRoute.slice(1);
      target = target.underlings.find((e: any) => e.employeeName === nextRoute);
    }
    return target;
  };

  const sanitizeRowForGrid = (d: any) => {
    return {
      group: !!d.underlings && !!d.underlings.length,
      employeeId: d.employeeId,
      employeeName: d.employeeName,
      employmentType: d.employmentType,
      startDate: d.startDate,
    };
  };

  fakeServer = {
    getData: (request: IServerSideGetRowsRequest) => {
      function extractRowsFromData(groupKeys: string[], data: any[]): any {
        if (groupKeys.length === 0) {
          return data.map(sanitizeRowForGrid);
        }

        const key = groupKeys[0];
        for (let i = 0; i < data.length; i++) {
          if (data[i].employeeName === key) {
            return extractRowsFromData(
              groupKeys.slice(1),
              data[i].underlings.slice(),
            );
          }
        }
      }

      return extractRowsFromData(request.groupKeys, fakeServerData);
    },
    addChildRow: (route: string[], newRow: any) => {
      const target = getDataAtRoute(route);
      if (!target.underlings || target.underlings.length === 0) {
        target.underlings = [newRow];

        // update the parent row via transaction
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 1),
          update: [sanitizeRowForGrid(target)],
        });
      } else {
        target.underlings.push(newRow);

        // add the child row via transaction
        api.applyServerSideTransaction({
          route,
          add: [sanitizeRowForGrid(newRow)],
        });
      }
    },
    toggleEmployment: (route: string[]) => {
      const target = getDataAtRoute(route);
      // update the data at the source
      target.employmentType =
        target.employmentType === "Contract" ? "Permanent" : "Contract";

      // inform the grid of the changes
      api.applyServerSideTransaction({
        route: route.slice(0, route.length - 1),
        update: [sanitizeRowForGrid(target)],
      });
    },
    removeEmployee: (route: string[]) => {
      const target = getDataAtRoute(route);

      const parent = getDataAtRoute(route.slice(0, route.length - 1));
      parent.underlings = parent.underlings.filter(
        (child: any) => child.employeeName !== target.employeeName,
      );
      if (parent.underlings.length === 0) {
        // update the parent row via transaction, as it's no longer a group
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 2),
          update: [sanitizeRowForGrid(parent)],
        });
      } else {
        // inform the grid of the changes
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 1),
          remove: [sanitizeRowForGrid(target)],
        });
      }
    },
    moveEmployee: (route: string[], to: string[]) => {
      const target = getDataAtRoute(route);

      // remove employee from old group
      fakeServer.removeEmployee(route);

      // add employee to new group
      fakeServer.addChildRow(to, target);
    },
  };
  return fakeServer;
}

function createServerSideDatasource(fakeServer: any) {
  const dataSource: IServerSideDatasource = {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("ServerSideDatasource.getRows: params = ", params);
      const request = params.request;
      const allRows = fakeServer.getData(request);
      const doingInfinite = request.startRow != null && request.endRow != null;
      const result = doingInfinite
        ? {
            rowData: allRows.slice(request.startRow, request.endRow),
            rowCount: allRows.length,
          }
        : { rowData: allRows };
      console.log("getRows: result = ", result);
      setTimeout(() => {
        params.success(result);
      }, 500);
    },
  };

  return dataSource;
}

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).addToSelected = addToSelected;
  (<any>window).updateSelected = updateSelected;
  (<any>window).deleteSelected = deleteSelected;
  (<any>window).moveSelected = moveSelected;
}
```

[Live example: Transactions with Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/transactions-tree-data/typescript)

## Selection with Tree Data

Tree Data can have row selection applied in the same way as row groups. This is explained in the [SSRM Row Selection](https://www.ag-grid.com/javascript-data-grid/server-side-model-selection/) section.

The example below demonstrates row selection with Tree Data where the grid has been configured with `rowSelection.groupSelects = 'descendants'`. This means that selecting a row with descendants selects all of its descendants. Selecting some, but not all, of the descendants of a row places that row in the indeterminate state.

#### Selection with Tree Data

```ts
import {
  GridApi,
  GridOptions,
  IServerSideDatasource,
  IServerSideGetRowsRequest,
  ModuleRegistry,
  TextFilterModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ServerSideRowModelApiModule,
  TextFilterModule,
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
]);

interface FakeServer {
  getData: (request: IServerSideGetRowsRequest) => any;
  addChildRow: (route: string[], newRow: any) => void;
  toggleEmployment: (route: string[]) => void;
  removeEmployee: (route: string[]) => void;
  moveEmployee: (from: string[], to: string[]) => void;
}

let fakeServer: FakeServer;

let gridApi: GridApi;

const gridOptions: GridOptions = {
  defaultColDef: {
    width: 235,
    flex: 1,
    sortable: false,
  },
  autoGroupColumnDef: {
    field: "employeeName",
  },
  rowModelType: "serverSide",
  treeData: true,
  columnDefs: [
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "employmentType" },
    { field: "startDate" },
  ],
  cacheBlockSize: 10,
  rowSelection: {
    mode: "multiRow",
    groupSelects: "descendants",
  },
  isServerSideGroupOpenByDefault: (params) => {
    const isKathrynPowers =
      params.rowNode.level == 0 && params.data.employeeName == "Kathryn Powers";
    const isMabelWard =
      params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
    return isKathrynPowers || isMabelWard;
  },
  getRowId: (row) => String(row.data.employeeId),
  isServerSideGroup: (dataItem) => dataItem.group,
  getServerSideGroupKey: (dataItem) => dataItem.employeeName,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/tree-data.json")
  .then((response) => response.json())
  .then(function (data) {
    const adjustedData = [
      {
        employeeId: -1,
        employeeName: "Robert Peterson",
        employmentType: "Founder",
        startDate: "24/01/1990",
      },
      ...data,
    ];
    const fakeServer = createFakeServer(adjustedData, gridApi!);
    const datasource = createServerSideDatasource(fakeServer);
    gridApi!.setGridOption("serverSideDatasource", datasource);
  });

function createFakeServer(fakeServerData: any[], api: GridApi) {
  const getDataAtRoute = (route: string[]) => {
    let mutableRoute = [...route];
    let target: any = { underlings: fakeServerData };
    while (mutableRoute.length) {
      const nextRoute = mutableRoute[0];
      mutableRoute = mutableRoute.slice(1);
      target = target.underlings.find((e: any) => e.employeeName === nextRoute);
    }
    return target;
  };

  const sanitizeRowForGrid = (d: any) => {
    return {
      group: !!d.underlings && !!d.underlings.length,
      employeeId: d.employeeId,
      employeeName: d.employeeName,
      employmentType: d.employmentType,
      startDate: d.startDate,
    };
  };

  fakeServer = {
    getData: (request: IServerSideGetRowsRequest): any => {
      function extractRowsFromData(groupKeys: string[], data: any[]): any {
        if (groupKeys.length === 0) {
          return data.map(sanitizeRowForGrid);
        }

        const key = groupKeys[0];
        for (let i = 0; i < data.length; i++) {
          if (data[i].employeeName === key) {
            return extractRowsFromData(
              groupKeys.slice(1),
              data[i].underlings.slice(),
            );
          }
        }
      }

      return extractRowsFromData(request.groupKeys, fakeServerData);
    },
    addChildRow: (route: string[], newRow: any) => {
      const target = getDataAtRoute(route);
      if (!target.underlings || target.underlings.length === 0) {
        target.underlings = [newRow];

        // update the parent row via transaction
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 1),
          update: [sanitizeRowForGrid(target)],
        });
      } else {
        target.underlings.push(newRow);

        // add the child row via transaction
        api.applyServerSideTransaction({
          route,
          add: [sanitizeRowForGrid(newRow)],
        });
      }
    },
    toggleEmployment: (route: string[]) => {
      const target = getDataAtRoute(route);
      // update the data at the source
      target.employmentType =
        target.employmentType === "Contract" ? "Permanent" : "Contract";

      // inform the grid of the changes
      api.applyServerSideTransaction({
        route: route.slice(0, route.length - 1),
        update: [sanitizeRowForGrid(target)],
      });
    },
    removeEmployee: (route: string[]) => {
      const target = getDataAtRoute(route);

      const parent = getDataAtRoute(route.slice(0, route.length - 1));
      parent.underlings = parent.underlings.filter(
        (child: any) => child.employeeName !== target.employeeName,
      );
      if (parent.underlings.length === 0) {
        // update the parent row via transaction, as it's no longer a group
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 2),
          update: [sanitizeRowForGrid(parent)],
        });
      } else {
        // inform the grid of the changes
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 1),
          remove: [sanitizeRowForGrid(target)],
        });
      }
    },
    moveEmployee: (route: string[], to: string[]) => {
      const target = getDataAtRoute(route);

      // remove employee from old group
      fakeServer.removeEmployee(route);

      // add employee to new group
      fakeServer.addChildRow(to, target);
    },
  };
  return fakeServer;
}

function createServerSideDatasource(fakeServer: FakeServer) {
  const dataSource: IServerSideDatasource = {
    getRows: (params) => {
      console.log("ServerSideDatasource.getRows: params = ", params);
      const request = params.request;
      const allRows = fakeServer.getData(request);
      const doingInfinite = request.startRow != null && request.endRow != null;
      const result = doingInfinite
        ? {
            rowData: allRows.slice(request.startRow, request.endRow),
            rowCount: allRows.length,
          }
        : { rowData: allRows };
      console.log("getRows: result = ", result);
      setTimeout(() => {
        params.success(result);
      }, 500);
    },
  };

  return dataSource;
}
```

[Live example: Selection with Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/selecting-tree-data/typescript)

## Filtering Tree Data

Server-Side Tree Data Filtering should behave the same as Client-Side [Tree Data Filtering](https://www.ag-grid.com/javascript-data-grid/tree-data-filtering/). A group will be included if:

1. it has any children that pass the filter, or
2. it has a parent that passes the filter, or
3. its own data passes the filter

The following example demonstrates Server-Side Tree Data Filtering using the [Set Filter Tree List](https://www.ag-grid.com/javascript-data-grid/filter-set-tree-list/), which replicates the Tree Data structure in the filter.

- The **Group** column has the Set Filter Tree List enabled via `filterParams.treeList = true`. A Key Creator is specified to convert the path into a string.
- The **Group** column has the filter values supplied asynchronously as a nested array of strings that matches the data paths. The supplied values are at the child level only.
- The **Date** column has the Set Filter Tree List enabled via `filterParams.treeList = true`, and is grouped by year -> month -> day.
- The **Date** column has the filter values supplied asynchronously as an array of `Date` objects.
- The **Date** column has a `filterParams.keyCreator` provided to convert the `Date` values into the (string) format the server is expecting in the Filter Model.
- The **Group** and **Date** columns both have `filterParams.excelMode = 'windows'`, which allows changes to the tree filter to be applied in batches.

#### Filtering Tree Data

```ts
import {
  ColDef,
  GridApi,
  GridOptions,
  IServerSideDatasource,
  ISetFilterParams,
  IsServerSideGroupOpenByDefaultParams,
  KeyCreatorParams,
  ModuleRegistry,
  SetFilterValuesFuncParams,
  TextFilterModule,
  ValueFormatterParams,
  ValueGetterParams,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelModule,
  SetFilterModule,
  TreeDataModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
  SetFilterModule,
  TextFilterModule,
]);

const columnDefs: ColDef[] = [
  { field: "employeeId", hide: true },
  { field: "employeeName", hide: true },
  { field: "employmentType" },
  {
    field: "startDate",
    valueGetter: valueGetter,
    valueFormatter: cellValueFormatter,
    filter: "agSetColumnFilter",
    filterParams: {
      treeList: true,
      excelMode: "windows",
      keyCreator: dateKeyCreator,
      valueFormatter: floatingFilterValueFormatter,
      values: getDatesAsync,
    } as ISetFilterParams<any, Date>,
  },
];

let gridApi: GridApi;

const gridOptions: GridOptions = {
  defaultColDef: {
    width: 240,
    filter: "agTextColumnFilter",
    floatingFilter: true,
    flex: 1,
    sortable: false,
  },
  autoGroupColumnDef: {
    field: "employeeName",
    filter: "agSetColumnFilter",
    filterParams: {
      treeList: true,
      excelMode: "windows",
      keyCreator: treeDataKeyCreator,
      values: getEmployeesAsync,
    } as ISetFilterParams<any, string[]>,
  },
  rowModelType: "serverSide",
  treeData: true,
  columnDefs: columnDefs,
  isServerSideGroupOpenByDefault: (
    params: IsServerSideGroupOpenByDefaultParams,
  ) => {
    // open first level by default
    return params.rowNode.level === 0;
  },
  isServerSideGroup: (dataItem: any) => {
    // indicate if node is a group
    return dataItem.underlings;
  },
  getServerSideGroupKey: (dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeName;
  },
};

function valueGetter(params: ValueGetterParams) {
  // server is returning a string, so need to convert to `Date`.
  // could instead do this inside `IServerSideDatasource.getRows`
  return params.data.startDate ? new Date(params.data.startDate) : null;
}

function cellValueFormatter(params: ValueFormatterParams) {
  return params.value ? params.value.toLocaleDateString() : null;
}

function floatingFilterValueFormatter(params: ValueFormatterParams) {
  return params.value ? params.value.toLocaleDateString() : "(Blanks)";
}

function dateKeyCreator(params: KeyCreatorParams) {
  // this is what is being sent in the Filter Model to the server, so want the matching format
  return params.value ? params.value.toISOString() : null;
}

function treeDataKeyCreator(params: KeyCreatorParams) {
  // tree data group filter value is a string[], so convert to a unique string
  return params.value ? params.value.join(",") : null;
}

let fakeServer: any;

function getServerSideDatasource(server: any): IServerSideDatasource {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);

      // simulating real server call with a 500ms delay
      setTimeout(() => {
        // get data for request from our fake server
        const response = server.getData(params.request);
        if (response.success) {
          // supply rows for requested block to grid
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          params.fail();
        }
      }, 500);
    },
  };
}

function getDatesAsync(params: SetFilterValuesFuncParams<any, Date>) {
  if (!fakeServer) {
    // wait for init
    setTimeout(() => getDatesAsync(params), 500);
    return;
  }
  let dates = fakeServer.getDates();

  if (dates) {
    // values need to match the cell value (what the `valueGetter` returns)
    dates = dates.map((isoDateString: string) =>
      isoDateString ? new Date(isoDateString) : isoDateString,
    );
  }

  // simulating real server call with a 500ms delay
  setTimeout(() => {
    params.success(dates);
  }, 500);
}

function getEmployeesAsync(params: SetFilterValuesFuncParams<any, string[]>) {
  if (!fakeServer) {
    // wait for init
    setTimeout(() => getEmployeesAsync(params), 500);
    return;
  }
  const employees = fakeServer.getEmployees();

  // simulating real server call with a 500ms delay
  setTimeout(() => {
    params.success(employees);
  }, 500);
}

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/tree-data.json")
  .then((response) => response.json())
  .then(function (data) {
    // setup the fake server with entire dataset
    fakeServer = new FakeServer(data);

    // create datasource with a reference to the fake server
    const datasource = getServerSideDatasource(fakeServer);

    // register the datasource with the grid
    gridApi!.setGridOption("serverSideDatasource", datasource);
  });
```

[Live example: Filtering Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/filtering-tree-data/typescript)

## Providing Hierarchical Data via API

Below is an example of utilising `api.applyServerSideRowData` to populate data. The datasource is only used to provide root level rows, and when these are loaded the grid is provided the child row data via the API.

#### Hierarchical Data

```ts
import {
  ColDef,
  GridApi,
  GridOptions,
  ICellRendererParams,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

ModuleRegistry.registerModules([
  ColumnsToolPanelModule,
  TreeDataModule,
  ColumnMenuModule,
  ContextMenuModule,
  ServerSideRowModelModule,
  ServerSideRowModelApiModule,
]);

const columnDefs: ColDef[] = [
  { field: "employeeId", hide: true },
  { field: "employeeName", hide: true },
  { field: "jobTitle" },
  { field: "employmentType" },
];

let gridApi: GridApi;

const gridOptions: GridOptions = {
  defaultColDef: {
    width: 240,
    flex: 1,
    sortable: false,
  },
  autoGroupColumnDef: {
    field: "employeeName",
    cellRendererParams: {
      innerRenderer: (params: ICellRendererParams) => {
        // display employeeName rather than group key (employeeId)
        return params.data.employeeName;
      },
    },
  },
  rowModelType: "serverSide",
  treeData: true,
  columnDefs: columnDefs,
  isServerSideGroupOpenByDefault: (
    params: IsServerSideGroupOpenByDefaultParams,
  ) => {
    // open first two levels by default
    return params.rowNode.level < 2;
  },
  isServerSideGroup: (dataItem: any) => {
    // indicate if node is a group
    return !!dataItem.underlings;
  },
  getServerSideGroupKey: (dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeId;
  },
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);

fetch("https://www.ag-grid.com/example-assets/tree-data.json")
  .then((response) => response.json())
  .then(function (data) {
    const datasource = createServerSideDatasource(data);
    gridApi!.setGridOption("serverSideDatasource", datasource);

    function createServerSideDatasource(data: any) {
      const dataSource: IServerSideDatasource = {
        getRows: (params: IServerSideGetRowsParams) => {
          console.log("ServerSideDatasource.getRows: params = ", params);

          const request = params.request;
          if (request.groupKeys.length) {
            // this example doesn't need to support lower levels.
            params.fail();
            return;
          }

          const result = {
            rowData: data.slice(request.startRow, request.endRow),
          };
          console.log("getRows: result = ", result);
          setTimeout(() => {
            params.success(result);

            const recursivelyPopulateHierarchy = (
              route: string[],
              node: any,
            ) => {
              if (node.underlings) {
                gridApi!.applyServerSideRowData({
                  route,
                  successParams: {
                    rowData: node.underlings,
                    rowCount: node.underlings.length,
                  },
                });
                node.underlings.forEach((child: any) => {
                  recursivelyPopulateHierarchy(
                    [...route, child.employeeId],
                    child,
                  );
                });
              }
            };
            result.rowData.forEach((topLevelNode: any) => {
              recursivelyPopulateHierarchy(
                [topLevelNode.employeeId],
                topLevelNode,
              );
            });
          }, 200);
        },
      };

      return dataSource;
    }
  });
```

[Live example: Hierarchical Data](https://www.ag-grid.com/examples/server-side-model-tree-data/supplying-hierarchy/typescript)

Continue to the next section to learn how to combine [Master Detail](https://www.ag-grid.com/javascript-data-grid/server-side-model-master-detail/) with the SSRM.
