Filtering in memory

Is there a way to apply the filters to the data that's already fetched?

I want to filter the data on the current page only, without doing a call to the backend.

I am using the UrlAdaptor


4 Replies 1 reply marked as answer

VK Vasanthakumar K Syncfusion Team September 10, 2024 07:37 AM UTC

Hi Niels Van Goethem,


Greetings from Syncfusion support.


We have validated your query and understand that you need to know the way available to perform filter operations locally based on the current page data alone without making a request to the back-end and without performing a filter based on the full data source. We would like to inform you that if you are using an adaptor based as you mentioned UrlAdaptor, for every data operation, the grid makes a request for getting data as a result and count format from server-side and this is the default behavior of the grid’s UrlAdaptor data binding.


However, if you want to perform a custom way of data binding as you mentioned only filtering needs to be performed locally based on current page data alone, you can achieve this by using our grid’s custom data binding way with UrlAdaptor based promise data binding. We have prepared a sample based on your requirement for your reference purpose. Please refer to the below code example, documentation and sample for more details.


[code example]

public clientData?: DataResult; // maintained current page final request response data in application level.

constructor(public crudService: UrlService) {

}

ngOnInit(): void {

    const state = { skip: 0, take: 12 };

    const query = new Query().skip(state.skip).take(state.take). requiresCount();

    (this.crudService.execute(state, query) as Promise<any>).then((e) => { // perform initial custom data binding with request.

      (this.grid as GridComponent).dataSource = e;

      this.clientData = {...e};

    });

}

 

dataStateChange(state: DataStateChangeEventArgs) { // triggered at the time of data binding to grid for all action except CRUD.

    const query = (this.grid as GridComponent).getDataModule().generateQuery().requiresCount();

    if (state.action && (state.action as any).action === 'filter') { // perform custom data binding without request for filtering action of grid.

      (this.grid as GridComponent).dataSource = new DataManager(this.clientData?.result).executeLocal(query);

    } else {

      (this.crudService.execute(state, query) as Promise<any>).then((e) => { // perform custom data binding with request for all action of grid except filtering.

        (this.grid as GridComponent).dataSource = e;

        this.clientData = {...e};

      });

    }

}

 

export class UrlService {

  public execute(state: any, query: Query): Promise<any> {

    return this.getAllData(state, query);

  }

  /** GET all data from the server */

  getAllData(state: any, query: Query): Promise<any> {

    return new DataManager({

      url: SERVICE_URI + 'api/UrlDataSource',

      adaptor: new UrlAdaptor

    }).executeQuery(query).then((response: DataResult | Response) => {

      if (state.dataSource) {

        // binding dataSource for the string filter to get typing suggestion

        state.dataSource((response as DataResult).result);

        return;

      }

      return response;

    });

  }

}


Custom data binding documentation: https://helpej2.syncfusion.com/angular/documentation/grid/data-binding/remote-data


Sample: https://stackblitz.com/edit/github-c6tbvp-y7pst8?file=src%2Fapp.component.ts,src%2Furl.service.ts


If you still do not meet your requirement or face complexity in achieving your requirement, please confirm if you are ready to adapt our custom data binding method as mentioned in the above sample and documentation. Provide the complete grid rendering configuration code details to know more about your grid-enabled features (such as filter types, etc.), package details, and detailed description of your requirement use case scenario, along with a video demonstration for further validating your query on our end.


Regards,

Vasanthakumar K


Marked as answer

NV Niels Van Goethem September 10, 2024 08:18 AM UTC

This almost works like we want it to, but there is a small problem. When I change the filterType to "Menu", and I open the popup, the data disappears. When I start typing, I still get the suggestions at that point. When I try to apply the filter, still no data is show and I see this error:

