Vaadin Grid Search Not Updating Until Multi-Sort is Triggered

Hi everyone,

I have implemented a search functionality for my Vaadin Grid angular application, but I’m facing an issue where the grid does not re-render when typing in the search field. However, when I use the multi-sort arrows to sort the grid, the search functionality starts working. Please suggest corrections in my code.

here is my code:


import { Component, CUSTOM_ELEMENTS_SCHEMA, OnInit, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { GridDataProviderCallback, GridDataProviderParams } from '@vaadin/grid';

import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-column-group.js';
import '@vaadin/grid/vaadin-grid-filter-column.js';
import '@vaadin/grid/vaadin-grid-selection-column.js';
import '@vaadin/grid/vaadin-grid-sort-column.js';
import '@vaadin/grid/vaadin-grid-tree-column.js';
import '@vaadin/text-field'
import '@vaadin/tooltip'
import { NgFor } from '@angular/common';
import { GridEventContext } from '@vaadin/grid';

@Component({
  selector: 'app-vaadin-grid',
  imports: [NgFor],
  templateUrl: './vaadin-grid.component.html',
  styleUrl: './vaadin-grid.component.css',
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class VaadinGridComponent implements OnInit {

  data: any[] = [];
  columnKeys: string[] = [];
  count: number = 0;
  filteredData: any[] = [];
  @ViewChild('grid') grid: any;

  constructor(private http: HttpClient) {}

  ngOnInit(): void {
    this.http.get<any>('/assets/response.json').subscribe({
      next: (response) => {
        this.data = response.TktDataList;

        if (this.data.length > 0) {
          this.columnKeys = Object.keys(this.data[0]);  // Get column names dynamically
          this.filteredData = [...this.data]; // Initially set filteredData to all data
        }
        this.count = this.filteredData.length;
      },
      error: (error) => {
        console.error('Error loading JSON data:', error);
      }
    });
  }


  dataProvider = (
    params: GridDataProviderParams<any>,
    callback: GridDataProviderCallback<any>
  ) => {
    const { page, pageSize, sortOrders } = params;
    const startIndex = page * pageSize;
    const endIndex = startIndex + pageSize;
  
  //  console.log('Sort Orders:', sortOrders); // Log sorting parameters
  
    // Simulate async data fetching
    setTimeout(() => {
      let chunk = this.filteredData.slice(startIndex, endIndex);
  
      // Apply sorting if sortOrders are provided
      if (sortOrders && sortOrders.length > 0) {
        chunk = this.sortData(chunk, sortOrders);
      }
  
      callback(chunk, this.filteredData.length); // Pass the chunk and total size
    }, 200); // Simulate network delay
  };

  sortData(data: any[], sortOrders: any[]): any[] {
    return data.sort((a, b) => {
      for (const order of sortOrders) {
        const { path, direction } = order;
        const aValue = a[path];
        const bValue = b[path];
  
        if (aValue < bValue) {
          return direction === 'asc' ? -1 : 1;
        }
        if (aValue > bValue) {
          return direction === 'asc' ? 1 : -1;
        }
      }
      return 0;
    });
  }

  onSearchChange(event: CustomEvent): void {

    const searchTerm = (event.detail.value || '').trim().toLowerCase();
    this.filteredData = this.data.filter(item =>
      this.columnKeys.some(key =>
        item[key]?.toString().toLowerCase().includes(searchTerm)
    ))
    this.count = this.filteredData.length;  // Update the count based on filtered data
    this.refreshGrid(); // Refresh the grid
  }

  refreshGrid() {
    if (this.grid) {
      this.grid.size = this.filteredData.length; // Set the total number of items
      this.grid.dataProvider = this.dataProvider; // Reassign the data provider
    }
  }
  

  tooltipGenerator = (context: GridEventContext<any>): string => {
    let text = '';

    const { column, item } = context;
    if (column && item) {
      switch (column.path) {
        case 'TK_AIR_XO':
          text = 'random text';
          break;
        default:
          break;
      }
    }

    return text;
  };
}

html code:

<vaadin-text-field placeholder="Search" (value-changed)="onSearchChange($event)" >
</vaadin-text-field>

<vaadin-grid #grid [dataProvider]="dataProvider" multi-sort column-reordering-allowed multi-sort-priority="append">
  <vaadin-grid-selection-column />
  <ng-container *ngFor="let key of columnKeys">
    <vaadin-grid-sort-column [path]="key" header="{{key}}"></vaadin-grid-sort-column>
  </ng-container>

  <vaadin-tooltip slot="tooltip" [generator]="tooltipGenerator"/>
</vaadin-grid>

<div>
  <h2>count : {{count}}</h2>
</div>

this.grid.dataProvider = this.dataProvider; doesn’t do anything, because it’s not assigning a new reference. You probably want this: https://cdn.vaadin.com/vaadin-web-components/24.7.0-alpha8/#/elements/vaadin-grid#method-clearCache.

Since you preload all data initially you could also try using the items API instead of creating a data provider. With that you would just assign filteredData to items whenever it changes.

2 Likes

If I remove the data provider, the lazy loading functionality that I implemented will also be removed. However, I need both search functionality and lazy loading to work together.

What changes should I make to ensure that the grid updates correctly while keeping lazy loading intact? Any suggestions would be really helpful! :blush:

it would be helpful if anyone could explain me what changes should i do to my code

Also i want to implement context menu in my angular app but it is giving errors despite i am referring to documentation provided by @sissbruecker :

here is my code:

html:

<vaadin-context-menu #contextMenu [items]="menuItems" (item-selected)="onItemSelected($event)">
  <vaadin-grid #grid [dataProvider]="dataProvider" multi-sort column-reordering-allowed multi-sort-priority="append">
    <vaadin-grid-selection-column />
    <ng-container *ngFor="let key of columnKeys">
      <vaadin-grid-sort-column [path]="key" header="{{key}}"></vaadin-grid-sort-column>
    </ng-container>
    <vaadin-tooltip slot="tooltip" [generator]="tooltipGenerator"/>
  </vaadin-grid>
</vaadin-context-menu>

typescript code:

import { AfterViewInit, Component, CUSTOM_ELEMENTS_SCHEMA, OnInit, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { GridDataProviderCallback, GridDataProviderParams } from '@vaadin/grid';

import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-column-group.js';
import '@vaadin/grid/vaadin-grid-filter-column.js';
import '@vaadin/grid/vaadin-grid-selection-column.js';
import '@vaadin/grid/vaadin-grid-sort-column.js';
import '@vaadin/grid/vaadin-grid-tree-column.js';
import '@vaadin/text-field'
import '@vaadin/tooltip'
import '@vaadin/context-menu';
import { NgFor } from '@angular/common';
import { GridEventContext } from '@vaadin/grid';

@Component({
  selector: 'app-vaadin-grid',
  imports: [NgFor],
  templateUrl: './vaadin-grid.component.html',
  styleUrl: './vaadin-grid.component.css',
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class VaadinGridComponent implements OnInit, AfterViewInit {

  data: any[] = [];
  columnKeys: string[] = [];
  count: number = 0;
  filteredData: any[] = [];
  @ViewChild('grid', { static: false }) grid: any;

  @ViewChild('contextMenu', { static: false }) contextMenu: any;
  selectedItem: any = null;

  constructor(private http: HttpClient) {}

  menuItems = [
    {
      text: 'View Details',
      theme: 'primary',
      children: [
        { text: 'Full Information', checked: true },
        { text: 'Summary' }
      ]
    },
    { component: 'hr' },
    {
      text: 'Actions',
      children: [
        { text: 'Edit' },
        { text: 'Delete', theme: 'error' }
      ]
    },
    { text: 'Export', disabled: false }
  ];

  ngOnInit(): void {
    this.http.get<any>('/assets/response.json').subscribe({
      next: (response) => {
        this.data = response.TktDataList;

        if (this.data.length > 0) {
          this.columnKeys = Object.keys(this.data[0]);  // Get column names dynamically
          this.filteredData = [...this.data]; // Initially set filteredData to all data
        }
        this.count = this.filteredData.length;
      },
      error: (error) => {
        console.error('Error loading JSON data:', error);
      }
    });
  }

  ngAfterViewInit(): void {
    if (this.grid) {
      this.grid.addEventListener('vaadin-contextmenu', (event: CustomEvent) => {
        event.preventDefault();
        this.selectedItem = event.detail.context.item;
        this.contextMenu.open(event);
      });
    }
  }

  onItemSelected(event: CustomEvent) {
    const selectedItem = event.detail.value;
    console.log(`${selectedItem.text} selected for item:`, this.selectedItem);

    // Handle the selected menu item
    switch (selectedItem.text) {
      case 'Full Information':
        // Handle full information action
        break;
      case 'Summary':
        // Handle summary action
        break;
      case 'Edit':
        // Handle edit action
        break;
      case 'Delete':
        // Handle delete action
        break;
      case 'Export':
        // Handle export action
        break;
      default:
        break;
    }
  }

  dataProvider = (
    params: GridDataProviderParams<any>,
    callback: GridDataProviderCallback<any>
  ) => {
    const { page, pageSize, sortOrders } = params;
    const startIndex = page * pageSize;
    const endIndex = startIndex + pageSize;

    // Simulate async data fetching
    setTimeout(() => {
      let chunk = this.filteredData.slice(startIndex, endIndex);

      // Apply sorting if sortOrders are provided
      if (sortOrders && sortOrders.length > 0) {
        chunk = this.sortData(chunk, sortOrders);
      }

      callback(chunk, this.filteredData.length); // Pass the chunk and total size
    }, 200); // Simulate network delay
  };

  sortData(data: any[], sortOrders: any[]): any[] {
    return data.sort((a, b) => {
      for (const order of sortOrders) {
        const { path, direction } = order;
        const aValue = a[path];
        const bValue = b[path];

        if (aValue < bValue) {
          return direction === 'asc' ? -1 : 1;
        }
        if (aValue > bValue) {
          return direction === 'asc' ? 1 : -1;
        }
      }
      return 0;
    });
  }

  onSearchChange(event: CustomEvent): void {
    const searchTerm = (event.detail.value || '').trim().toLowerCase();
    this.filteredData = this.data.filter(item =>
      this.columnKeys.some(key =>
        item[key]?.toString().toLowerCase().includes(searchTerm)
      ));
    this.count = this.filteredData.length;  // Update the count based on filtered data
    this.refreshGrid(); // Refresh the grid
  }

  refreshGrid() {
    if (this.grid) {
      this.grid.size = this.filteredData.length; // Set the total number of items
      this.grid.dataProvider = this.dataProvider; // Reassign the data provider
    }
  }

  tooltipGenerator = (context: GridEventContext<any>): string => {
    let text = '';

    const { column, item } = context;
    if (column && item) {
      switch (column.path) {
        case 'TK_AIR_XO':
          text = 'random text';
          break;
        default:
          break;
      }
    }

    return text;
  };
}