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