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 withpageSizeMode
asRoot
.
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 thengOnInit
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:
-
Subscribe to the observable data in the component.
-
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 thengOnInit
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 ourAngular Tree Grid example
to knows how to present and manipulate data.