javascriptangularprimengprimeng-datatable

Primeng dataView pagination and lazy load


I have an api that uses pagination to get products from a product database page of certain size and I want to implement a way to search trough the data with filters and display it in primeng dataview. Ideally what I would like is to set the products array as the value in dataview and when user searches for data it loads the first page and then when the user presses a new page in the pagination it get's loaded into the array from the api. if the user moves back the data should already be in the array and retrieved from it instead of the api.

The code bellow is my attempt at getting it to work, but using onPage and LazyLoad at the same time causes 2 requests to be sent to the api and it keeps retrieving the same chunk of data for some reason. I am pretty new to angular so any other tips would also be appreciated.

search component

export class SearchComponent implements OnInit {
  @ViewChild('dataView') dataView: any;
  products: Product[] = []; // Array to store the retrieved products
  virtualProducts: Product[] = [];
  selectedSize = 40; // Number of products per page
  totalRecords = 0; // Total number of products
  selectedPage = 0; // Current page number
  name?: string; // Name search parameter, adjust the type as per your requirement
  category = this.route.snapshot.paramMap.get('category'); // Category search parameter, adjust the type as per your requirement
  subcategory?: string; // Subcategory search parameter, adjust the type as per your requirement
  selectedBrands: string[] = []; // Brands search parameter, initialized as an empty array
  priceRange: number[] = [0, 4000]; // Price range search parameter, adjust the type as per your requirement
  subcategories: string[] = [];
  brands: string[] = [];
  sizeOptions: number[] = [5, 20, 40, 60, 80, 100, 200, 500];
  selectedPriceRange: number[] = [20, 1000];
  first = 0;
  loading: boolean = false;
  oldSearch: any;

  constructor(
    private categoryService: CategoryService,
    private brandService: BrandService,
    private productService: ProductService,
    private route: ActivatedRoute,
    private titleService: Title
  ) {
  }

  ngOnInit(): void {
    this.updateFilterValues();
    if(this.category)
    this.titleService.setTitle(this.category);
  }

  search(): void {
    if(this.category){
      const newSearch = {
        size: this.selectedSize,
        page: this.selectedPage,
        category: this.category,
        subcategory: this.subcategory,
        brands: this.selectedBrands,
        priceRange: this.selectedPriceRange,
        name: this.name
      }
      if(newSearch !== this.oldSearch) {
        console.log("search")
        this.productService
          .searchProducts(newSearch)
          .subscribe((value) => {
            this.products.push(...value.products)
            this.totalRecords = value.total_products;
            console.log(this.products)
            console.log(this.totalRecords)
            this.oldSearch = newSearch
          });
      }

    }

    console.log(this.name)
    console.log(this.category)
    console.log(this.subcategory)
    console.log(this.selectedBrands)
    console.log(this.selectedPriceRange)
    console.log(this.selectedSize)
    console.log(this.first)
    console.log(this.selectedPage)
  }

  onPageChange(event: any) {
    this.loading = true;
    this.first= (<number>event.first)
    this.selectedSize= (<number>event.rows)
    this.loading = false
  }

  loadProducts(event: any) {
    this.loading = true;
    this.first= (<number>event.first)
    this.selectedSize= (<number>event.rows)
    this.selectedPage= Math.floor(this.first / this.selectedSize);
    console.log(this.first)
    console.log(this.selectedSize)
    console.log(this.selectedPage)
    if(this.first >= this.products.length){
      this.search()
      console.log("before")
      console.log(this.virtualProducts)
      setTimeout(() => {
        let loadedProducts = this.products.slice(this.first, this.first + this.selectedSize);
        this.virtualProducts.splice(this.first,0,...loadedProducts);
        console.log("after")
        console.log(this.virtualProducts)
        event.forceUpdate
        this.loading = false;
      }, 1000);
    }else{
      event.forceUpdate
      this.loading = false
    }


  }

  private fetchCategories(category: string) {
    this.categoryService
      .getSubcategoriesByCategoryName(category)
      .subscribe((categories: CategoryDto[]) => {
        this.subcategories = categories.map((category: CategoryDto) => category.name);
      });
  }

  private fetchBrands(category: string) {
    this.brandService.getBrandsByCategoryName(category).subscribe((brands: BrandDto[]) => {
      this.brands = brands.map((brand: BrandDto) => brand.name);
    });
  }

  updateFilterValues(): void {
    if(this.category){
      this.fetchCategories(this.category);
      this.fetchBrands(this.category);
    }
  }
}

search component html

