Observables in Angular Treegrid component

An Observable is used extensively by Angular since it provides significant benefits over techniques for event handling, asynchronous programming, and handling multiple values.

You can also check on this video for Observable binding in Tree Grid:

Observable binding using Async pipe

TreeGrid data can be consumed from an Observable object by piping it through an async pipe. The async pipe is used to subscribe the observable object and resolve with the latest value emitted by it.

Data binding

The TreeGrid expects an object from the Observable. The emitted value should be an object with properties result and count.

import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { CrudService } from './data.service';
import { Tasks } from './tasks';
import { TreeGridComponent, DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
@Component({
    selector: 'app-root',
    template: `<ejs-treegrid #treegrid [dataSource]='data | async' [treeColumnIndex]='1' parentIdMapping='ParentId' idMapping='TaskId' hasChildMapping='isParent' (dataStateChange)= 'dataStateChange($event)' [allowPaging]="true" [allowSorting]="true" [pageSettings]="pageSettings">
        <e-columns>
            <e-column field='TaskId' headerText='Task ID' width='90' textAlign='Right'></e-column>
            <e-column field='TaskName' headerText='Task Name' width='170'></e-column>
            <e-column field='Progress' headerText='Progress' width='130' textAlign='Right'></e-column>
            <e-column field='Duration' headerText='Duration' width='80' textAlign='Right'></e-column>
        </e-columns>
                </ejs-treegrid>`
})
export class AppComponent implements OnInit {

    public data: Observable<DataStateChangeEventArgs>;
    public pageSettings: Object;
    public state: DataStateChangeEventArgs;
    @ViewChild('treegrid')
    public treegrid: TreeGridComponent;
    tasks: Tasks[];
    constructor(private dataService: DataService) {
        this.data = dataService;
    }

    public dataStateChange(state: DataStateChangeEventArgs): void {
        this.dataService.execute(state);
    }

    public ngOnInit(): void {
        this.pageSettings = { pageSize: 1, pageSizeMode: 'Root' };
        const state: any = { skip: 0, take: 1 };
        this.dataService.execute(state);
    }
}
import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { CrudService } from './data.service';
import { Tasks } from './tasks';
import { TreeGridComponent, DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
@Component({
    selector: 'app-root',
    template: `<ejs-treegrid #treegrid [dataSource]='data | async' [treeColumnIndex]='1' parentIdMapping='ParentId' idMapping='TaskId' hasChildMapping='isParent' (dataStateChange)= 'dataStateChange($event)' [allowPaging]="true" [allowSorting]="true" [pageSettings]="pageSettings">
        <e-columns>
            <e-column field='TaskId' headerText='Task ID' width='90' textAlign='Right'></e-column>
            <e-column field='TaskName' headerText='Task Name' width='170'></e-column>
            <e-column field='Progress' headerText='Progress' width='130' textAlign='Right'></e-column>
            <e-column field='Duration' headerText='Duration' width='80' textAlign='Right'></e-column>
        </e-columns>
                </ejs-treegrid>`
})
export class AppComponent implements OnInit {

    public data: Observable<DataStateChangeEventArgs>;
    public pageSettings: Object;
    public state: DataStateChangeEventArgs;
    @ViewChild('treegrid')
    public treegrid: TreeGridComponent;
    tasks: Tasks[];
    constructor(private dataService: DataService) {
        this.data = dataService;
    }

    public dataStateChange(state: DataStateChangeEventArgs): void {
        this.dataService.execute(state);
    }

    public ngOnInit(): void {
        this.pageSettings = { pageSize: 1, pageSizeMode: 'Root' };
        const state: any = { skip: 0, take: 1 };
        this.dataService.execute(state);
    }
}
import { DataManager, DataResult, Query } from '@syncfusion/ej2-data';
import { Injectable } from '@angular/core';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
@Injectable()
export class DataService extends Subject<Object> {

  private BASE_URL = '/api/tasks';  //// provide the service url required to fetch data from server  

   constructor(private http: HttpClient) {
    super();
  }
  private applyPaging(query: Query, state: any) {
    // Check if both 'take' and 'skip' values are available
    if (state.take && state.skip) {
      // Calculate pageSkip and pageTake values to get pageIndex and pageSize
      const pageSkip = state.skip / state.take + 1;
      const pageTake = state.take;
      query.page(pageSkip, pageTake);
    }
    // If if only 'take' is available and 'skip' is 0, apply paging for the first page.
    else if (state.skip === 0 && state.take) {
      query.page(1, state.take);
    }
  }

  public getData(state: DataStateChangeEventArgs): Observable<DataStateChangeEventArgs> {
   const query = new Query();
    // paging
    this.applyPaging(query, state);
    return this.http.get(`${this.BASE_URL}`).pipe(
      map((response: any[]) => {
        const currentResult: any = new DataManager(response).executeLocal(
          query
        );
       return {
          result: currentResult,
          count: response.length,
        };
      }))
  }
  public execute(state: DataStateChangeEventArgs): void {
    this.getData(state).subscribe(x => {
      super.next(x)
    });
  }
}

You should maintain the same Observable instance for every treegrid actions.
We have a limitation for Custom Binding feature of TreeGrid. This feature works only for Self Referential data binding with pageSizeMode as Root.

Handling child data

Using the custom binding feature you can bind the child data for a parent record as per your custom logic. When a parent record is expanded, dataStateChange event is triggered in which you can assign your custom data to the childData property of the dataStateChange event arguments.
After assigning the child data, childDataBind method should be called from the
dataStateChange event arguments to indicate that the data is bound.


import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { CrudService } from './data.service';
import { Tasks } from './tasks';
import { TreeGridComponent, DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
@Component({
    selector: 'app-root',
    template: `<ejs-treegrid #treegrid [dataSource]='data | async' [treeColumnIndex]='1' parentIdMapping='ParentId' idMapping='TaskId' hasChildMapping='isParent' height=265 (dataStateChange)= 'dataStateChange($event)' [allowPaging]="true" [allowSorting]="true" [pageSettings]="pageSettings">
        <e-columns>
            <e-column field='TaskId' headerText='Task ID' width='90' textAlign='Right'></e-column>
            <e-column field='TaskName' headerText='Task Name' width='170'></e-column>
            <e-column field='Progress' headerText='Progress' width='130' textAlign='Right'></e-column>
            <e-column field='Duration' headerText='Duration' width='80' textAlign='Right'></e-column>
        </e-columns>
                </ejs-treegrid>`
})
export class AppComponent implements OnInit {

    public data: Observable<DataStateChangeEventArgs>;
    public pageSettings: Object;
    public state: DataStateChangeEventArgs;
    @ViewChild('treegrid')
    public treegrid: TreeGridComponent;
    tasks: Tasks[];
    constructor(private dataService: DataService) {
        this.data = dataService;
    }

    public dataStateChange(state: DataStateChangeEventArgs): void {
         if (state.requestType === 'expand') {

         /////    assigning the child data for the expanded record.

            state.childData = <any>[
                { TaskId: 2, TaskName: 'VINER', ParentId: 1, Duration: 23, Progress: 34, isParent: false },
                { TaskId: 3, TaskName: 'JOHN', ParentId: 1, Duration: 23, Progress: 34, isParent: false },
                { TaskId: 4, TaskName: 'TOMSP', ParentId: 1, Duration: 23, Progress: 34, isParent: false }
            ]
            state.childDataBind();       //// to indicate that the child data is bound.
        } else {
            this.dataService.execute(state);
        }
    }

    public ngOnInit(): void {
        this.pageSettings = { pageSize: 1, pageSizeMode: 'Root' };
        const state: any = { skip: 0, take: 1 };
        this.dataService.execute(state);
    }
}
import { DataManager, Query, } from '@syncfusion/ej2-data';
import { Http } from '@angular/http';
import { Injectable } from '@angular/core';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class DataService extends Subject<Object> {

  private BASE_URL = '/api/tasks';  //// provide the service url required to fetch data from server
  private dataManager = new DataManager({
    url: this.BASE_URL,
    crossDomain: true
  });

  constructor(private http: Http) {
    super();
  }

  public getData(state: DataStateChangeEventArgs): Observable<DataStateChangeEventArgs> {
    const pageQuery = `$skip=${state.skip}&$top=${state.take}`;

      /// filter query for fetching only the root level records
    const treegridQuery = "$filter='ParentId eq null'";
    return this.http
      .get(`${this.BASE_URL}?${pageQuery}&${treegridQuery}&$inlinecount=allpages&$format=json`)
      .map((response: any) => response.json())
      .map((response: any) => (<DataResult>{
                result: response['d']['results'],
                count: parseInt(response['d']['__count'], 10)
            }))
      .map((data: any) => data);
  }

  public execute(state: DataStateChangeEventArgs): void {
      this.getData(state).subscribe(x => {
        super.next(x)
      });
  }
}

Handling TreeGrid actions

For TreeGrid actions such as paging, sorting, etc., the dataStateChange event is invoked. You have to query and resolve data using Observable in this event based on the state arguments.

import { Component, OnInit, Inject, ViewChild } from '@angular/core';
import { TreeGridComponent, DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { DataService } from './data.service';

@Component({
  selector: 'app-root',
  templateUrl: `<ejs-treegrid #treegrid [dataSource]='data | async' [treeColumnIndex]='1' parentIdMapping='ParentId' idMapping='TaskId' hasChildMapping='isParent' (dataStateChange)= 'dataStateChange($event)' [allowPaging]="true" [allowSorting]="true" [pageSettings]="pageSettings">
        <e-columns>
            <e-column field='TaskId' headerText='Task ID' width='90' textAlign='Right'></e-column>
            <e-column field='TaskName' headerText='Task Name' width='170'></e-column>
            <e-column field='Progress' headerText='Progress' width='130' textAlign='Right'></e-column>
            <e-column field='Duration' headerText='Duration' width='80' textAlign='Right'></e-column>
        </e-columns>
                </ejs-treegrid>`
})
export class AppComponent implements OnInit {
  title = 'app';
  @ViewChild('treegrid') private treegrid: TreeGridComponent;
  public data: Object;
  public state: DataStateChangeEventArgs;
  public pageSettings: any;

  constructor(@Inject(DataService) private service: DataService) {
    this.data = service;
  }

  public dataStateChange(state: DataStateChangeEventArgs): void {
    this.service.execute(state);
  }

  ngOnInit() {
    this.pageSettings = { pageCount: 4 };
    let state = { skip: 0, take: 12 };
    this.service.execute(state);
  }
}

When initial rendering, the dataStateChange event will not be triggered. You can perform the operation in the ngOnInit if you want the treegrid to show the record.

Perform CRUD operations

The dataSourceChanged event is triggered to update the treegrid data. You can perform the save operation based on the event arguments, and you need to call the endEdit method to indicate the completion of save operation.

import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { CrudService } from './crud.service';
import { Tasks } from './tasks';
import { DataSourceChangedEventArgs } from '@syncfusion/ej2-grids';
import { DataStateChangeEventArgs, TreeGridComponent } from '@syncfusion/ej2-angular-treegrid';
@Component({
    selector: 'app-root',
    template: `<ejs-treegrid #treegrid [dataSource]='data | async' [treeColumnIndex]='1' parentIdMapping='ParentId' idMapping='TaskId' hasChildMapping='isParent' (dataSourceChanged)='dataSourceChanged($event)' (dataStateChange)= 'dataStateChange($event)' [allowPaging]="true" [allowSorting]="true" [pageSettings]="pageSettings" [editSettings]="editSettings" [toolbar]="toolbar">
        <e-columns>
            <e-column field='TaskId' headerText='Task ID' width='90' textAlign='Right'></e-column>
            <e-column field='TaskName' headerText='Task Name' width='170'></e-column>
            <e-column field='Progress' headerText='Progress' width='130' textAlign='Right'></e-column>
            <e-column field='Duration' headerText='Duration' width='80' textAlign='Right'></e-column>
        </e-columns>
                </ejs-treegrid>`
})
export class AppComponent implements OnInit {

    public data: Observable<DataStateChangeEventArgs>;
    public pageOptions: Object;
    public editSettings: Object;
    public pagesettings: Object;
    public toolbar: string[];
    public state: DataStateChangeEventArgs;
    @ViewChild('treegrid')
    public treegrid: TreeGridComponent;
    tasks: Tasks[];
    constructor(private crudService: CrudService) {
        this.data = crudService;
    }

    public dataStateChange(state: DataStateChangeEventArgs): void {
        this.crudService.execute(state);
    }

    public dataSourceChanged(state: DataSourceChangedEventArgs): void {
        if (state.action === 'add') {
            this.crudService.addRecord(state).subscribe(() => {
                state.endEdit()
            });
            this.crudService.addRecord(state).subscribe(() => { }, error => console.log(error), () => {
                state.endEdit()
            });
        } else if (state.action === 'edit') {
            this.crudService.updateRecord(state).subscribe(() => {
                state.endEdit()
            }
            );
        } else if (state.requestType === 'delete') {
            this.crudService.deleteRecord(state).subscribe(() => {
                state.endEdit()
            });
        }
    }

    public ngOnInit(): void {
        this.editSettings = { allowEditing: true, allowAdding: true, allowDeleting: true, mode: 'Normal' };
        this.toolbar = ['Add', 'Edit', 'Delete', 'Update', 'Cancel'];
        this.pageSettings = { pageSize: 1, pageSizeMode: 'Root' };
        const state: any = { skip: 0, take: 1 };
        this.crudService.execute(state);
    }
}

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Subject } from 'rxjs/Subject';
import { Tasks } from './tasks';
import { DataStateChangeEventArgs, TreeGridComponent } from '@syncfusion/ej2-angular-treegrid';
import { DataSourceChangedEventArgs } from '@syncfusion/ej2-grids';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

@Injectable()
export class CrudService extends Subject<DataStateChangeEventArgs>  {

  private tasksUrl = 'api/tasks';  // URL to web api for fetching the data

  constructor(
    private http: HttpClient) {
    super();
  }

  public execute(state: any): void {
    if (state.requestType === 'expand') {
      this.getChildData(state).subscribe(x => super.next(x as DataStateChangeEventArgs));
    } else {
    this.getAllData(state).subscribe(x => super.next(x as DataStateChangeEventArgs));
  }
  }

  /** GET all data from the server */
  getAllData( state ?: any): Observable<any[]> {
    return this.http.get<Tasks[]>(this.tasksUrl)
      .map((response: any) => (<any>{
        result: state.take > 0 ? response.slice(0, state.take) : response,
        count: 2
      }))
  }

  getChildData( state ?: any): Observable<any[]> {
    return this.http.get<Tasks[]>(this.tasksUrl)
      .map((response: any) => (<any>{
        result : response.slice(1, 3),
      }))
    }

  /** POST: add a new record  to the server */
  addRecord(state: DataSourceChangedEventArgs): Observable<Tasks> {
    // you can apply empty string instead of state.data to get failure(error)
    return this.http.post<Tasks>(this.tasksUrl, state.data, httpOptions);
  }

  /** DELETE: delete the record from the server */
  deleteRecord(state: any): Observable<Tasks> {
    const id = state.data[0].id;
    const url = `${this.tasksUrl}/${id}`;

    return this.http.delete<Tasks>(url, httpOptions);
  }

  /** PUT: update the record on the server */
  updateRecord(state: DataSourceChangedEventArgs): Observable<any> {
    return this.http.put(this.tasksUrl, state.data, httpOptions);
  }

}

Calculate aggregates

The footer aggregate values should be calculated and sent along with the dataSource property as follows. The aggregate property of the data source should contain the aggregate value assigned to the field – type property. For example, the Sum aggregate value for the Duration field should be assigned to the Duration - sum property.

{
    result: [{..}, {..}, {..}, ...],
    count: 830,
    aggregates: { 'Duration - sum' : 450 }
}

The group footer and caption aggregate values can be calculated by the treegrid itself.

Provide Excel Filter data source

The dataStateChange event is triggered with appropriate arguments when the Excel filter requests the filter choice data source. You need to resolve the Excel filter data source using the dataSource resolver function from the state argument as follows.

import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { CrudService } from './data.service';
import { Tasks } from './tasks';
import { TreeGridComponent, DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
@Component({
    selector: 'app-root',
    template: `<ejs-treegrid #treegrid [dataSource]='data | async' [treeColumnIndex]='1' parentIdMapping='ParentId' idMapping='TaskId' hasChildMapping='isParent' (dataStateChange)= 'dataStateChange($event)' [allowPaging]="true" [allowSorting]="true" [pageSettings]="pageSettings">
        <e-columns>
            <e-column field='TaskId' headerText='Task ID' width='90' textAlign='Right'></e-column>
            <e-column field='TaskName' headerText='Task Name' width='170'></e-column>
            <e-column field='Progress' headerText='Progress' width='130' textAlign='Right'></e-column>
            <e-column field='Duration' headerText='Duration' width='80' textAlign='Right'></e-column>
        </e-columns>
                </ejs-treegrid>`
})
export class AppComponent implements OnInit {

    public data: Observable<DataStateChangeEventArgs>;
    public pageSettings: Object;
    public state: DataStateChangeEventArgs;
    @ViewChild('treegrid')
    public treegrid: TreeGridComponent;
    tasks: Tasks[];
    constructor(private dataService: DataService) {
        this.data = dataService;
    }

    public dataStateChange(state: DataStateChangeEventArgs): void {
      if (state.action.requestType === 'filterchoicerequest' || state.action.requestType === 'filtersearchbegin') {
        this.service.getData(state).subscribe((e) => state.dataSource(e));
      } else {
        this.service.execute(state);
      }
    }

    public ngOnInit(): void {
        this.pageSettings = { pageSize: 1, pageSizeMode: 'Root' };
        const state: any = { skip: 0, take: 1 };
        this.dataService.execute(state);
    }
}
import { DataManager, Query, } from '@syncfusion/ej2-data';
import { Http } from '@angular/http';
import { Injectable } from '@angular/core';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class DataService extends Subject<Object> {

  private BASE_URL = '/api/tasks';  //// provide the service url required to fetch data from server  

  constructor(private http: Http) {
    super();
  }

  public getData(state: DataStateChangeEventArgs): Observable<DataStateChangeEventArgs> {
    const pageQuery = `$skip=${state.skip}&$top=${state.take}`;

      /// filter query for fetching only the root level records
    const treegridQuery = "$filter='ParentId eq null'";
    return this.http
      .get(`${this.BASE_URL}?${pageQuery}&${treegridQuery}&$inlinecount=allpages&$format=json`)
      .map((response: any) => response.json())
      .map((response: any) => (<DataResult>{
                result: response['d']['results'],
                count: parseInt(response['d']['__count'], 10)
            }))
      .map((data: any) => data);
  }
  public execute(state: DataStateChangeEventArgs): void {
    this.getData(state).subscribe(x => {
      super.next(x)
    });
  }
}

Observable binding using without Async pipe

In Angular, Observables data can be bound to UI elements using the AsyncPipe, which simplifies the process of subscribing to observables and managing the subscription lifecycle. However, there are scenarios where you need to bind observable data to components without utilizing the async pipe. This approach offers more control over the subscription and data manipulation processes.

To bind observable data without using the async pipe in the TreeGrid, follow these steps:

  1. Subscribe to the observable data in the component.

  2. Manually update the data source of the grid when the observable emits new values.

Data binding

The custom binding feature of the Syncfusion Angular TreeGrid enables binding child data to a parent record based on application-specific logic, such as fetching from a server or filtering local data. When a parent record is expanded, the dataStateChange event is triggered. Within this event, assign an array of records matching the TreeGrid’s data model (e.g., with fields like TicketID and ParentTicketID) to the childData property of the event arguments. Then, call the childDataBind method to indicate that the data is bound. For robust applications, handle potential errors during data retrieval, such as failed server requests.

After assigning the child data, childDataBind method should be called from the dataStateChange event arguments to indicate that the data is bound.

import { Component, OnInit, ViewChild } from '@angular/core';
import { TreeGridAllModule, DataStateChangeEventArgs, TreeGridComponent, EditService, PageService, FilterService } from '@syncfusion/ej2-angular-treegrid';
import { TaskService } from './task-service';
import { AsyncPipe } from '@angular/common';
import { NgClass, NgIf, NgFor } from '@angular/common';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `<ejs-treegrid #treegrid [dataSource]='data | async' height='350' idMapping='TicketID'
            parentIdMapping='ParentTicketID' hasChildMapping='isParent' [treeColumnIndex]='1' allowPaging='true'
            allowSorting='true' [pageSettings]='pageSetting' allowFiltering="true"
            (dataStateChange)='dataStateChange($event)' gridLines="Both" [editSettings]='editSettings' >
            <e-columns>
                <e-column field='TicketID' headerText='Ticket ID' width='110' textAlign='Left' isPrimaryKey='true'></e-column>
                <e-column field='Title' textAlign='Left' headerText='Title' width='250'></e-column>
                <e-column field='Category' headerText='Category' width='120' textAlign='Left'></e-column>
                <e-column field='Priority' headerText='Priority' width='100' textAlign='Left'></e-column>
                <e-column field='Status' headerText='Status' width='120'textAlign='Left'></e-column>
                <e-column field='AssignedAgent' headerText='Assigned To' width='150' textAlign='Left'></e-column>
                <e-column field='CustomerName' headerText='Customer' width='140' textAlign='Left'></e-column>
                <e-column field='CreatedDate' headerText='Created Date' textAlign='Right' width='130' format='yMd' type='date'></e-column>
                <e-column field='DueDate' headerText='Due Date' textAlign='Right' width='130' format='yMd' type='date'></e-column>
            </e-columns>
          </ejs-treegrid>`,
  standalone: true,
  providers: [EditService, PageService, FilterService],
  imports: [TreeGridAllModule, AsyncPipe, NgClass],
})

export class AppComponent {
  public data: Observable<DataStateChangeEventArgs>;
  public pageSetting: Object;
  @ViewChild('treegrid')
  public treegrid: TreeGridComponent;
  constructor(private taskService: TaskService) {
    this.data = taskService;
  }

  public ngOnInit(): void {
    this.pageSetting = { pageSize: 10, pageCount: 4 };
    let state = { skip: 0, take: 10 };
    this.taskService.execute(state);
  }

  // Handles data state changes from the Tree Grid.
  public dataStateChange(state: DataStateChangeEventArgs): void {
    this.taskService.execute(state);
  }
}

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})

