PROGRAMMING LANGUAGES

Angular Search and pagination with example – Tech Incent

In this article, I will implement pagination and search for large amounts of data. This pagination and search will be optimized with the debounce technique. I also use some of RxJs operators like debounceTime, distinctUntilChanged, switchMap, exhaustMapcand more. And I will use fake product pagination data API from StoreRestApi.

Explanation search feature

Our goal for the search is, there will be a search field that takes input value. When the user inputs some value after .5s from stop input values that will call API.

We must be aware of every input change API calling, For example, if the search term is “shirt”, we don’t want to call for every letter (s,h,i,r,t). Search should be called when the shirt word inputs complete. For such this problem we will use debounceTime operator. We will not call API if input value is not changed using distinctUntilChanged operator

Let’s declare searchControl as Angular reactive form FormControl for html input binding. We are going to use FormControl for input binding because it gives a wide range of input handling methods.

Declare searchTerm$ property as searchControl valueChanges that will return Observable of search value. It takes 4 RxJs operators.

  • The first takeUntil will handle the subscription until the component is destroyed. If you use angular 16+ version os you can use takeUntilDestroyed operator from @angular/core/rxjs-interop.
  • debounceTime will prevent every input change API, But it will call after 0.5 seconds from inputting the stop
  • distinctUntilChanged handles save value API call
  • startWith(“”) will do emit a first empty string to this Observable
searchControl = new FormControl<string>('');
private searchTerm$ = this.searchControl.valueChanges.pipe(
  takeUntil(this._destroy$),
  debounceTime(500),
  distinctUntilChanged(),
  startWith('')
)

Here are the HTML search elements. The input field takes searchControl as formControl

<div style="max-width: 300px;margin: 50px auto;">   
   <label for="id_search">Search: </label>
   <input [formControl]="searchControl" type="search" id="id_search" placeholder="Search title, description...">
</div>

Explanation Pagination

Approach: Our pagination has the next and preview page route. and pagination page item limit can be updated.

Declare paginationEvent$ as Subject. paginationEvent$ is an emitter to APi calling. when the pagination page or limit change paginationEvent$ will next which is emitter. The setPage method updates the page number and emits a signal when a user requests a new page. The setItemLen method update the page limit

// Pagination Emitter to apu
private paginationEvent$ = new Subject<void>();

// page event handler
setPage(pageNumber: number): void 
  this.queryParams.page = pageNumber;
  this.paginationEvent$.next();


// Paination page item length handler
setItemLen(e: any): void 
  this.queryParams.page = 1;
  this.queryParams.limit = e.target?.value;
  this.paginationEvent$.next();

Html pagination element

<section style="display: flex;align-items: center;justify-content: center;margin-top: 1rem;gap: 16px;">
  <div>
    <label for="id_item_end">
      Item per page: 
      <select id="id_item_end" (change)="setItemLen($event)" [value]="queryParams.limit">
        <option [value]="6">6</option>
        <option [value]="9">9</option>
        <option [value]="12">12</option>
        <option [value]="21">21</option>
        <option [value]="42">42</option>
      </select>
    </label>
  </div>
  <div>
    <p>Current Page: queryParams.page</p>
  </div>
  <div style="display: flex;gap: 5px;">
    <button style="cursor: pointer;" [disabled]="queryParams.page === 1" (click)="setPage((queryParams.page || 1) - 1)" type="button">Prev</button>
    <button style="cursor: pointer;" [disabled]="isLastPage" (click)="setPage((queryParams.page || 1) + 1)" type="button">Next</button>
  </div>
</section>

Let’s go to proper example scenarios

Creating new project

new new angular-search-and-pagination

Generate product module and product service and productList component

ng g m product --routing
ng g c product/productList
ng g s product/product

In the app-routing.module.ts file

...
const routes: Routes = [
  
    path: '',
    pathMatch: 'full',
    loadChildren: () => import('./product/product.module').then(m => m.ProductModule)
  
];
...

Routing product list component, In the product-routing.module.ts file

...
const routes: Routes = [
  
    path: '',
    component: ProductListComponent
  
];
...

In the app.component.html file

<router-outlet></router-outlet>

Add HttpClientModule and ReactiveFormModule in the product.module.ts file

import  NgModule  from '@angular/core';
import  CommonModule  from '@angular/common';

import  ProductRoutingModule  from './product-routing.module';
import  ProductListComponent  from './product-list/product-list.component';
import  HttpClientModule  from '@angular/common/http';
import  ReactiveFormsModule  from '@angular/forms';

@NgModule(
  declarations: [
    ProductListComponent
  ],
  imports: [
    CommonModule,
    ProductRoutingModule,
    
    HttpClientModule,
    ReactiveFormsModule
  ]
)
export class ProductModule  

Implement product.service.ts

We are going to use StoreRestApi products API. That API provides search and pagination features also. Let’s define QueryParam type for safety. Then define the getProducts method which returns httpClient observable of products response. getProducts method takes Partial queryParam.

import  Injectable  from '@angular/core';
import  HttpClient from '@angular/common/http';
import  Observable  from 'rxjs';