<div class="flex-container">
  <div class="flex-item">
    <div class="p-inputgroup">
      <input type="text" class="p-inputtext" placeholder="Search" [(ngModel)]="name">
      <button pButton pRipple class="pi" (click)="search()"><i class="pi pi-search"></i></button>
    </div>
  </div>
  <p-divider></p-divider>
  <div class="card flex">
    <span>Price Range: {{selectedPriceRange[0]}} - {{selectedPriceRange[1]}}</span><br><br>
    <p-slider [(ngModel)]="selectedPriceRange" [range]="true" [min]="priceRange[0]" [max]="priceRange[1]"
              aria-label="label_number"></p-slider>
  </div>
  <p-divider></p-divider>
  <div class="flex-item">
    <div>
      <p-dropdown [options]="subcategories" [(ngModel)]="subcategory" placeholder="Choose a Category"></p-dropdown>
    </div>
    <div>
      <p-multiSelect [options]="brands" [(ngModel)]="selectedBrands" placeholder="Choose Brands"></p-multiSelect>
    </div>
  </div>
</div>
<p-divider></p-divider>
<p-dataView [value]="virtualProducts"
            [first]="first"
            [totalRecords]="totalRecords"
            [rows]="selectedSize"
            [rowsPerPageOptions]="sizeOptions"
            [paginator]="true"
            [lazy]="true"
            (onLazyLoad)="loadProducts($event)" (onPage)="onPageChange($event)">
  <ng-template let-product pTemplate="listItem">
    <div class="col-12">
      <div class="flex flex-column xl-flex-row xl-align-items-start p-4 gap-4">
        <div
          class="flex flex-column sm-flex-row justify-content-between align-items-center xl-align-items-start flex-1 gap-4">
          <div class="flex flex-column align-items-center sm-align-items-start gap-3">
            <a [routerLink]="['/product', product.id]">
              <div class="text-2xl font-bold text-900">{{ product.name }}</div>
            </a>
            <div class="flex align-items-center gap-3">
              <span class="flex align-items-center gap-2">
                <i class="pi pi-tag"></i>
                <span class="font-semibold">{{ product.subcategory }}</span>
              </span>
            </div>
          </div>
          <div class="flex sm-flex-column align-items-center sm-align-items-end gap-3 sm-gap-2">
            <span class="text-2xl font-semibold">{{ product.price + '€' }}</span>
            <button class="p-button p-button-rounded" [disabled]="product.stock === 0">
              <i class="pi pi-shopping-cart"></i>
            </button>
          </div>
        </div>
      </div>
    </div>
  </ng-template>
</p-dataView>

pagable products interface

export interface PageableProducts {
  products:Product[];
  total_pages:number;
  total_products:number;
}

product interface

export interface Product {
  id?:string;
  brand:string;
  name:string;
  price:number
  category:string;
  subcategory:string;
  stock:number;
  description:string;
  image_url:string;
}

product service

import { Injectable } from '@angular/core';
import {HttpClient, HttpHeaders, HttpParams} from "@angular/common/http";
import {catchError, Observable, of, tap} from "rxjs";
import {PageableProducts} from "../../interface/product/pagable-products";
import {Product} from "../../interface/product/product";
import {Search} from "../../interface/search/search";

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private productUrl:String = 'http://localhost:8080/product/api';
  httpOptions = {
    headers: new HttpHeaders({'Content-Type': 'application/json'})
  };
  constructor(private http:HttpClient) { }

  searchProducts(searchQuery:Search): Observable<PageableProducts> {
    let queryParams = new HttpParams();
    if(searchQuery){
      if(searchQuery.name != null && searchQuery.name.length > 0) queryParams = queryParams.append("name",searchQuery.name)
      if(searchQuery.category != null && searchQuery.category.length > 0) queryParams = queryParams.append("category",searchQuery.category)
      if(searchQuery.subcategory != null && searchQuery.subcategory.length > 0) queryParams = queryParams.append("subcategory",searchQuery.subcategory)
      if(searchQuery.brands != null && searchQuery.brands.length > 0) queryParams = queryParams.append("brands",searchQuery.brands.toString())
      if(searchQuery.priceRange != null && searchQuery.priceRange.length == 2) {
        queryParams = queryParams.append("pMin",searchQuery.priceRange[0])
        queryParams = queryParams.append("pMax",searchQuery.priceRange[1])
      }

      if(searchQuery.page != null && searchQuery.page >= 0) queryParams = queryParams.append("page",searchQuery.page)
      if(searchQuery.size != null && searchQuery.size > 0) queryParams = queryParams.append("size",searchQuery.size)
    }

    console.log(queryParams.toString())

    return this.http.get<PageableProducts>(`${this.productUrl}/public/search`,{params:queryParams}).pipe(
        tap(_ => {
          console.log(`fetched products`)

        }),
        catchError(this.handleError<PageableProducts>('getProducts', { products:[], total_pages:0,
          total_products:0}))
    );
  }

  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(operation);
      console.error(error);
      return of(result as T);
    };
  }




}