export class TaskService extends Subject<DataStateChangeEventArgs> {

  private BASE_URL = 'https://ej2services.syncfusion.com/angular/development/api/SupportTicketData';

  constructor() {
    super();
  }

  // Executes the data operation based on the provided grid state.
  public execute(state: any): void {
      this.getData(state).subscribe((x) => super.next(x));
  }

  // Fetches the main data based on the provided treegrid state.
  protected getData(state: any): Observable<DataStateChangeEventArgs> {
    const pageQuery = `$skip=${state.skip}&$top=${state.take}`;

    return this.fetchData(
      `${this.BASE_URL}?$inlinecount=allpages&${pageQuery}`
    ).pipe(
      map((response: any) => {
        const result = response['result'];
        const count = response['count'];
        return { result, count } as DataStateChangeEventArgs;
      })
    );
  }

  // Fetches data from the specified URL using the Fetch API and wraps it in an Observable.
  private fetchData(url: string): Observable<any> {
    return new Observable((observer) => {
      fetch(url)
        .then((response) => {
          if (!response.ok) {
            throw new Error('Network response was not ok');
          }
          return response.json();
        })
        .then((data) => {
          observer.next(data);
          observer.complete();
        })
        .catch((error) => {
          observer.error(error);
        });
    });
  }
}