core.mjs:9157 ERROR TypeError: Cannot read properties of null (reading 'adaptor')
    at Filter.updateModel (ej2-grids.es5.js:32885:49)
    at Filter.filterByColumn (ej2-grids.es5.js:33166:14)
    at StringFilterUI.read (ej2-grids.es5.js:31860:19)
    at FilterMenuRenderer.filterBtnClick (ej2-grids.es5.js:32413:36)
    at ej2-popups.es5.js:2397:56
    at timer (zone.js:3158:47)
    at _ZoneDelegate.invokeTask (zone.js:446:35)
    at Object.onInvokeTask (core.mjs:26237:33)
    at _ZoneDelegate.invokeTask (zone.js:445:64)
    at Zone.runTask (zone.js:214:51)

Then, when I close and open the popup, it says "No records found"

<ejs-grid
    #grid
    [loadingIndicator]="loadingIndicator"
    [pageSettings]="pageSettings"
    [allowPaging]="true"
    [editSettings]="editSettings"
    [toolbar]="toolbar"
    [gridLines]="lines"
    [allowFiltering]="true"
    [filterSettings]="filterSettings"
    [allowSorting]="true"
    [allowMultiSorting]="true"
    showColumnChooser="true"
    [allowReordering]="true"
    [allowResizing]="true"
    [allowRowDragAndDrop]="true"
    [allowSelection]="true"
    (dataBound)="dataBound()"
    (dataStateChange)="dataStateChange($event)"
    (recordDoubleClick)="recordDoubleClick($event)"
    (actionBegin)="actionBegin($event)"
    (rowDrop)="rowDrop($event)"
  >
    <e-columns>
      <e-column
        type="checkbox"
        width="40"
        freeze="Left"
        [showInColumnChooser]="false"
        [allowReordering]="false"
        [allowResizing]="false"
        [allowSorting]="false"
      ></e-column>
      <e-column
        field="id"
        [isPrimaryKey]="true"
        [visible]="false"
        [allowEditing]="false"
      ></e-column>
      <e-column [field]="nameField" [headerText]="'Processes.Headers.Name' | translate"></e-column>
      <e-column
        field="code"
        [headerText]="'Processes.Headers.Code' | translate"
        width="120"
      ></e-column>
      <e-column
        [field]="descriptionField"
        [headerText]="'Processes.Headers.Description' | translate"
      ></e-column>
      <e-column
        field="wcmPillarNames"
        [headerText]="'Processes.Headers.WcmPillars' | translate"
        [allowSorting]="false"
      >
        <ng-template #editTemplate>
          <ejs-multiselect
            [dataSource]="wcmPillars$ | async"
            [fields]="wcmPillarFields"
            [(value)]="selectedWcmPillarIds"
          ></ejs-multiselect>
        </ng-template>
      </e-column>
      <e-column
        field="strategicalAxisNames"
        [headerText]="'Processes.Headers.StrategicalAxis' | translate"
        [allowSorting]="false"
      >
        <ng-template #editTemplate>
          <ejs-multiselect
            [dataSource]="strategicalAxises$ | async"
            [fields]="strategicalAxisFields"
            [(value)]="selectedStrategicalAxisIds"
          ></ejs-multiselect>
        </ng-template>
      </e-column>
    </e-columns>
  </ejs-grid>
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { TranslationService } from '@arcelormittal-platform/core';
import {
  ProcessesGridService,
  WcmPillarsService,
  StrategicalAxisesService,
} from '@masterdata/services';
import {
  LoadingIndicator,
  PageSettingsModel,
  EditSettingsModel,
  GridComponent,
  RecordDoubleClickEventArgs,
  EditService,
  PageService,
  ToolbarService,
  RowDropEventArgs,
  FilterSettings,
  GridLine,
  ColumnChooserService,
  FilterService,
  FreezeService,
  ReorderService,
  ResizeService,
  SortService,
  RowDDService,
  DataResult,
  DataStateChangeEventArgs,
} from '@syncfusion/ej2-angular-grids';
import { FormArray, FormBuilder, Validators } from '@angular/forms';
import { FieldSettingsModel } from '@syncfusion/ej2-angular-dropdowns';
import { StrategicalAxisDropdownItem, WcmPillarDropdownItem } from '@masterdata/models';
import { Observable, Subject, takeUntil } from 'rxjs';
import { GridService, ToolbarConfigService } from '@shared/services';
import { MenuEventArgs } from '@syncfusion/ej2-angular-splitbuttons';
import { Query, DataManager } from '@syncfusion/ej2-data';