Solution

  • I FIGURED IT OUT. In short I used a json object and set the keys to the page number and the value is the list of products. This way if a user changes the page it checks if the object has it and only queries the api if it doesn't have the page.

    Here is the code:

    search component:

    export class SearchComponent implements OnInit {
      virtualProducts: Product[] = [];
      selectedSize = 40;
      totalRecords = 0; // Total number of products
      selectedPage = 0; // Current page number
      name?: string; // Name search parameter, adjust the type as per your requirement
      category: string = (<string>this.route.snapshot.paramMap.get('category')); // Category search parameter, adjust the type as per your requirement
      subcategory?: string; // Subcategory search parameter, adjust the type as per your requirement
      selectedBrands: string[] = []; // Brands search parameter, initialized as an empty array
      priceRange: number[] = [0, 4000]; // Price range search parameter, adjust the type as per your requirement
      subcategories: string[] = [];
      brands: string[] = [];
      sizeOptions: number[] = [5, 20, 40, 60, 80, 100, 200, 500];
      selectedPriceRange: number[] = [20, 1000];
      first = 0;
      loading: boolean = false;
      pagesItems?: any
    
      constructor(
        private categoryService: CategoryService,
        private brandService: BrandService,
        private productService: ProductService,
        private route: ActivatedRoute,
        private titleService: Title,
        private storageService: StorageService
      ) {
      }
    
      ngOnInit(): void {
        console.log("oninit")
        this.route.params.subscribe(params => {
          this.category = params['category']
          this.updateValues();
          if (this.category)
            this.titleService.setTitle(this.category);
          const initialSearchParams: Search = {
            size: this.selectedSize,
            page: this.selectedPage,
            category: this.category,
            subcategory: this.subcategory,
            brands: this.selectedBrands,
            priceRange: this.selectedPriceRange,
            name: this.name
          }
    
          this.search(initialSearchParams)
          setTimeout(() => {
            console.log('pagesItems:', this.pagesItems[this.selectedPage]);
            this.virtualProducts = [...this.pagesItems[this.selectedPage]]
            console.log('virtualProducts:', this.virtualProducts);
            console.log(`virtualProducts length: ${this.virtualProducts.length}`);
          }, 2000);
        });
    
      }
    
      search(searchParams: Search): void {
        console.log("search")
    
        this.productService
          .searchProducts(searchParams)
          .subscribe((value) => {
            this.pagesItems['searchParams'] = {
              category: searchParams.category,
              subcategory: searchParams.subcategory,
              brands: searchParams.brands,
              priceRange: searchParams.priceRange,
              name: searchParams.name
            }
            this.pagesItems['size'] = this.selectedSize
            this.pagesItems['page'] = this.selectedPage
            console.log('pagable products: ', value.products)
            this.pagesItems[this.selectedPage] = [...value.products]
            this.totalRecords = value.total_products;
            console.log(this.totalRecords)
          });
    
        console.log(`name: ${this.name}`)
        console.log(`category: ${this.category}`)
        console.log(`subcategory: ${this.subcategory}`)
        console.log(`brands: ${this.selectedBrands}`)
        console.log(`Price range: ${this.selectedPriceRange[0]} - ${this.selectedPriceRange[1]}`)
        console.log(`Size: ${this.selectedSize}`)
        console.log(`first: ${this.first}`)
        console.log(`Page: ${this.selectedPage}`)
      }
    
    
      loadProducts(event: any, isButtonClicked: boolean) {
        this.loading=true
        console.log("loadProducts")
        this.first = event.first;
        this.selectedSize = event.rows;
        this.selectedPage = Math.floor(this.first / this.selectedSize);
        console.log(`Size: ${this.selectedSize}`);
        console.log(`first: ${this.first}`);
        console.log(`Page: ${this.selectedPage + 1}`);
        let searchParams: Search = {
          size: this.selectedSize,
          page: this.selectedPage,
          category: this.category,
          subcategory: this.subcategory,
          brands: this.selectedBrands,
          priceRange: this.selectedPriceRange,
          name: this.name
        }
    
        if (!this.pagesItems || isButtonClicked || (this.pagesItems && !this.isPageChange(searchParams))) {
          this.pagesItems = {}
        }
    
        if (this.pagesItems[this.selectedPage] == undefined) {
          if (this.pagesItems && this.isPageChange(searchParams)) searchParams = {
            size: searchParams.size,
            page: searchParams.page,
            priceRange: this.pagesItems['searchParams'].priceRange,
            brands: this.pagesItems['searchParams'].brands,
            subcategory: this.pagesItems['searchParams'].subcategory,
            name: this.pagesItems['searchParams'].name,
            category: this.pagesItems['searchParams'].category
          }
    
          this.search(searchParams);
          setTimeout(() => {
            console.log('pagesItems:', this.pagesItems[this.selectedPage]);
            this.virtualProducts = [...this.pagesItems[this.selectedPage]]
            event.forceUpdate = true;
            this.loading=false
          }, 1000);
        } else {
    
          this.virtualProducts = [...this.pagesItems[this.selectedPage]]
          event.forceUpdate = true;
          this.loading=false
        }
    
    
      }
    
      isPageChange(searchParams:Search):boolean {
    
        return (searchParams.page != this.pagesItems['page']) &&
          searchParams.name == this.pagesItems['searchParams']?.name &&
          searchParams.size == this.pagesItems['size'] &&
          searchParams.category == this.pagesItems['searchParams'].category &&
          searchParams.subcategory == this.pagesItems['searchParams']?.subcategory &&
          searchParams.brands == this.pagesItems['searchParams']?.brands &&
          searchParams.priceRange == this.pagesItems['searchParams'].priceRange
      }
    
      private fetchCategories(category: string) {
        console.log("fetch categories")
        this.categoryService
          .getSubcategoriesByCategoryName(category)
          .subscribe((categories: CategoryDto[]) => {
            this.subcategories = categories.map((category: CategoryDto) => category.name);
          });
      }
    
      private fetchBrands(category: string) {
        console.log("fetch brands")
        this.brandService.getBrandsByCategoryName(category).subscribe((brands: BrandDto[]) => {
          this.brands = brands.map((brand: BrandDto) => brand.name);
        });
      }
    
      updateValues(): void {
        if (this.category) {
          this.fetchCategories(this.category);
          this.fetchBrands(this.category);
        }
      }
    
      addToCart(productId: string) {
        this.storageService.addToCart(productId)
        console.log('Cart:', this.storageService.getCart())
      }
    
      OnSearchButtonClick() {
        this.first = 0
        this.loadProducts({first: this.first, rows: this.selectedSize, forceUpdate: true}, true);
      }
    }
    

    html:

    <div class="flex-container">
      <div class="flex-item">
        <div class="p-inputgroup">
          <input type="text" class="p-inputtext" placeholder="Search" [(ngModel)]="name">
          <button pButton pRipple class="pi" (click)="OnSearchButtonClick()"><i class="pi pi-search" ></i></button>
        </div>
      </div>
      <p-divider></p-divider>
      <div class="card flex">
        <span>Price Range: {{selectedPriceRange[0]}} - {{selectedPriceRange[1]}}</span><br><br>
        <p-slider [(ngModel)]="selectedPriceRange" [range]="true" [min]="priceRange[0]" [max]="priceRange[1]"
                  aria-label="label_number"></p-slider>
      </div>
      <p-divider></p-divider>
      <div class="flex-item">
        <div>
          <p-dropdown [options]="subcategories" [(ngModel)]="subcategory" placeholder="Choose a Category"></p-dropdown>
        </div>
        <div>
          <p-multiSelect [options]="brands" [(ngModel)]="selectedBrands" placeholder="Choose Brands"></p-multiSelect>
        </div>
      </div>
    </div>
    <p-divider></p-divider>
    <p-dataView [value]="virtualProducts"
                [first]="first"
                [totalRecords]="totalRecords"
                [rows]="selectedSize"
                [rowsPerPageOptions]="sizeOptions"
                [paginator]="true"
                [pageLinks]="5"
                [lazy]="true"
                [loading]="loading"
                (onLazyLoad)="loadProducts($event,false)">
      <ng-template let-product pTemplate="listItem">
        <div class="col-12">
          <div class="flex flex-column xl-flex-row xl-align-items-start p-4 gap-4">
            <div
              class="flex flex-column sm-flex-row justify-content-between align-items-center xl-align-items-start flex-1 gap-4">
              <div class="flex flex-column align-items-center sm-align-items-start gap-3">
                <a [routerLink]="['/product', product.id]">
                  <div class="text-2xl font-bold text-900">{{ product.name }}</div>
                </a>
                <div class="flex align-items-center gap-3">
                  <span class="flex align-items-center gap-2">
                    <i class="pi pi-tag"></i>
                    <span class="font-semibold">{{ product.subcategory }}</span>
                  </span>
                </div>
              </div>
              <div class="flex sm-flex-column align-items-center sm-align-items-end gap-3 sm-gap-2">
                <span class="text-2xl font-semibold">{{ product.price + '€' }}</span>
                <button class="p-button p-button-rounded" [disabled]="product.stock === 0" (click)="addToCart(product.id)">
                  <i class="pi pi-shopping-cart"></i>
                </button>
              </div>
            </div>
          </div>
        </div>
      </ng-template>
    </p-dataView>