export type QueryParam = 
  q: string,
  page: number,
  limit: number


@Injectable(
  providedIn: 'root'
)
export class ProductService 
  readonly baseUrl="https://api.storerestapi.com";

  constructor(private httpClient: HttpClient) 

  getProducts(queryParams?: Partial<QueryParam>): Observable<any> 
    const httoOptions = 
      params: queryParams
    
    return this.httpClient.get(this.baseUrl + '/products', httoOptions)
  

Final product.component.ts file

Now we have queryParams, queryParams store page number, item per page as item, search term as q. We also have searchTerm$ Observable<string> of search, paginationEvent$ subject which is pagination emitter. Now it can merge into a single subscription and call productApi.

keep SearchTerm$ upper level, So if any search input changes, it’s triggered from beginning. Then switchMap paginationEvent$, this will trigger pagination event, You notice, I am using switchMap here because when multiple search events trigger, this switchMap will cancel the preview one. Then inside the paginationEvent$ exhaustMap productList Api, Here is used exhaustMap because it will complete one request than go next request

import  Component, Inject, OnDestroy, OnInit  from '@angular/core';
// import  takeUntilDestroyed  from '@angular/core/rxjs-interop';
import  ProductService, QueryParam  from '../product.service';
import  Subject, debounceTime, distinctUntilChanged, exhaustMap, startWith, switchMap, takeUntil  from 'rxjs';
import  FormControl  from '@angular/forms';

@Component(
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.scss']
)
export class ProductListComponent implements OnInit, OnDestroy {
  private _destroy$ = new Subject<void>()
  products: any[] = [];
  isLoading: boolean = true;
  isLoaded: boolean = false;
  isLastPage: boolean = false;

  queryParams: Partial<QueryParam> = 
    page: 1,
    limit: 9
  

  searchControl = new FormControl<string>('');
  private searchTerm$ = this.searchControl.valueChanges.pipe(
    takeUntil(this._destroy$),
    debounceTime(500),
    distinctUntilChanged(),
    startWith('')
  )

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

  constructor(private productService: ProductService) 

  ngOnInit(): void 
    // Subscription to products
    this.searchTerm$
      .pipe(
        switchMap((searchTerm: string 

  ngOnDestroy(): void 
    this._destroy$.next();
    this._destroy$.subscribe();
  

  setPage(pageNumber: number): void 
    this.queryParams.page = pageNumber;
    this.paginationEvent$.next();
  

  setItemLen(e: any): void 
    this.queryParams.page = 1;
    this.queryParams.limit = e.target?.value;
    this.paginationEvent$.next();
  
}

Final product.component.html file

<section class="product_area">
  <div style="max-width: 1200px;margin: 50px auto;">
    <div style="max-width: 300px;margin: 50px auto;">   
      <label for="id_search">Search: </label>
      <input [formControl]="searchControl" type="search" id="id_search" placeholder="Search title, description...">
    </div>

    <!-- Products -->
    <div *ngIf="!isLoaded && isLoading;else productContainerTemplate">
      Loading ...
    </div>
  </div>
  
  <ng-template #productContainerTemplate>
    <div style="display: grid;grid-template-columns: repeat(3, minmax(0, 1fr));gap: 16px;">
      <div *ngFor="let product of products" style="border: 1px solid gray; border-radius: 4px; padding: 1rem;">
        <div class="p-5">
            <a [routerLink]="['/products', product.slug]">
              <h3 style="margin-top: 0;">product.title</h3>
            </a>
            <p class="mb-3 font-normal text-gray-700 dark:text-gray-400">Here are the biggest enterprise technology acquisitions of 2021 so far, in reverse chronological order.</p>
            <a [routerLink]="['/products', product.slug]" style="display: inline-flex;align-items: center; gap: 12px;">
                Read more
                <svg style="width: 15px;height: 15px;" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
                    <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
                </svg>
            </a>
        </div>
      </div>
    </div>

    <div *ngIf="isLoading" style="display: flex;justify-content: center;margin-top: 20px;">
      Loading more...
    </div>

<section style="display: flex;align-items: center;justify-content: center;margin-top: 1rem;gap: 16px;">
  <div>
    <label for="id_item_end">
      Item per page: 
      <select id="id_item_end" (change)="setItemLen($event)" [value]="queryParams.limit">
        <option [value]="6">6</option>
        <option [value]="9">9</option>
        <option [value]="12">12</option>
        <option [value]="21">21</option>
        <option [value]="42">42</option>
      </select>
    </label>
  </div>
  <div>
    <p>Current Page: queryParams.page</p>
  </div>
  <div style="display: flex;gap: 5px;">
    <button style="cursor: pointer;" [disabled]="queryParams.page === 1" (click)="setPage((queryParams.page || 1) - 1)" type="button">Prev</button>
    <button style="cursor: pointer;" [disabled]="isLastPage" (click)="setPage((queryParams.page || 1) + 1)" type="button">Next</button>
  </div>
</section>
  </ng-template>
</section>

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button