Hybrid approach with offline filtering and remote fetching/updating data

Hi, I have following component:

import { Component, Inject, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { APP_CONFIG, CoreConfig } from '@arcelormittal-platform/core';
import {
  ProcessesGridService,
  WcmPillarsService,
  StrategicalAxisesService,
} from '@masterdata/services';
import {
  LoadingIndicator,
  PageSettingsModel,
  EditSettingsModel,
  GridComponent,
  EditService,
  PageService,
  ToolbarService,
  RowDropEventArgs,
  FilterSettings,
  GridLine,
  ColumnChooserService,
  FilterService,
  FreezeService,
  ReorderService,
  ResizeService,
  SortService,
  RowDDService,
  DataResult,
  DataStateChangeEventArgs,
  PdfExportService,
  ExcelExportService,
} from '@syncfusion/ej2-angular-grids';
import { FormArray } from '@angular/forms';
import { FieldSettingsModel } from '@syncfusion/ej2-angular-dropdowns';
import { StrategicalAxisDropdownItem, WcmPillarDropdownItem } from '@masterdata/models';
import { Observable, Subject, takeUntil, finalize } from 'rxjs';
import { GridService, ToolbarConfigService } from '@shared/services';
import { MenuEventArgs } from '@syncfusion/ej2-angular-splitbuttons';
import { Query, DataManager, RemoteSaveAdaptor } from '@syncfusion/ej2-data';

@Component({
  selector: 'am-process-tab-item',
  templateUrl: './process-tab-item.component.html',
  providers: [
    PageService,
    EditService,
    ToolbarService,
    ColumnChooserService,
    FilterService,
    FreezeService,
    ReorderService,
    ResizeService,
    SortService,
    RowDDService,
    PdfExportService,
    ExcelExportService,
  ],
})
export class ProcessTabItemComponent implements OnInit, OnDestroy {
  @Input() public fetchActive = true;
  @ViewChild('grid') grid: GridComponent | undefined;
  public clientData: DataResult | undefined;

  public loadingIndicator: Partial<LoadingIndicator>;
  public pageSettings: PageSettingsModel;
  public editSettings: EditSettingsModel;
  public toolbar: (string | object)[];


  public currentDescriptionFormArray: FormArray | undefined;
  public descriptionDialogVisible = false;

  public wcmPillars$: Observable<WcmPillarDropdownItem[]> | undefined;
  public selectedWcmPillarIds: number[] | undefined;
  public wcmPillarFields: FieldSettingsModel;

  public strategicalAxises$: Observable<StrategicalAxisDropdownItem[]> | undefined;
  public selectedStrategicalAxisIds: number[] | undefined;
  public strategicalAxisFields: FieldSettingsModel;

  public freezingOptions: { text: string; field: string }[];
  public selectedFrozenColumns: string[] = [];

  public filterTypeItems = [
    { text: 'FilterBar' },
    { text: 'Menu' },
    { text: 'CheckBox' },
    { text: 'Excel' },
  ];

  public gridLineItems = [
    { text: 'None' },
    { text: 'Default' },
    { text: 'Both' },
    { text: 'Horizontal' },
    { text: 'Vertical' },
  ];

  public lines: GridLine;
  public filterSettings: Partial<FilterSettings>;
  public allowRowDragAndDrop = true;

  private _baseUrl: string;
  private _accessToken: string;

  private destroy$ = new Subject<void>();

  constructor(
    private readonly processesService: ProcessesGridService,
    private readonly wcmPillarsService: WcmPillarsService,
    private readonly strategicalAxisesService: StrategicalAxisesService,
    private readonly toolbarConfigService: ToolbarConfigService,
    private readonly gridService: GridService,
    @Inject(APP_CONFIG) private readonly appConfig: CoreConfig
  ) {
    this._baseUrl = `${this.appConfig.api}masterdata/lists/processes/grid`;
    const authValue = JSON.parse(
      sessionStorage.getItem('oidc.user:https://sts.belgium.arcelormittal.com/TokenService:PROMATO')
    );
    this._accessToken = authValue.access_token;
  }

  public ngOnInit(): void {
    this.loadingIndicator = { indicatorType: 'Shimmer' };
    this.fetchData();
    this.pageSettings = { pageSize: 20 };
    this.editSettings = {
      allowEditing: true,
      allowAdding: true,
      allowDeleting: true,
      mode: 'Normal',
      newRowPosition: 'Bottom',
    };
    this.setupWcmPillars();
    this.setupStrategicalAxises();
    this.configureToolbar();
  }

  public ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  public dataStateChange(state: DataStateChangeEventArgs) {
    const query = this.grid.getDataModule().generateQuery().requiresCount();
    if (state.action) {
      this.grid.dataSource = new DataManager({
        json: this.clientData.result,
        updateUrl: `${this._baseUrl}/update`,
        insertUrl: `${this._baseUrl}/create`,
        adaptor: new RemoteSaveAdaptor(),
        headers: [
          {
            Authorization: `Bearer ${this._accessToken}`,
          },
        ],
      }).executeLocal(query);
      if (state.dataSource) {
        state.dataSource(this.clientData.result);
      }
    } else {
      this.fetchDataFromService(query);
    }
  }

  public dataBound() {
    this.configureFreezingOptions();
  }

  public actionBegin(args: any) {
    const requestType = args.requestType;
    if (requestType === 'beginEdit') {
      this.setCurrentWcmPillarValues(args.rowData);
      this.setCurrentStrategicalAxises(args.rowData);
    }

    if (requestType === 'save' || requestType === 'add') {
      if (requestType === 'add') {
        args.data.isActive = true;
        args.data.order = this.grid.pageSettings.totalRecordsCount + 1;
      }
    }

    if (requestType === 'sorting') {
      this.allowRowDragAndDrop = !args.columnName;
    }
  }

  public rowDrop(args: RowDropEventArgs) {
    const data = args.data[0] as any;
    const pageSize = this.grid.pageSettings.pageSize;
    const currentPage = this.grid.pageSettings.currentPage;
    const newIndex = args.dropIndex + pageSize * (currentPage - 1) + 1;
    const oldIndex = args.fromIndex + pageSize * (currentPage - 1) + 1;
    this.processesService
      .saveNewOrder$({ id: data.id, newIndex, oldIndex })
      .pipe(
        finalize(() => this.fetchData()),
        takeUntil(this.destroy$)
      )
      .subscribe();
  }

  public filterChangeHandler(args: MenuEventArgs) {
    this.gridService.handleFilterChange(args, this.grid);
  }

  public gridLinesChangeHandler(args: MenuEventArgs) {
    const type = args.item.text;
    this.lines = type as GridLine;
    setTimeout(() => {
      this.grid.refresh();
    }, 0);
  }

  public handleFreezeChange(args: any, fieldColumn: string) {
    this.selectedFrozenColumns = this.gridService.handleFreezeChange(
      args,
      fieldColumn,
      this.grid,
      this.selectedFrozenColumns
    );
  }

  public pdfExport() {
    this.grid.pdfExport();
  }

  public excelExport() {
    this.grid.excelExport();
  }

  public addNewRecord() {
    this.grid.addRecord();
  }

  public handleClearFiltering() {
    this.gridService.handleClearFiltering(this.grid);
  }

  private fetchData() {
    const state = { skip: 0, take: 20 };
    const query = new Query().skip(state.skip).take(state.take).requiresCount();
    this.fetchDataFromService(query);
  }

  private fetchDataFromService(query: Query) {
    this.processesService
      .getAllData(query, this.fetchActive)
      .then((response: DataResult | Response) => {
        this.grid.dataSource = response;
        this.clientData = response as DataResult;
      });
  }

  private setCurrentWcmPillarValues(data: any) {
    this.selectedWcmPillarIds = data.wcmPillars.map((x) => x.id);
  }

  private setupWcmPillars() {
    this.wcmPillars$ = this.wcmPillarsService.GetDropdownItems();
    this.wcmPillarFields = { value: 'id', text: 'name' };
  }

  private setCurrentStrategicalAxises(data: any) {
    this.selectedStrategicalAxisIds = data.strategicalAxises.map((x) => x.id);
  }

  private setupStrategicalAxises() {
    this.strategicalAxises$ = this.strategicalAxisesService.getDropdownItems();
    this.strategicalAxisFields = { value: 'id', text: 'name' };
  }

  private configureFreezingOptions() {
    if (this.grid) {
      this.freezingOptions = this.grid
        ?.getVisibleColumns()
        .map((column) => ({ text: column.headerText, field: column.field }))
        .filter((x) => !!x.field)
        .sort((a, b) => a.text?.localeCompare(b.text));
    }
  }

  private configureToolbar() {
    this.toolbar = this.toolbarConfigService.getConfig();
  }
}

Function in the service to get the data:
public getAllData(query: Query, fetchActive: boolean) {
    (query as any).addParams('isActive', fetchActive);

    return new DataManager({
      url: `${this._baseUrl}/read`,
      updateUrl: `${this._baseUrl}/update`,
      insertUrl: `${this._baseUrl}/create`,
      adaptor: new ProcessesAdaptor(),
      headers: [
        {
          Authorization: `Bearer ${this._accessToken}`,
        },
      ],
    })
      .executeQuery(query)
      .then((response: DataResult | Response) => {
        return response as DataResult;
      });
  }
So what I am trying to do, is fetch data, and apply the filters that the user chooses, to the data that's on the current page, without fetching the data from the backend again. This works, but I have the problem that inserting and updating data do not work anymore, and I think it's because I reassign the datasource with a new DataManager,
but I don't know how to solve the problem

5 Replies

AR Aishwarya Rameshbabu Syncfusion Team September 16, 2024 02:37 PM UTC

Hi Niels Van Goethem,


Greetings from Syncfusion support.


Upon reviewing your query, we have observed that you need to apply filtering offline, limited to current page records, while performing CRUD operations with remote data. Furthermore, the provided code example does not include the event handler function for the  dataSourceChanged event, which is necessary for processing CRUD actions based on the requestType for custom data binding in the Grid. The endEdit method needs to be called to signal the completion of the operation and update the Grid accordingly. We have extensively discussed the process of executing CRUD operations with remote data using custom data binding in the Grid in our documentation. For more detailed information, please refer to the documentation link provided below.


Documentation Link: https://ej2.syncfusion.com/angular/documentation/grid/data-binding/remote-data#handling-crud-operations


If you need any other assistance or have additional questions, please feel free to contact us.


Regards

Aishwarya R



NV Niels Van Goethem October 1, 2024 01:27 PM UTC

Hello, I have currently implemented this:

import {
  AfterViewInit,
  Component,
  Inject,
  Input,
  OnDestroy,
  OnInit,
  ViewChild
} from '@angular/core';
import { APP_CONFIG, CoreConfig, TranslationService } from '@arcelormittal-platform/core';
import { AuthenticationService } from '@arcelormittal-platform/security';
import { SnackBarService, SnackBarType } from '@arcelormittal-platform/ui';
import { StrategicalAxisDropdownItem, ThemeDropdownItem } from '@masterdata/models';
import {
  ProcessesGridService,
  StrategicalAxisesService,
  ThemesService
} from '@masterdata/services';
import { TranslateService } from '@ngx-translate/core';
import { GridService, ToolbarConfigService } from '@shared/services';
import { FieldSettingsModel } from '@syncfusion/ej2-angular-dropdowns';
import {
  ColumnChooserService,
  DataResult,
  DataStateChangeEventArgs,
  EditService,
  EditSettingsModel,
  ExcelExportService,
  FilterService,
  FreezeService,
  GridComponent,
  GridLine,
  PageService,
  PageSettingsModel,
  PdfExportService,
  ReorderService,
  ResizeService,
  RowDDService,
  RowDropEventArgs,
  SaveEventArgs,
  SortService,
  ToolbarService
} from '@syncfusion/ej2-angular-grids';
import { MenuEventArgs } from '@syncfusion/ej2-angular-splitbuttons';
import { DataManager, Query, RemoteSaveAdaptor } from '@syncfusion/ej2-data';
import { catchError, finalize, retry, Subject, takeUntil, tap } from 'rxjs';

@Component({
  selector: 'am-process-tab-item',
  templateUrl: './process-tab-item.component.html',
  styleUrls: ['./process-tab-item.component.scss'],
  providers: [
    PageService,
    EditService,
    ToolbarService,
    ColumnChooserService,
    FilterService,
    FreezeService,
    ReorderService,
    ResizeService,
    SortService,
    RowDDService,
    PdfExportService,
    ExcelExportService,
  ],
})
export class ProcessTabItemComponent implements OnInit, AfterViewInit, OnDestroy {
  @Input() public fetchActive = true;
  @ViewChild('grid') grid: GridComponent | undefined;
  public clientData: DataResult | undefined;

  public showLoader = true;
  public pageSettings: PageSettingsModel;
  public editSettings: EditSettingsModel;
  public toolbar: (string | object)[];

  public themes: ThemeDropdownItem[] | undefined;
  public selectedThemeIds: { [key: number]: number[] };
  public themeFields: FieldSettingsModel;

  public strategicalAxises: StrategicalAxisDropdownItem[] | undefined;
  public selectedStrategicalAxisIds: { [key: number]: number[] };
  public strategicalAxisFields: FieldSettingsModel;

  public freezingOptions: { text: string; field: string }[];

  public filterTypeItems = [
    { text: 'FilterBar', iconCss: 'e-icons e-bullet-5' },
    { text: 'Menu', iconCss: 'e-icons e-bullet-5' },
    { text: 'CheckBox', iconCss: 'e-icons e-bullet-5' },
    { text: 'Excel', iconCss: 'e-icons e-bullet-5' },
  ];

  public gridLineItems = [
    { text: 'None', iconCss: 'e-icons e-bullet-5' },
    { text: 'Default', iconCss: 'e-icons e-bullet-5' },
    { text: 'Both', iconCss: 'e-icons e-bullet-5' },
    { text: 'Horizontal', iconCss: 'e-icons e-bullet-5' },
    { text: 'Vertical', iconCss: 'e-icons e-bullet-5' },
  ];

  public allowRowDragAndDrop = true;
  public currentLanguage: string | undefined;

  public nameENRules: any;

  private _baseUrl: string | undefined;
  private _accessToken: string | undefined;
  private userSettingsKey: string | undefined;

  private destroy$ = new Subject<void>();

  constructor(
    private readonly processesGridService: ProcessesGridService,
    private readonly themeService: ThemesService,
    private readonly strategicalAxisesService: StrategicalAxisesService,
    private readonly toolbarConfigService: ToolbarConfigService,
    private readonly gridService: GridService,
    private readonly translationService: TranslationService,
    @Inject(APP_CONFIG) private readonly appConfig: CoreConfig,
    private readonly snackBarService: SnackBarService,
    private readonly translateService: TranslateService,
    private readonly authService: AuthenticationService
  ) {
    this._baseUrl = `${this.appConfig.api}masterdata/lists/processes/grid`;
    const authValue = JSON.parse(
      sessionStorage.getItem('oidc.user:https://sts.belgium.arcelormittal.com/TokenService:PROMATO')
    );
    this._accessToken = authValue.access_token;
  }

  public get selectedFrozenColumns() {
    return this.gridService.selectedFrozenColumns;
  }

  public get lines() {
    return this.gridService.lines;
  }

  public ngOnInit(): void {
    this.authService.userProfile$
      .pipe(
        tap((userProfile) => {
          this.userSettingsKey = `${userProfile.sid}-processes-gridState-${
            this.fetchActive ? 'active' : 'inactive'
          }`;
        }),
        takeUntil(this.destroy$)
      )
      .subscribe();
    this.currentLanguage = this.translationService.getCurrentLanguage();
    this.fetchData();
    this.configureValidationRules();
    this.pageSettings = { pageSize: 20 };
    this.editSettings = {
      allowEditing: true,
      allowAdding: true,
      allowDeleting: true,
      mode: 'Normal',
      newRowPosition: 'Bottom',
    };
    this.setupThemes();
    this.setupStrategicalAxises();
    this.configureToolbar();
  }

  public ngAfterViewInit(): void {
    this.configureToolbar();
    this.gridService.loadGridState(this.userSettingsKey, this.grid);
  }

  public ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  public dataBound() {
    this.configureFreezingOptions();
  }

  public dataStateChange(state: DataStateChangeEventArgs) {
    console.log('🚀 ~ ProcessTabItemComponent ~ dataStateChange ~ state:', state);
    const query = this.grid.getDataModule().generateQuery().requiresCount();
    if (state.action) {
      this.grid.dataSource = new DataManager(this.clientData.result).executeLocal(query);
      if (state.dataSource) {
        state.dataSource(this.clientData.result);
      }
    } else {
      this.fetchDataFromService(query);
    }
  }

  public actionBegin(args: any) {
    const requestType = args.requestType;
    if (requestType === 'beginEdit' || requestType === 'add') {
      this.setCurrentThemeValues(args.rowData);
      this.setCurrentStrategicalAxises(args.rowData);
    }

    if (requestType === 'save') {
      args.data.attachedThemeIds = this.selectedThemeIds[args.rowData.id];
      args.data.attachedStrategicalAxisIds = this.selectedStrategicalAxisIds[args.rowData.id];
      const isChanges = JSON.stringify(args.data) !== JSON.stringify(args.previousData);
      if (!isChanges) {
        args.cancel = true;
        this.grid.closeEdit();
      }
    }

    if (requestType === 'add') {
      args.data.isActive = true;
      args.data.order = ++(this.grid.getPreviousRowData() as any).order;
    }

    if (requestType === 'sorting') {
      this.allowRowDragAndDrop =
        !args.columnName || (args.columnName === 'order' && args.direction === 'Ascending');
    }
  }

  public onActionComplete(args: SaveEventArgs) {
    if (args.requestType === 'save') {
      this.snackBarService.open(
        {
          message: this.translateService.instant('Processes.SuccessMessages.Save'),
        },
        SnackBarType.CONFIRM
      );
      this.fetchData();
    }
  }

  public rowDrop(args: RowDropEventArgs) {
    const data = args.data[0] as any;
    const pageSize = this.grid.pageSettings.pageSize;
    const currentPage = this.grid.pageSettings.currentPage;
    const newIndex = args.dropIndex + pageSize * (currentPage - 1) + 1;
    const oldIndex = args.fromIndex + pageSize * (currentPage - 1) + 1;
    this.processesGridService
      .saveNewOrder$({ id: data.id, newIndex, oldIndex })
      .pipe(
        finalize(() => this.fetchData()),
        takeUntil(this.destroy$)
      )
      .subscribe();
  }

  public filterChangeHandler(args: MenuEventArgs) {
    this.gridService.handleFilterChange(args, this.grid);
    setTimeout(() => {
      this.saveGridState();
    }, 0);
  }

  public gridLinesChangeHandler(args: MenuEventArgs) {
    const type = args.item.text;
    this.gridService.lines = type as GridLine;
    this.saveGridState();
    setTimeout(() => {
      this.grid.refresh();
    }, 0);
  }

  public handleFreezeChange(args: any, fieldColumn: string) {
    this.gridService.selectedFrozenColumns = this.gridService.handleFreezeChange(
      args,
      fieldColumn,
      this.grid
    );
    this.saveGridState();
  }

  public pdfExport() {
    this.grid.pdfExport();
  }

  public excelExport() {
    this.grid.excelExport();
  }

  public addNewRecord() {
    this.grid.addRecord();
  }

  public handleClearFiltering() {
    this.gridService.handleClearFiltering(this.grid);
  }

  public fetchData() {
    const state = { skip: 0, take: 20 };
    const query = new Query().skip(state.skip).take(state.take).requiresCount();
    this.fetchDataFromService(query);
  }

  public searchList(searchTerm: string) {
    const state = { skip: 0, take: 20 };
    const query = new Query().skip(state.skip).take(state.take).requiresCount();
    this.fetchDataFromService(query, searchTerm);
  }

  public onActionFailure() {
    this.showLoader = false;
    this.snackBarService.open(
      {
        message: this.translateService.instant('Processes.ErrorMessages.Save'),
      },
      SnackBarType.ERROR
    );
  }

  public saveGridState() {
    this.gridService.saveGridState(this.userSettingsKey, this.grid);
  }

  private fetchDataFromService(query: Query, searchTerm = '') {
    this.processesGridService
      .getAllData$(query, this.fetchActive, searchTerm)
      .pipe(
        tap((response: DataResult | Response) => {
          this.showLoader = false;
          this.clientData = response as DataResult;
          this.grid.dataSource = new DataManager({
            json: this.clientData.result,
            updateUrl: `${this._baseUrl}/update`,
            insertUrl: `${this._baseUrl}/create`,
            adaptor: new RemoteSaveAdaptor(),
            headers: [
              {
                Authorization: `Bearer ${this._accessToken}`,
              },
            ],
          });
        }),
        catchError((err) => {
          this.showLoader = false;
          this.snackBarService.open(
            {
              message: this.translateService.instant('Processes.ErrorMessages.Fetch'),
            },
            SnackBarType.ERROR
          );
          return err;
        }),
        retry(3),
        takeUntil(this.destroy$)
      )
      .subscribe();
  }

  private setCurrentThemeValues(data: any) {
    this.selectedThemeIds = { ...this.selectedThemeIds, [data?.id ?? -1]: data.attachedThemeIds };
  }

  private setupThemes() {
    this.themeService
      .getDropdownItems$()
      .pipe(
        tap((theme) => (this.themes = theme)),
        takeUntil(this.destroy$)
      )
      .subscribe();
    this.themeFields = { value: 'id', text: 'name' };
  }

  private setCurrentStrategicalAxises(data: any) {
    this.selectedStrategicalAxisIds = {
      ...this.selectedStrategicalAxisIds,
      [data?.id ?? -1]: data.attachedStrategicalAxisIds,
    };
  }

  private setupStrategicalAxises() {
    this.strategicalAxisesService
      .getDropdownItems$()
      .pipe(
        tap((strategicalAxises) => (this.strategicalAxises = strategicalAxises)),
        takeUntil(this.destroy$)
      )
      .subscribe();
    this.strategicalAxisFields = { value: 'id', text: 'name' };
  }

  private configureFreezingOptions() {
    if (this.grid) {
      this.freezingOptions = this.grid
        ?.getVisibleColumns()
        .map((column) => ({ text: column.headerText, field: column.field }))
        .filter((x) => !!x.field && !!x.text)
        .sort((a, b) => a.text?.localeCompare(b.text));
    }
  }

  private configureToolbar() {
    this.toolbar = this.toolbarConfigService.getConfig();
  }

  private configureValidationRules() {
    this.nameENRules = {
      required: [true, this.translateService.instant('Processes.ErrorMessages.NameEN.Required')],
    };
  }
}
This allows me to fetch the data remotely, do the crud actions on server, but sort and filter locally.
Only pagination is not working, and I suspect it's because the grid doesn't know how much records there are, as I never pass this.clientData.


AR Aishwarya Rameshbabu Syncfusion Team October 9, 2024 05:03 AM UTC

Hi Niels Van Goethem,


Based on the provided information and code example, it appears that you are encountering issues with pagination in the Syncfusion Grid. When utilizing the custom data binding in Syncfusion Grid, the data must be returned in the format of an object of 'result' and 'count.' This structure enables the Grid to execute paging actions based on the total number of records and the records on the current page. This topic is further detailed in our documentation section. For additional information, please refer to the following link:  


Documentation Link: Handling-paging-operation


Additionally, we have noticed that you are assigning the Grid’s dataSource property using an instance of DataManager with RemoteSaveAdaptor to achieve the hybrid approach. Please note that when binding data to the Grid using RemoteSaveAdaptor, by default, the actions such as sorting, filtering, searching, and paging are performed in the client side, while CRUD operations like updating, inserting, and removing data are handled in the server side for data persistence. For more detailed information, please consult the documentation.


Documentation Link: Remote-save-adaptor



Regards

Aishwarya                                                                                                                                                                          



NV Niels Van Goethem October 9, 2024 06:32 AM UTC

Like stated in the title and my original post, I want the actions to be performed locally, and crud operations to be handled server side. That's why I use RemoveSaveAdaptor. 

I receive a correct object from the server, with a result and count property, but I cannot pass that object in the datamanager, because it is not "json"



AR Aishwarya Rameshbabu Syncfusion Team October 14, 2024 06:26 PM UTC

Hi Niels Van Goethem,


We have responded to your query about the pagination issue in the Grid on the following forum. For further communication, please refer to the forum link below:


Forum Link: https://www.syncfusion.com/forums/194710/hybrid-approach-on-grid-offline-filtering-remotely-fetching-and-updating-data-problems-with


You can communicate with us regarding this query using the above forum.


Regards

Aishwarya R


Loader.
Up arrow icon