Handling child data

Using the custom binding feature, child data can be bound to a parent record based on custom logic. When a parent record is expanded, the dataStateChange event is triggered. Assign the child data to the childData property of the event arguments and call the childDataBind method to indicate that the data is bound. For real-world applications, child data is typically fetched from a server.

import { Component, OnInit, ViewChild } from '@angular/core';
import { TreeGridAllModule, DataStateChangeEventArgs, TreeGridComponent, EditService, PageService, FilterService } from '@syncfusion/ej2-angular-treegrid';
import { TaskService } from './task-service';
import { AsyncPipe } from '@angular/common';
import { NgClass, NgIf, NgFor } from '@angular/common';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `<ejs-treegrid #treegrid [dataSource]='data | async' height='350' idMapping='TicketID'
  parentIdMapping='ParentTicketID' hasChildMapping='isParent' [treeColumnIndex]='1' allowPaging='true'
  allowSorting='true' [pageSettings]='pageSetting' allowFiltering="true"
  (dataStateChange)='dataStateChange($event)' gridLines="Both" [editSettings]='editSettings' >
  <e-columns>
      <e-column field='TicketID' headerText='Ticket ID' width='110' textAlign='Left' isPrimaryKey='true'></e-column>
      <e-column field='Title' textAlign='Left' headerText='Title' width='250'></e-column>
      <e-column field='Category' headerText='Category' width='120' textAlign='Left'></e-column>
      <e-column field='Priority' headerText='Priority' width='100' textAlign='Left'></e-column>
      <e-column field='Status' headerText='Status' width='120'textAlign='Left'></e-column>
      <e-column field='AssignedAgent' headerText='Assigned To' width='150' textAlign='Left'></e-column>
      <e-column field='CustomerName' headerText='Customer' width='140' textAlign='Left'></e-column>
      <e-column field='CreatedDate' headerText='Created Date' textAlign='Right' width='130' format='yMd' type='date'></e-column>
      <e-column field='DueDate' headerText='Due Date' textAlign='Right' width='130' format='yMd' type='date'></e-column>
  </e-columns>
</ejs-treegrid>`,
  standalone: true,
  providers: [EditService, PageService, FilterService],
  imports: [TreeGridAllModule, AsyncPipe, NgClass],
})

