---
title: "SSRM Tree Data"
enterprise: true
framework: angular
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/angular-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/angular-data-grid/modules/). |
| `getServerSideGroupKey` | `GetServerSideGroupKey` |  |  | SSRM Tree Data: Allows specifying group keys. Module: [`ServerSideRowModelModule`](https://www.ag-grid.com/angular-data-grid/modules/). |

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

```ts
<ag-grid-angular
    [rowModelType]="rowModelType"
    [treeData]="treeData"
    [isServerSideGroup]="isServerSideGroup"
    [getServerSideGroupKey]="getServerSideGroupKey"
    /* other grid options ... */ />

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

> **Note**
>
> Be careful not to get mixed up with the [Client-Side Tree Data](https://www.ag-grid.com/angular-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 { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  ICellRendererParams,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

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

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<ag-grid-angular
    style="width: 100%; height: 100%;"
    [columnDefs]="columnDefs"
    [defaultColDef]="defaultColDef"
    [autoGroupColumnDef]="autoGroupColumnDef"
    [rowModelType]="rowModelType"
    [treeData]="true"
    [isServerSideGroupOpenByDefault]="isServerSideGroupOpenByDefault"
    [isServerSideGroup]="isServerSideGroup"
    [getServerSideGroupKey]="getServerSideGroupKey"
    [rowData]="rowData"
    (gridReady)="onGridReady($event)"
  /> `,
})
export class AppComponent {
  columnDefs: ColDef[] = [
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "jobTitle" },
    { field: "employmentType" },
  ];
  defaultColDef: ColDef = {
    width: 240,
    flex: 1,
    sortable: false,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "employeeName",
    cellRendererParams: {
      innerRenderer: (params: ICellRendererParams) => {
        // display employeeName rather than group key (employeeId)
        return params.data.employeeName;
      },
    },
  };
  rowModelType: RowModelType = "serverSide";
  isServerSideGroupOpenByDefault: IsServerSideGroupOpenByDefault = (
    params: IsServerSideGroupOpenByDefaultParams,
  ) => {
    // open first two levels by default
    return params.rowNode.level < 2;
  };
  isServerSideGroup: IsServerSideGroup = (dataItem: any) => {
    // indicate if node is a group
    return dataItem.group;
  };
  getServerSideGroupKey: GetServerSideGroupKey = (dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeId;
  };
  rowData!: any[];

  constructor(private http: HttpClient) {}

  onGridReady(params: GridReadyEvent) {
    this.http
      .get<any[]>("https://www.ag-grid.com/example-assets/small-tree-data.json")
      .subscribe((data) => {
        const fakeServer = createFakeServer(data);
        const datasource = createServerSideDatasource(fakeServer);
        params.api!.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/angular)

## 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/angular-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 { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

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

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<div class="example-wrapper">
    <div style="margin-bottom: 5px">
      <button (click)="refreshCache([])">Refresh Everything</button>
      <button (click)="refreshCache(['Kathryn Powers', 'Mabel Ward'])">
        Refresh ['Kathryn Powers','Mabel Ward']
      </button>
    </div>
    <ag-grid-angular
      style="width: 100%; height: 100%;"
      [columnDefs]="columnDefs"
      [defaultColDef]="defaultColDef"
      [autoGroupColumnDef]="autoGroupColumnDef"
      [rowModelType]="rowModelType"
      [treeData]="true"
      [cacheBlockSize]="cacheBlockSize"
      [isServerSideGroupOpenByDefault]="isServerSideGroupOpenByDefault"
      [isServerSideGroup]="isServerSideGroup"
      [getServerSideGroupKey]="getServerSideGroupKey"
      [rowData]="rowData"
      (gridReady)="onGridReady($event)"
    />
  </div> `,
})
export class AppComponent {
  private gridApi!: GridApi;

  columnDefs: ColDef[] = [
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "employmentType" },
    { field: "startDate" },
  ];
  defaultColDef: ColDef = {
    width: 235,
    flex: 1,
    sortable: false,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "employeeName",
  };
  rowModelType: RowModelType = "serverSide";
  cacheBlockSize = 10;
  isServerSideGroupOpenByDefault: 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: IsServerSideGroup = (dataItem: any) => {
    // indicate if node is a group
    return dataItem.group;
  };
  getServerSideGroupKey: GetServerSideGroupKey = (dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeName;
  };
  rowData!: any[];

  constructor(private http: HttpClient) {}

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

  onGridReady(params: GridReadyEvent) {
    this.gridApi = params.api;

    this.http
      .get<any[]>("https://www.ag-grid.com/example-assets/tree-data.json")
      .subscribe((data) => {
        const fakeServer = createFakeServer(data);
        const datasource = createServerSideDatasource(fakeServer);
        params.api!.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;
}
```

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

## 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/angular-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 { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetRowIdFunc,
  GetRowIdParams,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IRowNode,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  RowSelectionOptions,
  TextFilterModule,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

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

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<div class="example-wrapper">
    <div style="margin-bottom: 5px">
      <button (click)="addToSelected()">Add Child to Selected</button>
      <button (click)="updateSelected()">Update Selected</button>
      <button (click)="deleteSelected()">Delete Selected</button>
      <button (click)="moveSelected()">Move Selected to Robert Peterson</button>
    </div>
    <ag-grid-angular
      style="width: 100%; height: 100%;"
      [columnDefs]="columnDefs"
      [defaultColDef]="defaultColDef"
      [autoGroupColumnDef]="autoGroupColumnDef"
      [rowModelType]="rowModelType"
      [treeData]="true"
      [cacheBlockSize]="cacheBlockSize"
      [rowSelection]="rowSelection"
      [isServerSideGroupOpenByDefault]="isServerSideGroupOpenByDefault"
      [getRowId]="getRowId"
      [isServerSideGroup]="isServerSideGroup"
      [getServerSideGroupKey]="getServerSideGroupKey"
      [rowData]="rowData"
      (gridReady)="onGridReady($event)"
    />
  </div> `,
})
export class AppComponent {
  private gridApi!: GridApi;

  columnDefs: ColDef[] = [
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "employmentType" },
    { field: "startDate" },
  ];
  defaultColDef: ColDef = {
    width: 235,
    flex: 1,
    sortable: false,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "employeeName",
  };
  rowModelType: RowModelType = "serverSide";
  cacheBlockSize = 10;
  rowSelection: RowSelectionOptions | "single" | "multiple" = {
    mode: "singleRow",
  };
  isServerSideGroupOpenByDefault: 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: GetRowIdFunc = (row: GetRowIdParams) => String(row.data.employeeId);
  isServerSideGroup: IsServerSideGroup = (dataItem: any) => dataItem.group;
  getServerSideGroupKey: GetServerSideGroupKey = (dataItem: any) =>
    dataItem.employeeName;
  rowData!: any[];

  constructor(private http: HttpClient) {}

  addToSelected() {
    const selected = this.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);
  }

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

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

  moveSelected() {
    const selected = this.gridApi.getSelectedNodes()[0];
    if (!selected) {
      console.log("No row was selected.");
      return;
    }
    const route = getRouteToNode(selected);
    fakeServer.moveEmployee(route, ["Robert Peterson"]);
  }

  onGridReady(params: GridReadyEvent) {
    this.gridApi = params.api;

    this.http
      .get<any[]>("https://www.ag-grid.com/example-assets/tree-data.json")
      .subscribe((data) => {
        const adjustedData = [
          {
            employeeId: -1,
            employeeName: "Robert Peterson",
            employmentType: "Founder",
            startDate: "24/01/1990",
          },
          ...data,
        ];
        const fakeServer = createFakeServer(adjustedData, params.api!);
        const datasource = createServerSideDatasource(fakeServer);
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }
}

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;
};
function getRouteToNode(rowNode: IRowNode): string[] {
  if (!rowNode.parent) {
    return [];
  }
  return [
    ...getRouteToNode(rowNode.parent),
    rowNode.key ? rowNode.key : rowNode.data.employeeName,
  ];
}
let latestId = 100000;
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;
}
```

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

## 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/angular-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 { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetRowIdFunc,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsRequest,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  ModuleRegistry,
  RowModelType,
  RowSelectionOptions,
  TextFilterModule,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";
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;
}

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<div class="example-wrapper">
    <ag-grid-angular
      style="width: 100%; height: 100%;"
      [defaultColDef]="defaultColDef"
      [autoGroupColumnDef]="autoGroupColumnDef"
      [rowModelType]="rowModelType"
      [treeData]="true"
      [columnDefs]="columnDefs"
      [cacheBlockSize]="cacheBlockSize"
      [rowSelection]="rowSelection"
      [isServerSideGroupOpenByDefault]="isServerSideGroupOpenByDefault"
      [getRowId]="getRowId"
      [isServerSideGroup]="isServerSideGroup"
      [getServerSideGroupKey]="getServerSideGroupKey"
      [rowData]="rowData"
      (gridReady)="onGridReady($event)"
    />
  </div> `,
})
export class AppComponent {
  defaultColDef: ColDef = {
    width: 235,
    flex: 1,
    sortable: false,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "employeeName",
  };
  rowModelType: RowModelType = "serverSide";
  columnDefs: ColDef[] = [
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "employmentType" },
    { field: "startDate" },
  ];
  cacheBlockSize = 10;
  rowSelection: RowSelectionOptions | "single" | "multiple" = {
    mode: "multiRow",
    groupSelects: "descendants",
  };
  isServerSideGroupOpenByDefault: 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: GetRowIdFunc = (row) => String(row.data.employeeId);
  isServerSideGroup: IsServerSideGroup = (dataItem) => dataItem.group;
  getServerSideGroupKey: GetServerSideGroupKey = (dataItem) =>
    dataItem.employeeName;
  rowData!: any[];

  constructor(private http: HttpClient) {}

  onGridReady(params: GridReadyEvent) {
    this.http
      .get<any[]>("https://www.ag-grid.com/example-assets/tree-data.json")
      .subscribe((data) => {
        const adjustedData = [
          {
            employeeId: -1,
            employeeName: "Robert Peterson",
            employmentType: "Founder",
            startDate: "24/01/1990",
          },
          ...data,
        ];
        const fakeServer = createFakeServer(adjustedData, params.api!);
        const datasource = createServerSideDatasource(fakeServer);
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }
}

let fakeServer: FakeServer;
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/angular)

## Filtering Tree Data

Server-Side Tree Data Filtering should behave the same as Client-Side [Tree Data Filtering](https://www.ag-grid.com/angular-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/angular-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 { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ISetFilterParams,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  KeyCreatorParams,
  ModuleRegistry,
  RowModelType,
  SetFilterValuesFuncParams,
  TextFilterModule,
  ValueFormatterParams,
  ValueGetterParams,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelModule,
  SetFilterModule,
  TreeDataModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

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

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<ag-grid-angular
    style="width: 100%; height: 100%;"
    [columnDefs]="columnDefs"
    [defaultColDef]="defaultColDef"
    [autoGroupColumnDef]="autoGroupColumnDef"
    [rowModelType]="rowModelType"
    [treeData]="true"
    [isServerSideGroupOpenByDefault]="isServerSideGroupOpenByDefault"
    [isServerSideGroup]="isServerSideGroup"
    [getServerSideGroupKey]="getServerSideGroupKey"
    [rowData]="rowData"
    (gridReady)="onGridReady($event)"
  /> `,
})
export class AppComponent {
  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>,
    },
  ];
  defaultColDef: ColDef = {
    width: 240,
    filter: "agTextColumnFilter",
    floatingFilter: true,
    flex: 1,
    sortable: false,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "employeeName",
    filter: "agSetColumnFilter",
    filterParams: {
      treeList: true,
      excelMode: "windows",
      keyCreator: treeDataKeyCreator,
      values: getEmployeesAsync,
    } as ISetFilterParams<any, string[]>,
  };
  rowModelType: RowModelType = "serverSide";
  isServerSideGroupOpenByDefault: IsServerSideGroupOpenByDefault = (
    params: IsServerSideGroupOpenByDefaultParams,
  ) => {
    // open first level by default
    return params.rowNode.level === 0;
  };
  isServerSideGroup: IsServerSideGroup = (dataItem: any) => {
    // indicate if node is a group
    return dataItem.underlings;
  };
  getServerSideGroupKey: GetServerSideGroupKey = (dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeName;
  };
  rowData!: any[];

  constructor(private http: HttpClient) {}

  onGridReady(params: GridReadyEvent) {
    this.http
      .get<any[]>("https://www.ag-grid.com/example-assets/tree-data.json")
      .subscribe((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
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }
}

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);
}
```

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

## 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 { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AgGridAngular } from "ag-grid-angular";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  ICellRendererParams,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

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

@Component({
  selector: "my-app",
  standalone: true,
  imports: [AgGridAngular],
  template: `<ag-grid-angular
    style="width: 100%; height: 100%;"
    [columnDefs]="columnDefs"
    [defaultColDef]="defaultColDef"
    [autoGroupColumnDef]="autoGroupColumnDef"
    [rowModelType]="rowModelType"
    [treeData]="true"
    [isServerSideGroupOpenByDefault]="isServerSideGroupOpenByDefault"
    [isServerSideGroup]="isServerSideGroup"
    [getServerSideGroupKey]="getServerSideGroupKey"
    [rowData]="rowData"
    (gridReady)="onGridReady($event)"
  /> `,
})
export class AppComponent {
  columnDefs: ColDef[] = [
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "jobTitle" },
    { field: "employmentType" },
  ];
  defaultColDef: ColDef = {
    width: 240,
    flex: 1,
    sortable: false,
  };
  autoGroupColumnDef: AutoGroupColumnDef = {
    field: "employeeName",
    cellRendererParams: {
      innerRenderer: (params: ICellRendererParams) => {
        // display employeeName rather than group key (employeeId)
        return params.data.employeeName;
      },
    },
  };
  rowModelType: RowModelType = "serverSide";
  isServerSideGroupOpenByDefault: IsServerSideGroupOpenByDefault = (
    params: IsServerSideGroupOpenByDefaultParams,
  ) => {
    // open first two levels by default
    return params.rowNode.level < 2;
  };
  isServerSideGroup: IsServerSideGroup = (dataItem: any) => {
    // indicate if node is a group
    return !!dataItem.underlings;
  };
  getServerSideGroupKey: GetServerSideGroupKey = (dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeId;
  };
  rowData!: any[];

  constructor(private http: HttpClient) {}

  onGridReady(params: GridReadyEvent) {
    this.http
      .get<any[]>("https://www.ag-grid.com/example-assets/tree-data.json")
      .subscribe((data) => {
        const datasource = createServerSideDatasource(data);
        params.api!.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) {
                    params.api!.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/angular)

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