@Component({
  selector: 'am-processes',
  templateUrl: './processes.component.html',
  styleUrls: ['./processes.component.scss'],
  providers: [
    PageService,
    EditService,
    ToolbarService,
    ColumnChooserService,
    FilterService,
    FreezeService,
    ReorderService,
    ResizeService,
    SortService,
    RowDDService,
  ],
})
export class ProcessesComponent implements OnInit, OnDestroy {
  @ViewChild('grid') grid: GridComponent | undefined;
  public clientData: DataResult | undefined;

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

  public currentNameFormArray: FormArray | undefined;
  public nameDialogVisible = false;

  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>;

  private currentRowData: any | undefined;
  private currentColumnName: string | undefined;

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

  constructor(
    private readonly processesService: ProcessesGridService,
    private readonly translationService: TranslationService,
    private readonly fb: FormBuilder,
    private readonly wcmPillarsService: WcmPillarsService,
    private readonly strategicalAxisesService: StrategicalAxisesService,
    private readonly toolbarConfigService: ToolbarConfigService,
    private readonly gridService: GridService
  ) {}

  get nameField(): string {
    return `name${this.translationService.getCurrentLanguage().toUpperCase()}`;
  }

  get descriptionField(): string {
    return `description${this.translationService.getCurrentLanguage().toUpperCase()}`;
  }

  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.toolbar = ['Add'];
    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 && (state.action as any).action === 'filter') {
      this.grid.dataSource = new DataManager(this.clientData.result).executeLocal(query);
    } else {
      this.processesService.getAllData(state, query).then((response: DataResult | Response) => {
        this.grid.dataSource = response;
        this.clientData = response as DataResult;
      });
    }
  }

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

  public recordDoubleClick(args: RecordDoubleClickEventArgs) {
    this.currentColumnName = args.column.field;
    this.currentRowData = args.rowData;
    this.currentNameFormArray = this.buildFormArray(args, 'name');
    this.currentDescriptionFormArray = this.buildFormArray(args, 'description');
  }

  public actionBegin(args: any) {
    const requestType = args.requestType;
    if (requestType === 'beginEdit') {
      if (this.currentColumnName.startsWith('name')) {
        args.cancel = true;
        this.nameDialogVisible = true;
      }

      if (this.currentColumnName.startsWith('description')) {
        args.cancel = true;
        this.descriptionDialogVisible = true;
      }
      this.setCurrentWcmPillarValues(args.rowData);
      this.setCurrentStrategicalAxises(args.rowData);
    }

    if (requestType === 'save' || requestType === 'add') {
      args.data.attachedWcmPillars = this.selectedWcmPillarIds;
      args.data.attachedStrategicalAxises = this.selectedStrategicalAxisIds;
      const nameTranslationsAsLocalizedText = this.currentNameFormArray?.value.map(
        (translation) => ({
          code: translation.code,
          text: translation.text,
        })
      );
      args.data.names = nameTranslationsAsLocalizedText ?? [
        {
          code: this.translationService.getCurrentLanguage().toUpperCase(),
          text: args.data[`name${this.translationService.getCurrentLanguage().toUpperCase()}`],
        },
      ];

      const descriptionTranslationsAsLocalizedText = this.currentDescriptionFormArray?.value.map(
        (translation) => ({
          code: translation.code,
          text: translation.text,
        })
      );
      args.data.descriptions = descriptionTranslationsAsLocalizedText ?? [
        {
          code: this.translationService.getCurrentLanguage().toUpperCase(),
          text: args.data[
            `description${this.translationService.getCurrentLanguage().toUpperCase()}`
          ],
        },
      ];

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

  public hideNameDialog() {
    this.nameDialogVisible = false;
  }

  public hideDescriptionDialog() {
    this.descriptionDialogVisible = false;
  }

  public saveNameTranslations() {
    this.saveTranslations(this.currentNameFormArray);
  }

  public saveDescriptionTranslations() {
    this.saveTranslations(this.currentDescriptionFormArray);
  }

  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(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.gridService.handleFreezeChange(args, fieldColumn, this.grid, this.selectedFrozenColumns);
  }

  private fetchData() {
    const state = { skip: 0, take: 20 };
    const query = new Query().skip(state.skip).take(state.take).requiresCount();
    this.processesService.getAllData(state, query).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 buildFormArray(args: RecordDoubleClickEventArgs, fieldPrefix: string): FormArray {
    const newFormArray = new FormArray([]);
    Object.keys(args.rowData)
      .filter((key) => key.startsWith(fieldPrefix) && key !== fieldPrefix && !!args.rowData[key])
      .forEach((key) => {
        newFormArray.push(
          this.fb.group({
            code: this.fb.control(key.slice(-2), Validators.required),
            text: this.fb.control(args.rowData[key], Validators.required),
          })
        );
      });
    return newFormArray;
  }

  private saveTranslations(formArray: FormArray | undefined): void {
    if (!formArray || !this.currentRowData) return;

    const translations = formArray?.value;
    const initialValues = JSON.parse(JSON.stringify(this.currentRowData));
    const columnName = this.currentColumnName.slice(0, -2);

    translations.forEach((translation) => {
      const field = `${columnName}${translation.code.toUpperCase()}`;
      this.currentRowData[field] = translation.text;
    });

    this.currentRowData['names'] = Object.keys(this.currentRowData)
      .filter((key) => key.startsWith('name') && key !== 'name' && !!this.currentRowData[key])
      .map((key) => ({ code: key.slice(-2), text: this.currentRowData[key] }));

    this.currentRowData['descriptions'] = Object.keys(this.currentRowData)
      .filter(
        (key) =>
          key.startsWith('description') && key !== 'description' && !!this.currentRowData[key]
      )
      .map((key) => ({ code: key.slice(-2), text: this.currentRowData[key] }));

    this.currentRowData['attachedWcmPillars'] = this.selectedWcmPillarIds;
    this.currentRowData['attachedStrategicalAxises'] = this.selectedStrategicalAxisIds;

    const allHaveValues = translations.every((translation) => translation.text);
    if (JSON.stringify(initialValues) !== JSON.stringify(this.currentRowData) && allHaveValues) {
      const rowIndex = this.grid.getRowIndexByPrimaryKey(this.currentRowData.id);
      this.grid.updateRowValue(rowIndex, this.currentRowData);
    }

    this.nameDialogVisible = false;
    this.descriptionDialogVisible = false;
  }

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


public getAllData(state: any, query: Query) {
    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) => {
        if (state.dataSource) {
          state.dataSource((response as DataResult).result);
          return null;
        }
        return response as DataResult;
      });
  }
import { CrudOptions, DataOptions, DataResult, Query, UrlAdaptor } from '@syncfusion/ej2-data';

export class ProcessesAdaptor extends UrlAdaptor {
  processResponse(
    data: DataResult,
    ds?: DataOptions,
    query?: Query,
    xhr?: Request,
    request?: Object,
    changes?: CrudOptions
  ): DataResult {
    const items = data.result as any[];
    data.result = items.map((item) => ({
      ...item,
      wcmPillarNames: item.wcmPillars.map((pillar) => pillar.name).join(', '),
      strategicalAxisNames: item.strategicalAxises.map((axis) => axis.name).join(', '),
    }));
    return super.processResponse(data, ds, query, xhr, request, changes);
  }
}



NV Niels Van Goethem September 10, 2024 09:19 AM UTC

I have solved my issues, and applied some tweaks to your proposition so it's tailored to our needs. Thank you!



AR Aishwarya Rameshbabu Syncfusion Team September 11, 2024 07:42 PM UTC

Hi Niels Van Goethem,


We are happy to hear that the issue has been resolved. Please get back to us if you need any other assistance.


Regards

Aishwarya R


Loader.
Up arrow icon