export class AppComponent {
  public data: Observable<DataStateChangeEventArgs>;
  public pageSetting: Object;
  @ViewChild('treegrid')
  public treegrid: TreeGridComponent;
  constructor(private taskService: TaskService) {
    this.data = taskService;
  }

  public ngOnInit(): void {
    this.pageSetting = { pageSize: 10, pageCount: 4 };
    let state = { skip: 0, take: 10 };
    this.taskService.execute(state);
  }

  // Handles data state changes from the Tree Grid.
  public dataStateChange(state: DataStateChangeEventArgs): void {
    this.taskService.execute(state);
  }
}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class TaskService extends Subject<DataStateChangeEventArgs> {
  private BASE_URL =
    'https://ej2services.syncfusion.com/angular/development/api/SupportTicketData';
  constructor() {
    super();
  }

  // Executes the data operation based on the provided grid state.
  public execute(state: any): void {
    if (state.requestType === 'expand') {
      this.getChildData(state).subscribe((childRecords: any) => {
        state.childData = childRecords.result;
        state.childDataBind();
      });
    } else {
      this.getData(state).subscribe((x) => super.next(x));
    }
  }

  // Fetches child records for a given parent record when a row is expanded.
  public getChildData(state: any): Observable<DataStateChangeEventArgs> {
    return this.fetchData(
      `${this.BASE_URL}?$filter=ParentTicketID%20eq%20${state.data.TicketID}`
    ).pipe(
      map((response: any) => {
        const parentId = state.data.id;
        const result = response['result'];
        const count = response['count'];
        return { result, count } as DataStateChangeEventArgs;
      })
    );
  }

  // Fetches data from the specified URL using the Fetch API and wraps it in an Observable.
  private fetchData(url: string): Observable<any> {
    return new Observable((observer) => {
      fetch(url)
        .then((response) => {
          if (!response.ok) {
            throw new Error('Network response was not ok');
          }
          return response.json();
        })
        .then((data) => {
          observer.next(data);
          observer.complete();
        })
        .catch((error) => {
          observer.error(error);
        });
    });
  }
}

