Remote Data in Angular Grid Component

25 Aug 202524 minutes to read

The Angular Grid component excels at binding remote data sources, enabling seamless integration with RESTful services and real-time data scenarios. Remote data binding involves connecting the grid to server-hosted data by assigning a DataManager instance to the dataSource property, specifying the endpoint URL where data resides.

This approach leverages Angular’s powerful Observable pattern for data retrieval and operations, providing robust event handling, asynchronous programming capabilities, and efficient concurrent value management in modern Angular applications.

Binding observable data using async pipe

Observables represent a fundamental reactive programming concept widely adopted throughout the Angular framework. An Observable creates a stream of data or events that can be observed over time, providing an elegant solution for handling asynchronous operations including user input processing, HTTP requests, and event management.

The Syncfusion Angular Grid seamlessly integrates with Observables through the async pipe, enabling effortless binding of grid data. The AsyncPipe automatically subscribes to observables, extracting the latest emitted value with the required result and count properties structure that aligns perfectly with the grid’s data expectations.

The Syncfusion Grid component delivers comprehensive functionality for handling grid actions including searching, filtering, sorting, grouping, and paging. These interactions trigger the dataStateChange event, providing opportunities to manage and manipulate data according to user interactions.

Using the dataStateChange event

The dataStateChange event triggers whenever you perform actions that modify the grid’s data state, such as changing pages, applying sorting, or grouping. This event provides detailed information about the performed action and current grid state, including parameters like page number, sorting details, and filtering criteria.

To implement the dataStateChange event effectively:

  1. Subscribe to the event: In your component, subscribe to the dataStateChange event using the appropriate event handler function. This function executes whenever you interact with the grid.

  2. Handle data state: Inside the event handler function, access the event arguments to determine user actions and intentions. The action property indicates the type of operation performed (paging, sorting, grouping).

The dataStateChange event will not trigger during initial rendering. To display records on grid initialization, perform the operation in the ngOnInit lifecycle hook.

Handling searching operation

When performing search operations in the grid, the dataStateChange event triggers, providing access to the following referenced arguments:

Searching

You can modify the Observable based on the new grid data state for search actions:

private applySearching(query: Query, search: Array<any>): void {
  // Check if a search operation is requested
  if (search && search.length > 0) {
    // Extract the search key and fields from the search array
    const { fields, key } = search[0];
    // perform search operation using the field and key on the query
    query.search(key, fields);
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // search
  if (state.search) {
    this.applySearching(query, state.search);
  }
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Handling filtering operation

When filtering operations are performed in the grid, the dataStateChange event triggers, providing access to the following referenced arguments:

FilterBar

You can modify the Observable based on the new grid data state for filter actions:

private applyFiltering(query: Query, filter: any): void {
  // Check if filter columns are specified
  if (filter.columns && filter.columns.length) {
    // Apply filtering for each specified column
    for (let i = 0; i < filter.columns.length; i++) {
      const field = filter.columns[i].field;
      const operator = filter.columns[i].operator;
      const value = filter.columns[i].value;
      query.where(field, operator, value);
    }
  } else {
    // Apply filtering based on direct filter conditions
    for (let i = 0; i < filter.length; i++) {
      const { fn, e } = filter[i];
      if (fn === 'onWhere') {
        query.where(e as string);
      }
    }
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // filtering
  if (state.where) {
    this.applyFiltering(query, action.queries);
  }
  // initial filtering
  if (state.filter && state.filter.columns && state.filter.columns.length) {
    this.applyFiltering(query, state.filter);
  }
  // To get the count of the data
  query.isCountRequired = true;
  
  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Filtering Multiple Values

When filtering multiple values, you can retrieve predicates as arguments in the dataStateChange event. Create your predicate execution based on the predicates values.

Handling sorting operation

When sorting operations are performed in the grid, the dataStateChange event triggers, and within this event, you can access the following referenced arguments:

Sorting

When performing multi-column sorting, you can access the following referenced arguments in the dataStateChange event:

Multi Sorting

You can modify the Observable based on the new grid data state for sort actions:

private applySorting(query: Query, sorted: sortInfo[]): void {
  // Check if sorting data is available
  if (sorted && sorted.length > 0) {
    // Iterate through each sorting info
    sorted.forEach(sort => {
      // Get the sort field name either by name or field
      const sortField = sort.name || sort.field;
      // Perform sort operation using the query based on the field name and direction
      query.sortBy(sortField as string, sort.direction);
    });
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // sorting
  if (state.sorted) {
    state.sorted.length ? this.applySorting(query, state.sorted) :
      // initial sorting
      state.sorted.columns.length ? this.applySorting(query, state.sorted.columns) : null;
  } 
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  ); 
}

Handling paging operation

When paging operations are performed in the grid, the dataStateChange event triggers, and within this event, you can access the following referenced arguments:

Paging

You can modify the Observable based on the new grid data state for page actions:

private applyPaging(query: Query, state: any): void {
  // 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 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);
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // paging
  this.applyPaging(query, state);
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Handling grouping operation

When grouping operations are performed in the grid, the dataStateChange event triggers, providing access to the following referenced arguments:

Grouping

You can modify the Observable based on the new grid data state for group actions:

private applyGrouping(query: Query, group: any): void {
  // Check if grouping data is available
  if (group.length > 0) {
    // Iterate through each group info
    group.forEach((column: string) => {
      // perform group operation using the column on the query
      query.group(column);
    });
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // grouping
  if (state.group) {
    state.group.length ? this.applyGrouping(query, state.group) :
      // initial grouping
      state.group.columns.length ? this.applyGrouping(query, state.group.columns) : null;
  }
  // To get the count of the data
  query.isCountRequired = true;
  
  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

To utilize group actions effectively, you must manage the sorting query appropriately.

Lazy load grouping

In Angular applications, lazy loading refers to the technique of loading data dynamically when needed, rather than loading everything upfront. Lazy load grouping allows efficient loading and display of grouped data by fetching only the required data on demand.

To enable this feature, set the groupSettings.enableLazyLoading property to true. You must also manage the state based on the initial grid action:

public ngOnInit(): void {
  this.groupOptions = { columns: ['ProductName'], enableLazyLoading: true };
  const state = { skip: 0, take: 12, group: this.groupOptions };
  this.crudService.execute(state, query);
}

Based on the initial state, you can access the arguments as shown below:

Lazy load group

You can modify the Observable based on the grid state:

private applyGrouping(query: Query, group: any): void {
  // Check if grouping data is available
  if (group.length > 0) {
    // Iterate through each group info
    group.forEach((column: string) => {
      // perform group operation using the column on the query
      query.group(column);
    });
  }
}

private applyLazyLoad = (query: Query, state: any): void => {
  if (state.isLazyLoad) {
    // Configure lazy loading for the main data
    query.lazyLoad.push({ key: 'isLazyLoad', value: true });
    // If on-demand group loading is enabled, configure lazy loading for grouped data
    if (state.onDemandGroupInfo) {
      query.lazyLoad.push({
        key: 'onDemandGroupInfo',
        value: state.action.lazyLoadQuery,
      });
    }
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // grouping
  if (state.group) {
    state.group.length ? this.applyGrouping(query, state.group) :
      // initial grouping
      state.group.columns.length ? this.applyGrouping(query, state.group.columns) : null;
  }
  // lazy load grouping
  this.applyLazyLoad(query, state);
  // initial grouping with lazy load
  if (state.group && state.group.enableLazyLoading) {
    query.lazyLoad.push({ key: 'isLazyLoad', value: true });
  }
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Further information can be accessed in the respective documentation for lazy load grouping.

The complete example is available in the handling CRUD operations topic.

Handling CRUD operations

The Grid component provides powerful options for dynamically inserting, deleting, and updating records, enabling direct data modification within the grid interface. This functionality proves essential when performing CRUD (Create, Read, Update, Delete) operations seamlessly.

Integrating CRUD Operations

To implement CRUD operations using Syncfusion Grid, follow these steps:

  1. Configure grid settings: Set up the necessary grid settings for editing, adding, and deleting records. Define the toolbar options to facilitate user interactions.

  2. Handle data state changes: Utilize the dataStateChange event to respond to changes in the grid’s data state. This event triggers whenever you interact with the grid through paging or sorting.

  3. Execute CRUD operations: Within the event handler for dataSourceChanged, implement logic to handle various CRUD actions based on the action or requestType property of the event arguments.

  4. Call endEdit method: After performing CRUD operations (adding, editing, or deleting), call the endEdit method to signal operation completion and update the grid accordingly.

Insert operation

When an insert operation is performed in the grid, the dataSourceChanged event triggers, providing access to the following referenced arguments:

Insert record

/** POST: add a new record to the server */
addRecord(state: DataSourceChangedEventArgs): Observable<Customer> {
  return this.http.post<Customer>(this.customersUrl, state.data, httpOptions);
}

Edit operation

When an edit operation is performed in the grid, the dataSourceChanged event triggers, providing access to the following referenced arguments:

Edit record

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

Delete operation

When a delete operation is performed in the grid, the dataSourceChanged event triggers, providing access to the following referenced arguments:

Delete record

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

The following example demonstrates how to bind observable data using async pipe to handle grid actions and CRUD operations:

import { Component, OnInit, ViewChild } from '@angular/core';
import { GridAllModule, GridComponent } from '@syncfusion/ej2-angular-grids';
import { DataStateChangeEventArgs, DataSourceChangedEventArgs, SortService, FilterService, CommandColumnService, SearchService, ColumnChooser, isComplexField } from '@syncfusion/ej2-angular-grids';
import { EditService, ToolbarService, PageService } from '@syncfusion/ej2-angular-grids';
import { CrudService } from './crud.service';
import { Observable } from 'rxjs';
import { Query } from '@syncfusion/ej2-data';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-root',
  template: `
  <div style="padding-top:100px">
    <ejs-grid #grid [dataSource]='data | async' allowPaging="true" allowGrouping="true" [groupSettings]="groupOptions" 
    allowSorting="true" [sortSettings]="sortOptions" allowFiltering="true" [filterSettings]="filterOptions" [editSettings]='editSettings' [toolbar]='toolbar'
    (dataSourceChanged)='dataSourceChanged($event)' (dataStateChange)= 'dataStateChange($event)' height='315px'>
      <e-columns>
        <e-column field='id' headerText='Order ID' width='90' textAlign='Right' [validationRules]='orderIDRules' isPrimaryKey='true'></e-column>
        <e-column field="CustomerName" headerText="Customer Name" width="100"></e-column>
        <e-column field='ProductID' headerText='Product ID' width=100></e-column>
        <e-column field='ProductName' headerText='Product Name' width='160'></e-column>
      </e-columns>
    </ejs-grid>
  </div>`,
  standalone: true,
  imports: [
    CommonModule,
    GridAllModule,
  ],
  providers: [ToolbarService, SortService, EditService, FilterService, PageService, CommandColumnService, SearchService, CrudService]
})
export class AppComponent implements OnInit {
  public data?: Observable<any>;
  public state?: DataStateChangeEventArgs;
  public toolbar?: string[];
  public editSettings?: Object;
  public orderIDRules?: object;
  @ViewChild('grid')
  public grid?: GridComponent;
  public groupOptions?: object;  
  public filterOptions?: object;
  public sortOptions?: object;
  constructor(public crudService: CrudService) {
  }

  dataStateChange(state: DataStateChangeEventArgs): void {
    const query = (this.grid as GridComponent).getDataModule().generateQuery();
    this.crudService.execute(state, query);
  }

  dataSourceChanged(state: DataSourceChangedEventArgs): void {
    console.log('DataSourceChanged event triggered with state:', state);
    switch (state.action || state.requestType) {
      case 'add':
        this.crudService.addRecord(state).subscribe({
          next: () => {
            (state as GridComponent).endEdit();
          }
        });
        break;
      case 'edit':
        this.crudService.updateRecord(state).subscribe({
          next: () => {
            (state as GridComponent).endEdit();
          }
        });
        break;
      case 'delete':
        this.crudService.deleteRecord(state).subscribe({
          next: () => {
            (state as GridComponent).endEdit();
          }
        })
        break;
    }
  }

  public ngOnInit(): void {
    this.groupOptions = { columns: ['ProductName'], enableLazyLoading: true, showGroupedColumn: true, };
    this.filterOptions = { columns: [{ field: 'CustomerName', matchCase: false, operator: 'startswith', predicate: 'and', value: 'Maria' }] }
    this.sortOptions = { columns: [{ field: 'ProductID', direction: 'Descending' }] }
    const state = { skip: 0, take: 12, group: this.groupOptions, filter:this.filterOptions, sorted:this.sortOptions };
    this.toolbar = ['Add', 'Edit', 'Delete', 'Update', 'Cancel', 'Search'];
    this.editSettings = {
      allowEditing: true,
      allowAdding: true,
      allowDeleting: true,
    };
    this.orderIDRules = { required: true };
    const query = new Query();
    this.data = this.crudService.state$;
    this.crudService.execute(state, query);
  }
}
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders} from '@angular/common/http';
import { DataManager, Query } from '@syncfusion/ej2-data';
import { Customer } from './customers';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs';
import { DataStateChangeEventArgs, DataSourceChangedEventArgs } from '@syncfusion/ej2-grids';

interface sortInfo {
  name: string
  field: string,
  direction: string
}

interface filterInfo {
  fn?: string;
  e?: string;
  field: string;
  matchCase: boolean;
  operator: string;
  predicates: any;
  value: string;
}

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

@Injectable({
  providedIn: 'root'
})
export class CrudService {
  private customersUrl = 'api/customers';  // URL to web api
  constructor(private http: HttpClient) {  }

  // Observable to push data state updates
  private stateSubject = new BehaviorSubject<DataStateChangeEventArgs | null>(null);
  public state$ = this.stateSubject.asObservable();

  // Execute method to trigger data load with current state and query
  public execute(state: any, query: Query): void {
    this.getAllData(state, query).subscribe(data => this.stateSubject.next(data));
  }

  private applyFiltering(query: Query, filter: filterInfo[]): void {
    for (let i = 0; i < filter.length; i++) {
      const { fn, e } = filter[i];
      if (fn === 'onWhere') {
        query.where(e as string);
      }
    }
  }

  private applySorting(query: Query, sorted: sortInfo[]): void {
    if (sorted && sorted.length > 0) {
      sorted.forEach(sort => {
        query.sortBy(sort.name as string, sort.direction);
      });
    }
  }

  private applyGrouping(query: Query, group: any): void {
    if (group && group.columns && group.columns.length > 0) {
      group.columns.forEach((column: string) => {
        query.group(column);
      });
    } else if (group && group.length > 0) {
      group.forEach((column: string) => {
        query.group(column);
      });
    }
  }

  private applyLazyLoad = (query: Query, state: any) => {
    if (state.isLazyLoad) {
      query.lazyLoad.push({ key: 'isLazyLoad', value: true });
      if (state.onDemandGroupInfo) {
        console.log(state.onDemandGroupInfo)
        query.lazyLoad.push({
          key: 'onDemandGroupInfo',
          value: state.action.lazyLoadQuery,
        });
      }
    }
  }

  private applySearching(query: Query, search: Array<any>): void {
    if (search && search.length > 0) {
      const { fields, key } = search[0];
      query.search(key, fields);
    }
  }

  private applypaging(query: Query, state: any) {
    if (state.take && state.skip) {
      const pageSkip = state.skip / state.take + 1;
      const pageTake = state.take;
      query.page(pageSkip, pageTake);
    }
    else if (state.skip === 0 && state.take) {
      query.page(1, state.take);
    }
  }

  getAllData(state: any, action: any): Observable<any> {
    const query = new Query();
    switch (state) {
      case state: {
        // filtering
        if (state.where) {
          this.applyFiltering(query, action.queries);
        }
        // soritng
        if (state.sorted && state.sorted.length > 0) {
          this.applySorting(query, state.sorted);
        }
        // grouping
        if (state.group) {
          if (state.group.length > 0) {
            this.applyGrouping(query, state.group);
          }
          // initial grouping
          else if (state.group.columns.length > 0) {
            this.applyGrouping(query, state.group);
          }
        }
        // lazy load grouping
        if (state.group) {
          if (state.isLazyLoad) {
            this.applyLazyLoad(query, state)
          }
          if (state.group.enableLazyLoading) {
            query.lazyLoad.push({ key: 'isLazyLoad', value: true })
          }
        }
        // search
        if (state.search) {
          this.applySearching(query, state.search);
        };
        // paging
        this.applypaging(query, state)
        query.isCountRequired = true
      }
    }
    return this.http.get<Customer[]>(this.customersUrl).pipe(
      map((response: any[]) => {
        const currentResult: any = new DataManager(response).executeLocal(query);
        return {
          result: currentResult.result,
          count: currentResult.count
        };
      })
    );
  }
  /** POST: add a new record  to the server */
  addRecord(state: DataSourceChangedEventArgs): Observable<Customer> {
    return this.http.post<Customer>(this.customersUrl, state.data, httpOptions);
  }

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

  /** PUT: update the record on the server */
  updateRecord(state: DataSourceChangedEventArgs): Observable<Customer> {
    return this.http.put(this.customersUrl, state.data, httpOptions);
  }
}
import { InMemoryDbService } from 'angular-in-memory-web-api';

export class DataService implements InMemoryDbService {
createDb() {
    const customers = createLazyLoadData();
    return { customers };
    }
}
function createLazyLoadData(): Object[] {
    let lazyLoadData: Object[] = [];
    let customerid: string[] = ['VINET', 'TOMSP', 'HANAR', 'VICTE', 'SUPRD', 'HANAR', 'CHOPS', 'RICSU', 'WELLI', 'HILAA', 'ERNSH', 'CENTC',
    'OTTIK', 'QUEDE', 'RATTC', 'ERNSH', 'FOLKO', 'BLONP', 'WARTH', 'FRANK', 'GROSR', 'WHITC', 'WARTH', 'SPLIR', 'RATTC', 'QUICK', 'VINET',
    'MAGAA', 'TORTU', 'MORGK', 'BERGS', 'LEHMS', 'BERGS', 'ROMEY', 'ROMEY', 'LILAS', 'LEHMS', 'QUICK', 'QUICK', 'RICAR', 'REGGC', 'BSBEV',
    'COMMI', 'QUEDE', 'TRADH', 'TORTU', 'RATTC', 'VINET', 'LILAS', 'BLONP', 'HUNGO', 'RICAR', 'MAGAA', 'WANDK', 'SUPRD', 'GODOS', 'TORTU',
    'OLDWO', 'ROMEY', 'LONEP', 'ANATR', 'HUNGO', 'THEBI', 'DUMON', 'WANDK', 'QUICK', 'RATTC', 'ISLAT', 'RATTC', 'LONEP', 'ISLAT', 'TORTU',
    'WARTH', 'ISLAT', 'PERIC', 'KOENE', 'SAVEA', 'KOENE', 'BOLID', 'FOLKO', 'FURIB', 'SPLIR', 'LILAS', 'BONAP', 'MEREP', 'WARTH', 'VICTE',
    'HUNGO', 'PRINI', 'FRANK', 'OLDWO', 'MEREP', 'BONAP', 'SIMOB', 'FRANK', 'LEHMS', 'WHITC', 'QUICK', 'RATTC', 'FAMIA'];

    let product: string[] = ['Chai', 'Chang', 'Aniseed Syrup', 'Chef Anton\'s Cajun Seasoning', 'Chef Anton\'s Gumbo Mix', 'Grandma\'s Boysenberry Spread',
    'Uncle Bob\'s Organic Dried Pears', 'Northwoods Cranberry Sauce', 'Mishi Kobe Niku', 'Ikura', 'Queso Cabrales', 'Queso Manchego La Pastora', 'Konbu',
    'Tofu', 'Genen Shouyu', 'Pavlova', 'Alice Mutton', 'Carnarvon Tigers', 'Teatime Chocolate Biscuits', 'Sir Rodney\'s Marmalade', 'Sir Rodney\'s Scones',
    'Gustaf\'s Knäckebröd', 'Tunnbröd', 'Guaraná Fantástica', 'NuNuCa Nuß-Nougat-Creme', 'Gumbär Gummibärchen', 'Schoggi Schokolade', 'Rössle Sauerkraut',
    'Thüringer Rostbratwurst', 'Nord-Ost Matjeshering', 'Gorgonzola Telino', 'Mascarpone Fabioli', 'Geitost', 'Sasquatch Ale', 'Steeleye Stout', 'Inlagd Sill',
    'Gravad lax', 'Côte de Blaye', 'Chartreuse verte', 'Boston Crab Meat', 'Jack\'s New England Clam Chowder', 'Singaporean Hokkien Fried Mee', 'Ipoh Coffee',
    'Gula Malacca', 'Rogede sild', 'Spegesild', 'Zaanse koeken', 'Chocolade', 'Maxilaku', 'Valkoinen suklaa', 'Manjimup Dried Apples', 'Filo Mix', 'Perth Pasties',
    'Tourtière', 'Pâté chinois', 'Gnocchi di nonna Alice', 'Ravioli Angelo', 'Escargots de Bourgogne', 'Raclette Courdavault', 'Camembert Pierrot', 'Sirop d\'érable',
    'Tarte au sucre', 'Vegie-spread', 'Wimmers gute Semmelknödel', 'Louisiana Fiery Hot Pepper Sauce', 'Louisiana Hot Spiced Okra', 'Laughing Lumberjack Lager', 'Scottish Longbreads',
    'Gudbrandsdalsost', 'Outback Lager', 'Flotemysost', 'Mozzarella di Giovanni', 'Röd Kaviar', 'Longlife Tofu', 'Rhönbräu Klosterbier', 'Lakkalikööri', 'Original Frankfurter grüne Soße'];

    let customername: string[] = ['Maria', 'Ana Trujillo', 'Antonio Moreno', 'Thomas Hardy', 'Christina Berglund', 'Hanna Moos', 'Frédérique Citeaux', 'Martín Sommer', 'Laurence Lebihan', 'Elizabeth Lincoln',
    'Victoria Ashworth', 'Patricio Simpson', 'Francisco Chang', 'Yang Wang', 'Pedro Afonso', 'Elizabeth Brown', 'Sven Ottlieb', 'Janine Labrune', 'Ann Devon', 'Roland Mendel', 'Aria Cruz', 'Diego Roel',
    'Martine Rancé', 'Maria Larsson', 'Peter Franken', 'Carine Schmitt', 'Paolo Accorti', 'Lino Rodriguez', 'Eduardo Saavedra', 'José Pedro Freyre', 'André Fonseca', 'Howard Snyder', 'Manuel Pereira',
    'Mario Pontes', 'Carlos Hernández', 'Yoshi Latimer', 'Patricia McKenna', 'Helen Bennett', 'Philip Cramer', 'Daniel Tonini', 'Annette Roulet', 'Yoshi Tannamuri', 'John Steel', 'Renate Messner', 'Jaime Yorres',
    'Carlos González', 'Felipe Izquierdo', 'Fran Wilson', 'Giovanni Rovelli', 'Catherine Dewey', 'Jean Fresnière', 'Alexander Feuer', 'Simon Crowther', 'Yvonne Moncada', 'Rene Phillips', 'Henriette Pfalzheim',
    'Marie Bertrand', 'Guillermo Fernández', 'Georg Pipps', 'Isabel de Castro', 'Bernardo Batista', 'Lúcia Carvalho', 'Horst Kloss', 'Sergio Gutiérrez', 'Paula Wilson', 'Maurizio Moroni', 'Janete Limeira', 'Michael Holz',
    'Alejandra Camino', 'Jonas Bergulfsen', 'Jose Pavarotti', 'Hari Kumar', 'Jytte Petersen', 'Dominique Perrier', 'Art Braunschweiger', 'Pascale Cartrain', 'Liz Nixon', 'Liu Wong', 'Karin Josephs', 'Miguel Angel Paolino',
    'Anabela Domingues', 'Helvetius Nagy', 'Palle Ibsen', 'Mary Saveley', 'Paul Henriot', 'Rita Müller', 'Pirkko Koskitalo', 'Paula Parente', 'Karl Jablonski', 'Matti Karttunen', 'Zbyszek Piestrzeniewicz'];

    let customeraddress: string[] = ['507 - 20th Ave. E.\r\nApt. 2A', '908 W. Capital Way', '722 Moss Bay Blvd.', '4110 Old Redmond Rd.', '14 Garrett Hill', 'Coventry House\r\nMiner Rd.', 'Edgeham Hollow\r\nWinchester Way',
    '4726 - 11th Ave. N.E.', '7 Houndstooth Rd.', '59 rue de l\'Abbaye', 'Luisenstr. 48', '908 W. Capital Way', '722 Moss Bay Blvd.', '4110 Old Redmond Rd.', '14 Garrett Hill', 'Coventry House\r\nMiner Rd.', 'Edgeham Hollow\r\nWinchester Way',
    '7 Houndstooth Rd.', '2817 Milton Dr.', 'Kirchgasse 6', 'Sierras de Granada 9993', 'Mehrheimerstr. 369', 'Rua da Panificadora, 12', '2817 Milton Dr.', 'Mehrheimerstr. 369'];

    let quantityperunit: string[] = ['10 boxes x 20 bags', '24 - 12 oz bottles', '12 - 550 ml bottles', '48 - 6 oz jars', '36 boxes', '12 - 8 oz jars', '12 - 1 lb pkgs.', '12 - 12 oz jars', '18 - 500 g pkgs.', '12 - 200 ml jars',
    '1 kg pkg.', '10 - 500 g pkgs.', '2 kg box', '40 - 100 g pkgs.', '24 - 250 ml bottles', '32 - 500 g boxes', '20 - 1 kg tins', '16 kg pkg.', '10 boxes x 12 pieces', '30 gift boxes', '24 pkgs. x 4 pieces', '24 - 500 g pkgs.', '12 - 250 g pkgs.',
    '12 - 355 ml cans', '20 - 450 g glasses', '100 - 250 g bags'];

    let id: number = 10248;
    for (let i: number = 0; i < 20000; i++) {
        lazyLoadData.push({
            'id': id + i,
            'CustomerID': customerid[Math.floor(Math.random() * customerid.length)],
            'CustomerName': customername[Math.floor(Math.random() * customername.length)],
            'CustomerAddress': customeraddress[Math.floor(Math.random() * customeraddress.length)],
            'ProductName': product[Math.floor(Math.random() * product.length)],
            'ProductID': i,
            'Quantity': quantityperunit[Math.floor(Math.random() * quantityperunit.length)]
        })
    }
    return lazyLoadData;
}
export class Customer {
    id?: number;
    CustomerName?: string;
    ProductID?:number
    ProductName?:string
}
import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { DataService } from './data';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
  providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideHttpClient(),
    importProvidersFrom(HttpClientInMemoryWebApiModule.forRoot(DataService))
  ]
};

  • When working with grid edit operations, defining the isPrimaryKey property of a column is mandatory. If the primary key column is not defined, the edit or delete action will operate on the first row of the grid.
  • Maintain the same observable instance for all grid actions.
  • You can refer to the guidelines for CRUD using observables here

Export all records in client side

Exporting all records with async pipe proves especially beneficial when dealing with large datasets that require export for offline analysis or sharing purposes.

By default, when utilizing observables for Grid data binding, the export operation exports only records on the current page. However, the Syncfusion Angular Grid component allows you to export all records, including those from multiple pages, by configuring the pdfExportProperties and excelExportProperties in conjunction with the Async Pipe for data binding.

To export all records, including those from multiple pages, configure the pdfExportProperties.dataSource for PDF exporting and excelExportProperties.dataSource for Excel exporting within the toolbarClick event handler. Inside this event, set the dataSource property of pdfExportProperties and excelExportProperties for PDF and Excel exporting to include all records.

Excel Exporting

To export the complete grid data to Excel, utilize the excelExportProperties.dataSource when initiating the Excel export. Use the following code snippet to export all records within the grid:

this.service.getData(state).subscribe((e: any) => {
  let excelExportProperties: ExcelExportProperties = {
    dataSource: e.result ? e.result : result
  };
  (this.grid as GridComponent).excelExport(excelExportProperties); // need to call excelExport method of grid when get the entire data
});

PDF Exporting

To export the complete grid data to PDF document, utilize the pdfExportProperties.dataSource when initiating the PDF export. Use the following code snippet to export all records within the grid:

this.service.getData(state).subscribe((e: any) => {
  let pdfExportProperties: PdfExportProperties = {
    dataSource: e.result ? e.result : result
  };
  (this.grid as GridComponent).pdfExport(pdfExportProperties); // need to call pdfExport method of grid when get the entire data
});

Further customization on grid export can be accessed in the respective documentation for PDF exporting and Excel exporting

The following code example shows how to export all records in client side for observable using the async pipe:

import { Component, OnInit, ViewChild } from '@angular/core';
import { GridComponent } from '@syncfusion/ej2-angular-grids';
import {DataStateChangeEventArgs, PdfExportProperties, ExcelExportProperties} from '@syncfusion/ej2-angular-grids';
import { ClickEventArgs } from "@syncfusion/ej2-navigations";
import { DataService } from './order.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `<ejs-grid #grid [dataSource]='data | async' (excelExportComplete)="exportComplete()" (pdfExportComplete)="exportComplete()" [allowExcelExport]='true' [allowPdfExport]='true' allowPaging='true' [pageSettings]='pageOptions' [toolbar]="toolbar" (toolbarClick)='toolbarClick($event)' (dataStateChange)= 'dataStateChange($event)'>
                <e-columns>
                  <e-column field='OrderID' headerText='Order ID' width='90' textAlign='Right' isPrimaryKey='true'></e-column>
                  <e-column field="CustomerID" headerText="Customer Name" width="100"></e-column>
                  <e-column field='ShipName' headerText="Ship Name" width=110></e-column>
                  <e-column field='ShipCountry' headerText='Ship Country' width=100></e-column>
                  <e-column field='Freight' headerText='Freight' format='C2' textAlign='Right' width=100></e-column>
                </e-columns>
              </ejs-grid>`,
  providers: [DataService],
})
export class AppComponent implements OnInit {
  public data?: Observable<DataStateChangeEventArgs>;
  public state?: DataStateChangeEventArgs;
  public pageOptions?: object;
  public toolbar?: string[];
  @ViewChild('grid')
  public grid?: GridComponent;

  constructor(public service: DataService) {
    this.data = service;
  }

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

  exportComplete() {
    (this.grid as GridComponent).hideSpinner(); // hide the spinner when export completed
  }

  toolbarClick(args: ClickEventArgs): void {
    let state: any = { action: {}, skip: 0, take: (this.grid as GridComponent).pageSettings.totalRecordsCount };
    let result = {};
    switch (args.item.text) {
      case "PDF Export":
        (this.grid as GridComponent).showSpinner(); // show the spinner when send the post to service
        state.action.isPdfExport = true;
        // fetch the entire data while PDF exporting
        this.service.getData(state).subscribe((e: any) => {
          let pdfExportProperties: PdfExportProperties = {
            dataSource: e.result ? e.result : result
          };
          (this.grid as GridComponent).pdfExport(pdfExportProperties); // need to call pdfExport method of grid when get the entire data
        });
        break;
      case "Excel Export":
        // fetch the entire data while Excel exporting
        (this.grid as GridComponent).showSpinner(); // show the spinner when send the post to service
        state.action.isExcelExport = true;
        this.service.getData(state).subscribe((e: any) => {
          let excelExportProperties: ExcelExportProperties = {
            dataSource: e.result ? e.result : result
          };
          (this.grid as GridComponent).excelExport(excelExportProperties); // need to call excelExport method of grid when get the entire data
        });
        break;
    }
  }
  public ngOnInit(): void {
    this.pageOptions = { pageSize: 10, pageCount: 4 };
    const state = { skip: 0, take: 10 };
    this.toolbar = ["ExcelExport", "PdfExport"];
    this.service.execute(state);
  }
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {DataStateChangeEventArgs,DataResult} from '@syncfusion/ej2-angular-grids';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs';
import { map } from 'rxjs';

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

  private BASE_URL = 'https://services.odata.org/V4/Northwind/Northwind.svc/Orders';

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

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

  public getData(state: DataStateChangeEventArgs): Observable<DataStateChangeEventArgs> {
    const pageQuery = `$skip=${state.skip}&$top=${state.take}`;
    
    return this.http
      .get(`${this.BASE_URL}?${pageQuery}&$count=true`)
      .pipe(map((response: any) => response))
      .pipe(map((response: any) => (<DataResult>{
        result: response['value'],
        count: parseInt(response['@odata.count'], 10),
      })))
    .pipe((data: any) => data);
  }
}

Binding observable data without using 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 Grid, 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.

Handling searching operation

When searching operations are performed in the grid, the dataStateChange event triggers, providing access to the following referenced arguments:

Searching

You can modify the Observable based on the new grid data state for search actions:

private applySearching(query: Query, search: Array<any>): void {
  // Check if a search operation is requested
  if (search && search.length > 0) {
    // Extract the search key and fields from the search array
    const { fields, key } = search[0];
    // perform search operation using the field and key on the query
    query.search(key, fields);
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // search
  if (state.search) {
    this.applySearching(query, state.search);
  }
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Handling filtering operation

When filtering operations are performed in the grid, the dataStateChange event triggers, providing access to the following referenced arguments:

FilterBar

You can modify the Observable based on the new grid data state for filter actions:

private applyFiltering(query: Query, filter: any): void {
  // Check if filter columns are specified
  if (filter.columns && filter.columns.length) {
    // Apply filtering for each specified column
    for (let i = 0; i < filter.columns.length; i++) {
      const field = filter.columns[i].field;
      const operator = filter.columns[i].operator;
      const value = filter.columns[i].value;
      query.where(field, operator, value);
    }
  } else {
    // Apply filtering based on direct filter conditions
    for (let i = 0; i < filter.length; i++) {
      const { fn, e } = filter[i];
      if (fn === 'onWhere') {
        query.where(e as string);
      }
    }
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // filtering
  if (state.where) {
    this.applyFiltering(query, action.queries);
  }
  // initial filtering
  if (state.filter && state.filter.columns && state.filter.columns.length) {
    this.applyFiltering(query, state.filter);
  }
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Filtering Multiple Values

When filtering multiple values, you can retrieve predicates as arguments in the dataStateChange event. Create your predicate execution based on the predicates values.

Handling sorting operation

When sorting operations are performed in the grid, the dataStateChange event triggers, and within this event, you can access the following referenced arguments:

Sorting

When performing multi-column sorting, you can access the following referenced arguments in the dataStateChange event:

Multi Sorting

You can modify the Observable based on the new grid data state for sort actions:

private applySorting(query: Query, sorted: sortInfo[]): void {
  // Check if sorting data is available
  if (sorted && sorted.length > 0) {
    // Iterate through each sorting info
    sorted.forEach(sort => {
      // Get the sort field name either by name or field
      const sortField = sort.name || sort.field;
      // Perform sort operation using the query based on the field name and direction
      query.sortBy(sortField as string, sort.direction);
    });
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // sorting
  if (state.sorted) {
    state.sorted.length ? this.applySorting(query, state.sorted) :
      // initial sorting
      state.sorted.columns.length ? this.applySorting(query, state.sorted.columns) : null;
  } 
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Handling paging operation

When paging operations are performed in the grid, the dataStateChange event triggers, and within this event, you can access the following referenced arguments:

Paging

You can modify the Observable based on the new grid data state for page actions:

private applyPaging(query: Query, state: any): void {
  // 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 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);
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // paging
  this.applyPaging(query, state);
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Handling grouping operation

When grouping operations are performed in the grid, the dataStateChange event triggers, providing access to the following referenced arguments:

Grouping

You can modify the Observable based on the new grid data state for group actions:

private applyGrouping(query: Query, group: any): void {
  // Check if grouping data is available
  if (group.length > 0) {
    // Iterate through each group info
    group.forEach((column: string) => {
      // perform group operation using the column on the query
      query.group(column);
    });
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // grouping
  if (state.group) {
    state.group.length ? this.applyGrouping(query, state.group) :
      // initial grouping
      state.group.columns.length ? this.applyGrouping(query, state.group.columns) : null;
  }
  // To get the count of the data
  query.isCountRequired = true;
  
  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

To utilize group actions effectively, you must manage the sorting query appropriately.

Lazy load grouping

In Angular applications, lazy loading refers to the technique of loading data dynamically when needed, rather than loading everything upfront. Lazy load grouping enables efficient loading and display of grouped data by fetching only the required data on demand.

To enable this feature, set the groupSettings.enableLazyLoading property to true. You must also manage the state based on the initial grid action:

public ngOnInit(): void {
  this.groupOptions = { columns: ['ProductName'], enableLazyLoading: true };
  const state = { skip: 0, take: 12, group: this.groupOptions };
  this.crudService.execute(state, query);
}

Based on the initial state, you can access the arguments as shown below:

Lazy load group

You can modify the Observable based on the grid state:

private applyGrouping(query: Query, group: any): void {
  // Check if grouping data is available
  if (group.length > 0) {
    // Iterate through each group info
    group.forEach((column: string) => {
      // perform group operation using the column on the query
      query.group(column);
    });
  }
}

private applyLazyLoad = (query: Query, state: any): void => {
  if (state.isLazyLoad) {
    // Configure lazy loading for the main data
    query.lazyLoad.push({ key: 'isLazyLoad', value: true });
    // If on-demand group loading is enabled, configure lazy loading for grouped data
    if (state.onDemandGroupInfo) {
      query.lazyLoad.push({
        key: 'onDemandGroupInfo',
        value: state.action.lazyLoadQuery,
      });
    }
  }
}

/** GET all data from the server */
getAllData(state: any, action: any): Observable<any> {
  const query = new Query();
  // grouping
  if (state.group) {
    state.group.length ? this.applyGrouping(query, state.group) :
      // initial grouping
      state.group.columns.length ? this.applyGrouping(query, state.group.columns) : null;
  }
  // lazy load grouping
  this.applyLazyLoad(query, state);
  // initial grouping with lazy load
  if (state.group && state.group.enableLazyLoading) {
    query.lazyLoad.push({ key: 'isLazyLoad', value: true });
  }
  // To get the count of the data
  query.isCountRequired = true;

  return this.http.get<Customer[]>(this.customersUrl).pipe(
    map((response: any[]) => {
      // Execute local data operations using the provided query
      const currentResult: any = new DataManager(response).executeLocal(query);
      // Return the result along with the count of total records
      return {
        result: currentResult.result, // Result of the data
        count: currentResult.count // Total record count
      };
    })
  );
}

Further information can be accessed in the respective documentation for lazy load grouping.

The complete example is available in the Handling CRUD operations topic.

Handling CRUD operations

The Grid component provides powerful options for dynamically inserting, deleting, and updating records, enabling direct data modification within the grid interface. This functionality proves essential when performing CRUD (Create, Read, Update, Delete) operations seamlessly.

Integrating CRUD Operations

To implement CRUD operations using Syncfusion Grid, follow these steps:

  1. Configure grid settings: Set up the necessary grid settings for editing, adding, and deleting records. Define the toolbar options to facilitate user interactions.

  2. Handle data state changes: Utilize the dataStateChange event to respond to changes in the grid’s data state. This event triggers whenever you interact with the grid through paging or sorting.

  3. Execute CRUD operations: Within the event handler for dataSourceChanged, implement logic to handle various CRUD actions based on the action or requestType property of the event arguments.

  4. Call endEdit method: After performing CRUD operations (adding, editing, or deleting), call the endEdit method to signal operation completion and update the grid accordingly.

Insert operation

When an insert operation is performed in the grid, the dataSourceChanged event triggers, providing access to the following referenced arguments:

Adding

/** POST: add a new record to the server */
addRecord(state: DataSourceChangedEventArgs): Observable<Customer> {
  return this.http.post<Customer>(this.customersUrl, state.data, httpOptions);
}

Edit operation

When an edit operation is performed in the grid, the dataSourceChanged event triggers, providing access to the following referenced arguments:

Editing

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

Delete operation

When a delete operation is performed in the grid, the dataSourceChanged event triggers, providing access to the following referenced arguments:

Deleting

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

The following example demonstrates how to bind observable data without using async pipe to handle grid actions and CRUD operations:

import { Component, OnInit, ViewChild } from '@angular/core';
import { GridAllModule, GridComponent } from '@syncfusion/ej2-angular-grids';
import { DataStateChangeEventArgs, DataSourceChangedEventArgs, SortService, FilterService, CommandColumnService, SearchService, ColumnChooser, isComplexField } from '@syncfusion/ej2-angular-grids';
import { EditService, ToolbarService, PageService } from '@syncfusion/ej2-angular-grids';
import { CrudService } from './crud.service';
import { Subscription } from 'rxjs';
import { Query } from '@syncfusion/ej2-data';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-root',
  template: `
  <div style="padding-top:100px">
    <ejs-grid #grid [dataSource]='data' allowPaging="true" allowGrouping="true" [groupSettings]="groupOptions" 
    allowSorting="true" [sortSettings]="sortOptions" allowFiltering="true" [filterSettings]="filterOptions" [editSettings]='editSettings' [toolbar]='toolbar'
    (dataSourceChanged)='dataSourceChanged($event)' (dataStateChange)= 'dataStateChange($event)' height='315px'>
      <e-columns>
        <e-column field='id' headerText='Order ID' width='90' textAlign='Right' [validationRules]='orderIDRules' isPrimaryKey='true'></e-column>
        <e-column field="CustomerName" headerText="Customer Name" width="100"></e-column>
        <e-column field='ProductID' headerText='Product ID' width=100></e-column>
        <e-column field='ProductName' headerText='Product Name' width='160'></e-column>
      </e-columns>
    </ejs-grid>
  </div>`,
  standalone: true,
  imports: [
    CommonModule,
    GridAllModule,
  ],
  providers: [ToolbarService, SortService, EditService, FilterService, PageService, CommandColumnService, SearchService, CrudService]
})
export class AppComponent implements OnInit {
  public data?: object;
  public state?: DataStateChangeEventArgs;
  public toolbar?: string[];
  public editSettings?: Object;
  public orderIDRules?: object;
  @ViewChild('grid')
  public grid?: GridComponent;
  public groupOptions?: object;  
  public filterOptions?: object;
  public sortOptions?: object;
  private stateSubscription: Subscription | undefined;
  constructor(public crudService: CrudService) {
  }

  dataStateChange(state: DataStateChangeEventArgs): void {
    const query = (this.grid as GridComponent).getDataModule().generateQuery();
    this.crudService.execute(state, query);
  }

  dataSourceChanged(state: DataSourceChangedEventArgs): void {
    switch (state.action || state.requestType) {
      case 'add':
        this.crudService.addRecord(state).subscribe({
          next: () => {
            (state as GridComponent).endEdit();
          }
        });
        break;
      case 'edit':
        this.crudService.updateRecord(state).subscribe({
          next: () => {
            (state as GridComponent).endEdit();
          }
        });
        break;
      case 'delete':
        this.crudService.deleteRecord(state).subscribe({
          next: () => {
            (state as GridComponent).endEdit();
          }
        })
        break;
    }
  }

  public ngOnInit(): void {
    this.groupOptions = { columns: ['ProductName'], enableLazyLoading: true, showGroupedColumn: true, };
    this.filterOptions = { columns: [{ field: 'CustomerName', matchCase: false, operator: 'startswith', predicate: 'and', value: 'Maria' }] }
    this.sortOptions = { columns: [{ field: 'ProductID', direction: 'Descending' }] }
    const state = { skip: 0, take: 12, group: this.groupOptions, filter:this.filterOptions, sorted:this.sortOptions };
    this.toolbar = ['Add', 'Edit', 'Delete', 'Update', 'Cancel', 'Search'];
    this.editSettings = {
      allowEditing: true,
      allowAdding: true,
      allowDeleting: true,
    };
    this.orderIDRules = { required: true };
    const query = new Query();
    // Subscribe to state updates manually
    this.stateSubscription = this.crudService.state$.subscribe((data) => {
      if (data) {
        this.data = data; // Update the grid's data source
      }
    });
    this.crudService.execute(state, query);
  }
  ngOnDestroy(): void {
    // Ensure to unsubscribe to avoid memory leaks
    if (this.stateSubscription) {
      this.stateSubscription.unsubscribe();
    }
  }
}
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders} from '@angular/common/http';
import { DataManager, Query } from '@syncfusion/ej2-data';
import { Customer } from './customers';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs';
import { DataStateChangeEventArgs, DataSourceChangedEventArgs } from '@syncfusion/ej2-grids';

interface sortInfo {
  name: string
  field: string,
  direction: string
}

interface filterInfo {
  fn?: string;
  e?: string;
  field: string;
  matchCase: boolean;
  operator: string;
  predicates: any;
  value: string;
}

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

@Injectable({
  providedIn: 'root'
})
export class CrudService {
  private customersUrl = 'api/customers';  // URL to web api
  constructor(private http: HttpClient) {  }

  // Observable to push data state updates
  private stateSubject = new BehaviorSubject<DataStateChangeEventArgs | null>(null);
  public state$ = this.stateSubject.asObservable();

  // Execute method to trigger data load with current state and query
  public execute(state: any, query: Query): void {
    this.getAllData(state, query).subscribe(data => this.stateSubject.next(data));
  }

  private applyFiltering(query: Query, filter: filterInfo[]): void {
    for (let i = 0; i < filter.length; i++) {
      const { fn, e } = filter[i];
      if (fn === 'onWhere') {
        query.where(e as string);
      }
    }
  }

  private applySorting(query: Query, sorted: sortInfo[]): void {
    if (sorted && sorted.length > 0) {
      sorted.forEach(sort => {
        query.sortBy(sort.name as string, sort.direction);
      });
    }
  }

  private applyGrouping(query: Query, group: any): void {
    if (group && group.columns && group.columns.length > 0) {
      group.columns.forEach((column: string) => {
        query.group(column);
      });
    } else if (group && group.length > 0) {
      group.forEach((column: string) => {
        query.group(column);
      });
    }
  }

  private applyLazyLoad = (query: Query, state: any) => {
    if (state.isLazyLoad) {
      query.lazyLoad.push({ key: 'isLazyLoad', value: true });
      if (state.onDemandGroupInfo) {
        console.log(state.onDemandGroupInfo)
        query.lazyLoad.push({
          key: 'onDemandGroupInfo',
          value: state.action.lazyLoadQuery,
        });
      }
    }
  }

  private applySearching(query: Query, search: Array<any>): void {
    if (search && search.length > 0) {
      const { fields, key } = search[0];
      query.search(key, fields);
    }
  }

  private applypaging(query: Query, state: any) {
    if (state.take && state.skip) {
      const pageSkip = state.skip / state.take + 1;
      const pageTake = state.take;
      query.page(pageSkip, pageTake);
    }
    else if (state.skip === 0 && state.take) {
      query.page(1, state.take);
    }
  }

  getAllData(state: any, action: any): Observable<any> {
    const query = new Query();
    switch (state) {
      case state: {
        // filtering
        if (state.where) {
          this.applyFiltering(query, action.queries);
        }
        // soritng
        if (state.sorted && state.sorted.length > 0) {
          this.applySorting(query, state.sorted);
        }
        // grouping
        if (state.group) {
          if (state.group.length > 0) {
            this.applyGrouping(query, state.group);
          }
          // initial grouping
          else if (state.group.columns.length > 0) {
            this.applyGrouping(query, state.group);
          }
        }
        // lazy load grouping
        if (state.group) {
          if (state.isLazyLoad) {
            this.applyLazyLoad(query, state)
          }
          if (state.group.enableLazyLoading) {
            query.lazyLoad.push({ key: 'isLazyLoad', value: true })
          }
        }
        // search
        if (state.search) {
          this.applySearching(query, state.search);
        };
        // paging
        this.applypaging(query, state)
        query.isCountRequired = true
      }
    }
    return this.http.get<Customer[]>(this.customersUrl).pipe(
      map((response: any[]) => {
        const currentResult: any = new DataManager(response).executeLocal(query);
        return {
          result: currentResult.result,
          count: currentResult.count
        };
      })
    );
  }
  /** POST: add a new record  to the server */
  addRecord(state: DataSourceChangedEventArgs): Observable<Customer> {
    return this.http.post<Customer>(this.customersUrl, state.data, httpOptions);
  }

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

  /** PUT: update the record on the server */
  updateRecord(state: DataSourceChangedEventArgs): Observable<Customer> {
    return this.http.put(this.customersUrl, state.data, httpOptions);
  }
}
import { InMemoryDbService } from 'angular-in-memory-web-api';

export class DataService implements InMemoryDbService {
createDb() {
    const customers = createLazyLoadData();
    return { customers };
    }
}
function createLazyLoadData(): Object[] {
    let lazyLoadData: Object[] = [];
    let customerid: string[] = ['VINET', 'TOMSP', 'HANAR', 'VICTE', 'SUPRD', 'HANAR', 'CHOPS', 'RICSU', 'WELLI', 'HILAA', 'ERNSH', 'CENTC',
    'OTTIK', 'QUEDE', 'RATTC', 'ERNSH', 'FOLKO', 'BLONP', 'WARTH', 'FRANK', 'GROSR', 'WHITC', 'WARTH', 'SPLIR', 'RATTC', 'QUICK', 'VINET',
    'MAGAA', 'TORTU', 'MORGK', 'BERGS', 'LEHMS', 'BERGS', 'ROMEY', 'ROMEY', 'LILAS', 'LEHMS', 'QUICK', 'QUICK', 'RICAR', 'REGGC', 'BSBEV',
    'COMMI', 'QUEDE', 'TRADH', 'TORTU', 'RATTC', 'VINET', 'LILAS', 'BLONP', 'HUNGO', 'RICAR', 'MAGAA', 'WANDK', 'SUPRD', 'GODOS', 'TORTU',
    'OLDWO', 'ROMEY', 'LONEP', 'ANATR', 'HUNGO', 'THEBI', 'DUMON', 'WANDK', 'QUICK', 'RATTC', 'ISLAT', 'RATTC', 'LONEP', 'ISLAT', 'TORTU',
    'WARTH', 'ISLAT', 'PERIC', 'KOENE', 'SAVEA', 'KOENE', 'BOLID', 'FOLKO', 'FURIB', 'SPLIR', 'LILAS', 'BONAP', 'MEREP', 'WARTH', 'VICTE',
    'HUNGO', 'PRINI', 'FRANK', 'OLDWO', 'MEREP', 'BONAP', 'SIMOB', 'FRANK', 'LEHMS', 'WHITC', 'QUICK', 'RATTC', 'FAMIA'];

    let product: string[] = ['Chai', 'Chang', 'Aniseed Syrup', 'Chef Anton\'s Cajun Seasoning', 'Chef Anton\'s Gumbo Mix', 'Grandma\'s Boysenberry Spread',
    'Uncle Bob\'s Organic Dried Pears', 'Northwoods Cranberry Sauce', 'Mishi Kobe Niku', 'Ikura', 'Queso Cabrales', 'Queso Manchego La Pastora', 'Konbu',
    'Tofu', 'Genen Shouyu', 'Pavlova', 'Alice Mutton', 'Carnarvon Tigers', 'Teatime Chocolate Biscuits', 'Sir Rodney\'s Marmalade', 'Sir Rodney\'s Scones',
    'Gustaf\'s Knäckebröd', 'Tunnbröd', 'Guaraná Fantástica', 'NuNuCa Nuß-Nougat-Creme', 'Gumbär Gummibärchen', 'Schoggi Schokolade', 'Rössle Sauerkraut',
    'Thüringer Rostbratwurst', 'Nord-Ost Matjeshering', 'Gorgonzola Telino', 'Mascarpone Fabioli', 'Geitost', 'Sasquatch Ale', 'Steeleye Stout', 'Inlagd Sill',
    'Gravad lax', 'Côte de Blaye', 'Chartreuse verte', 'Boston Crab Meat', 'Jack\'s New England Clam Chowder', 'Singaporean Hokkien Fried Mee', 'Ipoh Coffee',
    'Gula Malacca', 'Rogede sild', 'Spegesild', 'Zaanse koeken', 'Chocolade', 'Maxilaku', 'Valkoinen suklaa', 'Manjimup Dried Apples', 'Filo Mix', 'Perth Pasties',
    'Tourtière', 'Pâté chinois', 'Gnocchi di nonna Alice', 'Ravioli Angelo', 'Escargots de Bourgogne', 'Raclette Courdavault', 'Camembert Pierrot', 'Sirop d\'érable',
    'Tarte au sucre', 'Vegie-spread', 'Wimmers gute Semmelknödel', 'Louisiana Fiery Hot Pepper Sauce', 'Louisiana Hot Spiced Okra', 'Laughing Lumberjack Lager', 'Scottish Longbreads',
    'Gudbrandsdalsost', 'Outback Lager', 'Flotemysost', 'Mozzarella di Giovanni', 'Röd Kaviar', 'Longlife Tofu', 'Rhönbräu Klosterbier', 'Lakkalikööri', 'Original Frankfurter grüne Soße'];

    let customername: string[] = ['Maria', 'Ana Trujillo', 'Antonio Moreno', 'Thomas Hardy', 'Christina Berglund', 'Hanna Moos', 'Frédérique Citeaux', 'Martín Sommer', 'Laurence Lebihan', 'Elizabeth Lincoln',
    'Victoria Ashworth', 'Patricio Simpson', 'Francisco Chang', 'Yang Wang', 'Pedro Afonso', 'Elizabeth Brown', 'Sven Ottlieb', 'Janine Labrune', 'Ann Devon', 'Roland Mendel', 'Aria Cruz', 'Diego Roel',
    'Martine Rancé', 'Maria Larsson', 'Peter Franken', 'Carine Schmitt', 'Paolo Accorti', 'Lino Rodriguez', 'Eduardo Saavedra', 'José Pedro Freyre', 'André Fonseca', 'Howard Snyder', 'Manuel Pereira',
    'Mario Pontes', 'Carlos Hernández', 'Yoshi Latimer', 'Patricia McKenna', 'Helen Bennett', 'Philip Cramer', 'Daniel Tonini', 'Annette Roulet', 'Yoshi Tannamuri', 'John Steel', 'Renate Messner', 'Jaime Yorres',
    'Carlos González', 'Felipe Izquierdo', 'Fran Wilson', 'Giovanni Rovelli', 'Catherine Dewey', 'Jean Fresnière', 'Alexander Feuer', 'Simon Crowther', 'Yvonne Moncada', 'Rene Phillips', 'Henriette Pfalzheim',
    'Marie Bertrand', 'Guillermo Fernández', 'Georg Pipps', 'Isabel de Castro', 'Bernardo Batista', 'Lúcia Carvalho', 'Horst Kloss', 'Sergio Gutiérrez', 'Paula Wilson', 'Maurizio Moroni', 'Janete Limeira', 'Michael Holz',
    'Alejandra Camino', 'Jonas Bergulfsen', 'Jose Pavarotti', 'Hari Kumar', 'Jytte Petersen', 'Dominique Perrier', 'Art Braunschweiger', 'Pascale Cartrain', 'Liz Nixon', 'Liu Wong', 'Karin Josephs', 'Miguel Angel Paolino',
    'Anabela Domingues', 'Helvetius Nagy', 'Palle Ibsen', 'Mary Saveley', 'Paul Henriot', 'Rita Müller', 'Pirkko Koskitalo', 'Paula Parente', 'Karl Jablonski', 'Matti Karttunen', 'Zbyszek Piestrzeniewicz'];

    let customeraddress: string[] = ['507 - 20th Ave. E.\r\nApt. 2A', '908 W. Capital Way', '722 Moss Bay Blvd.', '4110 Old Redmond Rd.', '14 Garrett Hill', 'Coventry House\r\nMiner Rd.', 'Edgeham Hollow\r\nWinchester Way',
    '4726 - 11th Ave. N.E.', '7 Houndstooth Rd.', '59 rue de l\'Abbaye', 'Luisenstr. 48', '908 W. Capital Way', '722 Moss Bay Blvd.', '4110 Old Redmond Rd.', '14 Garrett Hill', 'Coventry House\r\nMiner Rd.', 'Edgeham Hollow\r\nWinchester Way',
    '7 Houndstooth Rd.', '2817 Milton Dr.', 'Kirchgasse 6', 'Sierras de Granada 9993', 'Mehrheimerstr. 369', 'Rua da Panificadora, 12', '2817 Milton Dr.', 'Mehrheimerstr. 369'];

    let quantityperunit: string[] = ['10 boxes x 20 bags', '24 - 12 oz bottles', '12 - 550 ml bottles', '48 - 6 oz jars', '36 boxes', '12 - 8 oz jars', '12 - 1 lb pkgs.', '12 - 12 oz jars', '18 - 500 g pkgs.', '12 - 200 ml jars',
    '1 kg pkg.', '10 - 500 g pkgs.', '2 kg box', '40 - 100 g pkgs.', '24 - 250 ml bottles', '32 - 500 g boxes', '20 - 1 kg tins', '16 kg pkg.', '10 boxes x 12 pieces', '30 gift boxes', '24 pkgs. x 4 pieces', '24 - 500 g pkgs.', '12 - 250 g pkgs.',
    '12 - 355 ml cans', '20 - 450 g glasses', '100 - 250 g bags'];

    let id: number = 10248;
    for (let i: number = 0; i < 20000; i++) {
        lazyLoadData.push({
            'id': id + i,
            'CustomerID': customerid[Math.floor(Math.random() * customerid.length)],
            'CustomerName': customername[Math.floor(Math.random() * customername.length)],
            'CustomerAddress': customeraddress[Math.floor(Math.random() * customeraddress.length)],
            'ProductName': product[Math.floor(Math.random() * product.length)],
            'ProductID': i,
            'Quantity': quantityperunit[Math.floor(Math.random() * quantityperunit.length)]
        })
    }
    return lazyLoadData;
}
export class Customer {
    id?: number;
    CustomerName?: string;
    ProductID?:number
    ProductName?:string
}
import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { DataService } from './data';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
  providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideHttpClient(),
    importProvidersFrom(HttpClientInMemoryWebApiModule.forRoot(DataService))
  ]
};

Improper handling of observables and subscriptions may lead to memory leaks and unexpected behavior. Ensure proper subscription management, especially when dealing with long-lived observables.

Export all records in client side

Exporting all records proves especially beneficial when dealing with large datasets that require export for offline analysis or sharing purposes.

By default, when utilizing observables for Grid data binding, the export operation exports only records on the current page. However, the Syncfusion Angular Grid component allows you to export all records, including those from multiple pages, by configuring the pdfExportProperties and excelExportProperties.

To export all records, including those from multiple pages, configure the pdfExportProperties.dataSource for PDF exporting and excelExportProperties.dataSource for Excel exporting within the toolbarClick event handler. Inside this event, set the dataSource property of pdfExportProperties and excelExportProperties for PDF and Excel exporting to include all records.

Excel Exporting

To export the complete grid data to Excel, utilize the excelExportProperties.dataSource when initiating the Excel export. Use the following code snippet to export all records within the grid:

this.service.getData(state).subscribe((e: any) => {
  let excelExportProperties: ExcelExportProperties = {
    dataSource: e.result ? e.result : result
  };
  (this.grid as GridComponent).excelExport(excelExportProperties); // need to call excelExport method of grid when get the entire data
});

PDF Exporting

To export the complete grid data to PDF document, utilize the pdfExportProperties.dataSource when initiating the PDF export. Use the following code snippet to export all records within the grid:

this.service.getData(state).subscribe((e: any) => {
  let pdfExportProperties: PdfExportProperties = {
    dataSource: e.result ? e.result : result
  };
  (this.grid as GridComponent).pdfExport(pdfExportProperties); // need to call pdfExport method of grid when get the entire data
});

Further customization on grid export can be accessed in the respective documentation for PDF exporting and Excel exporting

The following code example shows how to export all records in client side for observable without using the async pipe:

import { Component, OnInit, ViewChild } from '@angular/core';
import { GridComponent } from '@syncfusion/ej2-angular-grids';
import {DataStateChangeEventArgs, PdfExportProperties, ExcelExportProperties} from '@syncfusion/ej2-angular-grids';
import { ClickEventArgs } from "@syncfusion/ej2-navigations";
import { DataService } from './order.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `<ejs-grid #grid [dataSource]='data' (excelExportComplete)="exportComplete()" (pdfExportComplete)="exportComplete()" [allowExcelExport]='true' [allowPdfExport]='true' allowPaging='true' [pageSettings]='pageOptions' [toolbar]="toolbar" (toolbarClick)='toolbarClick($event)' (dataStateChange)= 'dataStateChange($event)'>
                <e-columns>
                  <e-column field='OrderID' headerText='Order ID' width='90' textAlign='Right' isPrimaryKey='true'></e-column>
                  <e-column field="CustomerID" headerText="Customer Name" width="100"></e-column>
                  <e-column field='ShipName' headerText="Ship Name" width=110></e-column>
                  <e-column field='ShipCountry' headerText='Ship Country' width=100></e-column>
                  <e-column field='Freight' headerText='Freight' format='C2' textAlign='Right' width=100></e-column>
                </e-columns>
              </ejs-grid>`,
  providers: [DataService],
})
export class AppComponent implements OnInit {
  public data?: object;
  public state?: DataStateChangeEventArgs;
  public pageOptions?: object;
  public toolbar?: string[];
  @ViewChild('grid')
  public grid?: GridComponent;

  constructor(public service: DataService) {
    this.data = service;
  }

  public dataStateChange(state: DataStateChangeEventArgs): void {
    this.service.getData(state).subscribe(response => (this.data = response));
  }

  exportComplete() {
    (this.grid as GridComponent).hideSpinner(); // hide the spinner when export completed
  }

  toolbarClick(args: ClickEventArgs): void {
    let state: any = { action: {}, skip: 0, take: (this.grid as GridComponent).pageSettings.totalRecordsCount };
    let result = {};
    switch (args.item.text) {
      case "PDF Export":
        (this.grid as GridComponent).showSpinner(); // show the spinner when send the post to service
        state.action.isPdfExport = true;
        // fetch the entire data while PDF exporting
        this.service.getData(state).subscribe((e: any) => {
          let pdfExportProperties: PdfExportProperties = {
            dataSource: e.result ? e.result : result
          };
          (this.grid as GridComponent).pdfExport(pdfExportProperties); // need to call pdfExport method of grid when get the entire data
        });
        break;
      case "Excel Export":
        // fetch the entire data while Excel exporting
        (this.grid as GridComponent).showSpinner(); // show the spinner when send the post to service
        state.action.isExcelExport = true;
        this.service.getData(state).subscribe((e: any) => {
          let excelExportProperties: ExcelExportProperties = {
            dataSource: e.result ? e.result : result
          };
          (this.grid as GridComponent).excelExport(excelExportProperties); // need to call excelExport method of grid when get the entire data
        });
        break;
    }
  }
  public ngOnInit(): void {
    this.pageOptions = { pageSize: 10, pageCount: 4 };
    const state = { skip: 0, take: 10 };
    this.toolbar = ["ExcelExport", "PdfExport"];
    this.service.getData(state).subscribe(response => (this.data = response));
  }
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {DataStateChangeEventArgs,DataResult} from '@syncfusion/ej2-angular-grids';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs';
import { map } from 'rxjs';

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

  private BASE_URL = 'https://services.odata.org/V4/Northwind/Northwind.svc/Orders';

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

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

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

    return this.http
    .get(`${this.BASE_URL}?${pageQuery}&$count=true`)
    .pipe(map((response: any) => response))
    .pipe(map((response: any) => {
      return state.dataSource === undefined ? (<DataResult>{
          
        result: response['value'],
        count: parseInt(response['@odata.count'], 10),
      }) : response['value'];
    }))
    .pipe(map((data: any) => data));
  }
}

Sending additional parameters to the server

The Syncfusion Grid component enables you to include custom parameters in data requests. This feature proves particularly useful when providing additional information to the server for enhanced processing capabilities.

By utilizing the query property of the grid along with the addParams method of the Query class, you can easily incorporate custom parameters into data requests for every grid action.

To enable custom parameters in data requests for the grid component:

1. Bind the Query Object to the Grid: Assign the initialized query object to the query property of the Syncfusion Grid component.

2. Initialize the Query Object: Create a new instance of the Query class and use the addParams method to add the custom parameters.

3. Handle Data State Changes: If you need to dynamically update data based on interactions, implement the dataStateChange event handler to execute the query with the updated state.

4. Execute Data Request: In the service, execute the data request by combining the custom parameters with other query parameters such as paging and sorting.

The following example demonstrates how to send additional parameters to the server using observables:

import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { GridModule } from '@syncfusion/ej2-angular-grids'
import { HttpClient, HttpClientModule } from '@angular/common/http'
import { PageService, SortService, GroupService, PdfExportService, ExcelExportService, FilterService, EditService, ToolbarService, AggregateService } from '@syncfusion/ej2-angular-grids'

import { Component, OnInit, ViewChild } from '@angular/core';
import { GridComponent } from '@syncfusion/ej2-angular-grids';
import { DataStateChangeEventArgs } from '@syncfusion/ej2-angular-grids';
import { Query } from '@syncfusion/ej2-data';
import { DataService } from './order.service';
import { Observable } from 'rxjs';
import { CommonModule } from '@angular/common'
@Component({
imports: [
      CommonModule,
      GridModule,
      HttpClientModule,
    ],

providers: [PageService,
                SortService,
                FilterService,
                EditService,
                ToolbarService,
                GroupService,
                PdfExportService, ExcelExportService,
                AggregateService,HttpClient,DataService ],
standalone: true,
  selector: 'app-root',
  template: `<ejs-grid #grid [dataSource]='data | async' [query]="query" allowPaging='true' [pageSettings]='pageOptions' 
              allowSorting= 'true' allowGrouping= 'true' (dataStateChange)= 'dataStateChange($event)'>
                <e-columns>
                  <e-column field='OrderID' headerText='Order ID' width='90' textAlign='Right'></e-column>
                  <e-column field="CustomerID" headerText="Customer Name" width="100"></e-column>
                  <e-column field='ShipCountry' headerText='Ship Country' width=100></e-column>
                  <e-column field='Freight' headerText='Freight' format='C2' textAlign='Right' width=100></e-column>
                </e-columns>
              </ejs-grid>`
})
export class AppComponent implements OnInit {
  public data?: Observable<DataStateChangeEventArgs>;
  public state?: DataStateChangeEventArgs;
  public pageOptions?: object;
  public query?: Query;
  @ViewChild('grid')
  public grid?: GridComponent;

  constructor(public service: DataService) {
    this.data = service;
  }

  public dataStateChange(state: DataStateChangeEventArgs): void {
    this.service.execute(state, this.query);
  }
  public ngOnInit(): void {
    this.pageOptions = { pageSize: 10, pageCount: 4 };
    const state = { skip: 0, take: 10 };
    this.query = new Query().addParams('Syncfusion_Angular_Grid', 'true');
    this.service.execute(state, this.query);
  }
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
  DataStateChangeEventArgs,
  Sorts,
  DataResult,
} from '@syncfusion/ej2-angular-grids';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs';
import { map } from 'rxjs';

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

  private BASE_URL = 'https://services.odata.org/V4/Northwind/Northwind.svc/Orders';

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

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

  public getData(state: DataStateChangeEventArgs, addtionalParam: any): Observable<DataStateChangeEventArgs> {
    const pageQuery = `$skip=${state.skip}&$top=${state.take}`;
    let sortQuery: string = '';

    if ((state.sorted || []).length) {
      sortQuery = `&$orderby=` + state.sorted?.map((obj: Sorts) => {
        return obj.direction === 'descending' ? `${obj.name} desc` : obj.name;
      }).reverse().join(',');
    }

    const customQuery = `&${addtionalParam.params[0].key}=${addtionalParam.params[0].value}`;

    return this.http
      .get(`${this.BASE_URL}?${pageQuery}${sortQuery}${customQuery}&$count=true`)
      .pipe(map((response: any) => response))
      .pipe(map((response: any) => (<DataResult>{
        result: response['value'],
        count: parseInt(response['@odata.count'], 10),
      })))
      .pipe((data: any) => data);
  }

}

AdditionalParameters

Offline mode

On remote data binding, all grid actions such as paging, sorting, editing, grouping, filtering, etc., process on server-side. To avoid post back for every action, set the grid to load all data on initialization and make the actions process in client-side. To enable this behavior, use the offline property of DataManager.

import { Component, OnInit } from '@angular/core';
import { DataManager, ODataAdaptor } from '@syncfusion/ej2-data';

@Component({
    selector: 'app-root',
    template: `<ejs-grid [dataSource]='data' [allowPaging]='true' [allowGrouping]='true' [allowSorting]='true' [pageSettings]='pageOptions'>
                <e-columns>
                    <e-column field='OrderID' headerText='Order ID' textAlign='Right' width=120></e-column>
                    <e-column field='CustomerID' headerText='Customer ID' width=150></e-column>
                    <e-column field='ShipCity' headerText='Ship City' width=150></e-column>
                    <e-column field='ShipName' headerText='Ship Name' width=150></e-column>
                </e-columns>
                </ejs-grid>`
})
export class AppComponent implements OnInit {

    public data: DataManager;
    public pageOptions = { pageSize: 7 };

    ngOnInit(): void {
        this.data = new DataManager({
            url: 'https://js.syncfusion.com/demos/ejServices/Wcf/Northwind.svc/Orders?$top=7',
            adaptor: new ODataAdaptor(),
            offline: true
        });
    }
}

Fetch result from the DataManager query using external button

By default, Syncfusion Angular Grid automatically binds a remote data source using the DataManager. However, in some scenarios, you may need to fetch data dynamically from the server using a query triggered by an external button. This approach allows greater control over when and how data loads into the Grid.

To achieve this, you can use the executeQuery method of DataManager with a Query object. This method enables you to run a custom query and retrieve results dynamically.

The following example demonstrates how to fetch data from the server when an external button is clicked and display a status message indicating the data fetch status:

import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { GridModule,PageService } from '@syncfusion/ej2-angular-grids'
import { Component, OnInit } from '@angular/core';
import { DataManager, WebApiAdaptor, Query, ReturnOption } from '@syncfusion/ej2-data';

const SERVICE_URI: string = 'https://ej2services.syncfusion.com/production/web-services/';

// Define an interface for Order data.
interface Order {
    OrderID: number;
    CustomerID: string;
    Freight: number;
    OrderDate: string;
}

@Component({
    imports: [ GridModule],
    providers: [PageService],
    standalone: true,
    selector: 'app-root',
    template: `
        <div style="padding-bottom:20px">
            <button ejs-button (click)="executeQuery($event)">Execute Query</button>
        </div>
        <p *ngIf="statusMessage" style="text-align:center;color:red"></p>
        <ejs-grid #grid [dataSource]='result' allowPaging='true'>
            <e-columns>
                <e-column field='OrderID' headerText='Order ID' width='120' textAlign='Right'></e-column>
                <e-column field='CustomerID' headerText='Customer ID' width='160'></e-column>
                <e-column field='EmployeeID' headerText='Employee ID' width='120' textAlign='Right'></e-column>
                <e-column field='Freight' headerText='Freight' width='150' format="C2" textAlign='Right'></e-column>
                <e-column field='ShipCountry' headerText='Ship Country' width='150' ></e-column>
            </e-columns>
        </ejs-grid>`
})

export class AppComponent implements OnInit {
    public statusMessage: string = '';
    public getData: DataManager;
    public result: Order[] = [];

    public ngOnInit(): void {
       this.getData= new DataManager({ 
            url: SERVICE_URI + 'api/Orders', 
            adaptor: new WebApiAdaptor() 
        });
    }

    public executeQuery(event:MouseEvent): void { 
        this.statusMessage = "Fetching data...";
        this.getData.executeQuery(new Query()).then((e: ReturnOption) => {
            this.result = e.result as Order[];
            this.statusMessage = `Data fetched successfully! Total Records: ${this.result.length}`;
        }).catch((error) => {
            this.statusMessage = "Error fetching data!";
        });
    }
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));