Handling TreeGrid actions

For TreeGrid actions such as paging, sorting, and filtering, the dataStateChange event is triggered. Query and resolve data using an Observable based on the state arguments, and update the TreeGrid’s dataSource property manually in the subscription

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';


@Injectable({
  providedIn: 'root',
})

export class TaskService extends Subject<DataStateChangeEventArgs> {
  private BASE_URL = 'https://ej2services.syncfusion.com/angular/development/api/SupportTicketData';

  constructor() {
    super();
  }

  // Executes the data operation based on the provided grid state.
  public execute(state: any): void {
    if (state.requestType === 'expand') {
      this.getChildData(state).subscribe((childRecords: any) => {
        state.childData = childRecords.result;
        state.childDataBind();
      });
    } else {
      this.getData(state).subscribe((x) => super.next(x));
    }
  }

  // Fetches child records for a given parent record when a row is expanded.
  public getChildData(state: any): Observable<DataStateChangeEventArgs> {
    return this.fetchData(
      `${this.BASE_URL}?$filter=ParentTicketID%20eq%20${state.data.TicketID}`
    ).pipe(
      map((response: any) => {
        const parentId = state.data.id;
        const result = response['result'];
        const count = response['count'];
        return { result, count } as DataStateChangeEventArgs;
      })
    );
  }

  // Fetches the main data based on the provided treegrid state (paging, sorting, filtering).
  protected getData(state: any): Observable<DataStateChangeEventArgs> {
    const pageQuery = `$skip=${state.skip}&$top=${state.take}`;
    let sortQuery: string = '';
    let filterQuery: string = '';
    if (state.where) {
      filterQuery = this.buildFilterQuery(state.where);
    } else {
      filterQuery = '$filter=ParentTicketID eq null';
    }
    if (state.search) {
      filterQuery += this.buildSearchQuery(state.search);
    }
    if ((state.sorted || []).length) {
      sortQuery =
        `&$orderby=` +
        state.sorted
          .map((obj: any) => {
            return obj.direction === 'descending'
              ? `${obj.name} desc`
              : obj.name;
          })
          .reverse()
          .join(',');
    }

    return this.fetchData(
      `${this.BASE_URL}?$inlinecount=allpages&${pageQuery}&${filterQuery}&${sortQuery}`
    ).pipe(
      map((response: any) => {
        const result = response['result'];
        const count = response['count'];
        return { result, count } as DataStateChangeEventArgs;
      })
    );
  }

  // Builds the filter query string from the treegrid's filter settings.
  private buildFilterQuery(where: any[]): string {
    if (!where || where.length === 0) return '$filter=ParentTicketID eq null';
    const andConds: string[] = [];
    for (const cond of where) {
      if (cond.predicates?.length) {
        const groupFilters = cond.predicates.map((pred: any) =>
          this.predicateToString(pred)
        );
        andConds.push(`(${groupFilters.join(` ${cond.condition ?? 'and'} `)})`);
      } else {
        andConds.push(this.predicateToString(cond));
      }
    }
    if (andConds.length > 0) {
      return `$filter=ParentTicketID eq null and ${andConds.join(' and ')}`;
    }
    return '$filter=ParentTicketID eq null';
  }

  // Builds the OData search query string from the grid's search settings.
  private buildSearchQuery(search: any[]): string {
    if (!search || !search.length) return '';
    const s = search[0];
    const searchStr = (s.key as string).toLowerCase();
    const fields = s.fields || [];
    const orConds: string[] = [];

    fields.forEach((field: string) => {
      orConds.push(
        `substringof('${searchStr}',tolower(cast(${field}, 'Edm.String')))`
      );
    });
    if (!orConds.length) return '';
    return ` and (${orConds.join(' or ')})`;
  }

  // Converts a single filter predicate object to the filter string.
  private predicateToString(pred: any): string {
    let field = pred.field;
    let value = pred.value;
    let ignoreCase = pred.ignoreCase;
    let valStr = typeof value === 'string' ? `'${value}'` : value;

    switch (pred.operator) {
      case 'equal':
        if (ignoreCase && typeof value === 'string') {
          return `(tolower(${field}) eq '${value.toLowerCase()}')`;
        }
        return `${field} eq ${valStr}`;
      case 'contains':
        if (ignoreCase && typeof value === 'string') {
          return `contains(tolower(${field}), '${value.toLowerCase()}')`;
        }
        return `contains(${field}, ${valStr})`;
      case 'startswith':
        if (ignoreCase && typeof value === 'string') {
          return `startswith(tolower(${field}), '${value.toLowerCase()}')`;
        }
        return `startswith(${field}, ${valStr})`;
      default:
        return '';
    }
  }
}

When initial rendering, the dataStateChange event will not be triggered. You can perform the operation in the ngOnInit if you want the treegrid to show the record.

Perform CRUD operations

The dataSourceChanged event is triggered to update TreeGrid data during CRUD operations. The DataSourceChangedEventArgs object provides properties such as action (e.g., add, edit, delete), data (the modified record), and childData (for nested records). Perform the save operation based on these arguments, update the dataSource manually, and call the endEdit method to complete the operation.

import { Component, OnInit, ViewChild } from '@angular/core';
import { TreeGridAllModule, DataStateChangeEventArgs, TreeGridComponent,EditService, PageService, FilterService,
} from '@syncfusion/ej2-angular-treegrid';
import { TaskService } from './task-service';
import { AsyncPipe } from '@angular/common';
import { NgClass, NgIf, NgFor } from '@angular/common';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `<ejs-treegrid #treegrid [dataSource]='data | async' height='350' idMapping='TicketID'
              parentIdMapping='ParentTicketID' hasChildMapping='isParent' [treeColumnIndex]='1' allowPaging='true'
              allowSorting='true' [pageSettings]='pageSetting' allowFiltering="true"
              (dataStateChange)='dataStateChange($event)' (dataSourceChanged)='dataSourceChange($event)' gridLines="Both" [editSettings]='editSettings' >
              <e-columns>
                  <e-column field='TicketID' headerText='Ticket ID' width='110' textAlign='Left' isPrimaryKey='true'></e-column>
                  <e-column field='Title' textAlign='Left' headerText='Title' width='250' clipMode="EllipsisWithTooltip"></e-column>
                  <e-column field='Category' headerText='Category' width='120' textAlign='Left'></e-column>
                  <e-column field='Priority' headerText='Priority' width='100' textAlign='Left'></e-column>
                  <e-column field='Status' headerText='Status' width='120'textAlign='Left'></e-column>
                  <e-column field='AssignedAgent' headerText='Assigned To' width='150' textAlign='Left'></e-column>
                  <e-column field='CustomerName' headerText='Customer' width='140' textAlign='Left'></e-column>
                  <e-column field='CreatedDate' headerText='Created Date' [allowFiltering]="false" textAlign='Right' width='130' format='yMd' type='date' editType='datepickeredit' [validationRules]='daterules' [edit]="dateeditparam"></e-column>
                  <e-column field='DueDate' headerText='Due Date' [allowFiltering]="false" textAlign='Right' width='130' format='yMd' type='date' editType='datepickeredit' [validationRules]='daterules' [edit]="dateeditparam"></e-column>
              </e-columns>
            </ejs-treegrid>`,
  standalone: true,
  providers: [EditService, PageService, FilterService],
  imports: [TreeGridAllModule, AsyncPipe, NgClass],
})

export class AppComponent {
  public data: Observable<DataStateChangeEventArgs>;
  public pageSetting: Object;
  public editSettings: Object;
  public daterules: Object;
  public dateeditparam: Object;
  @ViewChild('treegrid')
  public treegrid: TreeGridComponent;

  constructor(private taskService: TaskService) {
    this.data = taskService;
  }

  public ngOnInit(): void {
    this.editSettings = {
      allowEditing: true,
      allowAdding: true,
      allowDeleting: true,
      mode: 'Row',
    };
    this.pageSetting = { pageSize: 10, pageCount: 4 };
    this.daterules = { date: true, required: true };
    this.dateeditparam = { params: { format: 'M/d/yyyy' } };
    let state = { skip: 0, take: 10 };
    this.taskService.execute(state);
  }

  // Handles data state changes from the Tree Grid (e.g., paging, sorting, filtering).
  public dataStateChange(state: DataStateChangeEventArgs): void {
    this.taskService.execute(state);
  }

  // Handles data source changes from the Tree Grid (e.g., CRUD operations).
  public dataSourceChange(state: any): void {
    if (state.action == 'add') {
      this.taskService.addRecord(state).subscribe({
        next: () => {
          (state as any).endEdit();
        },
      });
    } else if (state.action == 'edit') {
      this.taskService.updateRecord(state).subscribe({
        next: () => {
          (state as any).endEdit();
        },
      });
    } else if (state.requestType == 'delete') {
      this.taskService.deleteRecord(state).subscribe({
        next: () => {
          (state as any).endEdit();
        },
      });
    }
  }
}

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class TaskService extends Subject<DataStateChangeEventArgs> {
  private BASE_URL = 'https://ej2services.syncfusion.com/angular/development/api/SupportTicketData';

  constructor() {
    super();
  }

  // Executes the data operation based on the provided grid state.
  public execute(state: any): void {
    if (state.requestType === 'expand') {
      this.getChildData(state).subscribe((childRecords: any) => {
        state.childData = childRecords.result;
        state.childDataBind();
      });
    } else {
      this.getData(state).subscribe((x) => super.next(x));
    }
  }

  // Fetches child records for a given parent record when a row is expanded.
  public getChildData(state: any): Observable<DataStateChangeEventArgs> {
    return this.fetchData(
      `${this.BASE_URL}?$filter=ParentTicketID%20eq%20${state.data.TicketID}`
    ).pipe(
      map((response: any) => {
        const parentId = state.data.id;
        const result = response['result'];
        const count = response['count'];
        return { result, count } as DataStateChangeEventArgs;
      })
    );
  }

  // Fetches the main data based on the provided treegrid state (paging, sorting, filtering).
  protected getData(state: any): Observable<DataStateChangeEventArgs> {
    const pageQuery = `$skip=${state.skip}&$top=${state.take}`;
    return this.fetchData(
      `${this.BASE_URL}?$inlinecount=allpages&${pageQuery}`
    ).pipe(
      map((response: any) => {
        const result = response['result'];
        const count = response['count'];
        return { result, count } as DataStateChangeEventArgs;
      })
    );
  }

  // Fetches data from the specified URL using the Fetch API and wraps it in an Observable.
  private fetchData(url: string): Observable<any> {
    return new Observable((observer) => {
      fetch(url)
        .then((response) => {
          if (!response.ok) {
            throw new Error('Network response was not ok');
          }
          return response.json();
        })
        .then((data) => {
          observer.next(data);
          observer.complete();
        })
        .catch((error) => {
          observer.error(error);
        });
    });
  }

  // Deletes a record from the database.
  deleteRecord(state: any): Observable<any> {
    const id = state.data[0]?.TicketID || state.data[0]?.id;
    const url = `${this.BASE_URL}/${id}`;
    return new Observable((observer) => {
      fetch(url, { method: 'DELETE' })
        .then((data) => {
          observer.next(data);
          observer.complete();
        })
        .catch((error) => {
          observer.error(error);
        });
    });
  }

  // Updates an existing record in the database.
  updateRecord(state: any): Observable<any> {
    const url = `${this.BASE_URL}`;
    const data1 = state.data;
    return new Observable((observer) => {
      fetch(url, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data1),
      })
        .then((data) => {
          observer.next(data);
          observer.complete();
        })
        .catch((error) => {
          observer.error(error);
        });
    });
  }

  // Adds a new record to the database.
  addRecord(state: any): Observable<any> {
    const url = `${this.BASE_URL}`;
    const data1 = state.data;
    return new Observable((observer) => {
      fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data1),
      })
        .then((data) => {
          observer.next(data);
          observer.complete();
        })
        .catch((error) => {
          observer.error(error);
        });
    });
  }
}

Calculate aggregates

The footer aggregate values should be calculated and sent along with the dataSource property as follows. The aggregate property of the data source should contain the aggregate value assigned to the field – type property. For example, the Sum aggregate value for the Duration field should be assigned to the Duration - sum property.

{
    result: [{..}, {..}, {..}, ...],
    count: 830,
    aggregates: { 'Duration - sum' : 450 }
}

The group footer and caption aggregate values can be calculated by the treegrid itself.

Provide Excel Filter data source

The dataStateChange event is triggered with appropriate arguments when the Excel filter requests the filter choice data source. You need to resolve the Excel filter data source using the dataSource resolver function from the state argument as follows.

import { Component, OnInit, ViewChild } from '@angular/core';

import { TreeGridAllModule, DataStateChangeEventArgs, TreeGridComponent, EditService, PageService, FilterService } from '@syncfusion/ej2-angular-treegrid';
import { TaskService } from './task-service';
import { AsyncPipe } from '@angular/common';
import { NgClass, NgIf, NgFor } from '@angular/common';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `<ejs-treegrid #treegrid [dataSource]='data | async' height='350' idMapping='TicketID'
              parentIdMapping='ParentTicketID' hasChildMapping='isParent' [treeColumnIndex]='1' allowPaging='true' [pageSettings]='pageSetting'  [filterSettings]='filterSettings' allowFiltering="true" (dataStateChange)='dataStateChange($event)'>
              <e-columns>
                  <e-column field='TicketID' headerText='Ticket ID' width='110' textAlign='Left' isPrimaryKey='true'></e-column>
                  <e-column field='Title' textAlign='Left' headerText='Title' width='250'></e-column>
                  <e-column field='Category' headerText='Category' width='120' textAlign='Left'></e-column>
                  <e-column field='Priority' headerText='Priority' width='100' textAlign='Left'></e-column>
                  <e-column field='Status' headerText='Status' width='120'textAlign='Left'></e-column>
                  <e-column field='AssignedAgent' headerText='Assigned To' width='150' textAlign='Left'></e-column>
                  <e-column field='CustomerName' headerText='Customer' width='140' textAlign='Left'></e-column>
                  <e-column field='CreatedDate' headerText='Created Date' textAlign='Right' width='130' format='yMd' type='date'></e-column>
                  <e-column field='DueDate' headerText='Due Date' textAlign='Right' width='130' format='yMd' type='date'></e-column>
              </e-columns>
            </ejs-treegrid>`,
  standalone: true,
  providers: [PageService, FilterService],
  imports: [TreeGridAllModule, AsyncPipe, NgClass],filterSettings
})

export class AppComponent {
  public data: Observable<DataStateChangeEventArgs>;
  public pageSetting: Object;
  public filterSettings: Object;
  @ViewChild('treegrid')
  public treegrid: TreeGridComponent;

  constructor(private taskService: TaskService) {
    this.data = taskService;
  }

  public ngOnInit(): void {
    this.filterSettings = { type: 'Excel'}
    let state = { skip: 0, take: 10 };
    this.taskService.execute(state);
  }

  // Handles data state changes from the Tree Grid (e.g., paging, sorting, filtering).
  public dataStateChange(state: DataStateChangeEventArgs): void {
    if (state.action.requestType === 'filterchoicerequest' || state.action.requestType === 'filtersearchbegin') {
      this.taskservice.getData(state).subscribe((e) => state.dataSource(e));
    } else {
      this.taskservice.execute(state);
    }
  }
}

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-treegrid';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class TaskService extends Subject<DataStateChangeEventArgs> {
  private BASE_URL = 'https://ej2services.syncfusion.com/angular/development/api/SupportTicketData';

  constructor() {
    super();
  }

  // Executes the data operation based on the provided grid state.
  public execute(state: any): void {
      this.getData(state).subscribe((x) => super.next(x));
  }

  // Fetches the main data based on the provided treegrid state (paging, sorting, filtering).
  protected getData(state: any): Observable<DataStateChangeEventArgs> {
    const pageQuery = `$skip=${state.skip}&$top=${state.take}`;
    let filterQuery: string = '';
    if (state.where) {
      filterQuery = state.where;
    } else {
      filterQuery = '$filter=ParentTicketID eq null';
    }
    return this.fetchData(
      `${this.BASE_URL}?$inlinecount=allpages&${pageQuery}&${filterQuery}`
    ).pipe(
      map((response: any) => {
        const result = response['result'];
        const count = response['count'];
        return { result, count } as DataStateChangeEventArgs;
      })
    );
  }

  // Fetches data from the specified URL using the Fetch API and wraps it in an Observable.
  private fetchData(url: string): Observable<any> {
    return new Observable((observer) => {
      fetch(url)
        .then((response) => {
          if (!response.ok) {
            throw new Error('Network response was not ok');
          }
          return response.json();
        })
        .then((data) => {
          observer.next(data);
          observer.complete();
        })
        .catch((error) => {
          observer.error(error);
        });
    });
  }
}

You can refer to our Angular Tree Grid feature tour page for its groundbreaking feature representations. You can also explore our Angular Tree Grid example to knows how to present and manipulate data.