Simplifying Pagination in Angular with a Reusable Base Component

Our objective is to abstract the repetitive aspects of pagination—such as tracking the current page, page size, total items, and loading state—into a base component. This base component will provide a standardized way to handle pagination, which can be extended by other components to implement their specific data-fetching logic. Creating the BasePaginationComponent Let's define a generic BasePaginationComponent class that handles the core pagination functionality:​ import { finalize } from 'rxjs/operators'; import { Observable } from 'rxjs'; export abstract class BasePaginationComponent { data: T[] = []; currentPage = 1; pageSize = 10; totalItems = 0; isLoading = false; // Abstract method to fetch data; must be implemented by subclasses protected abstract fetchData(page: number, size: number): Observable; // Method to load data for a specific page loadPage(page: number): void { this.isLoading = true; this.fetchData(page, this.pageSize) .pipe(finalize(() => (this.isLoading = false))) .subscribe(response => { this.data = response.items; this.totalItems = response.total; this.currentPage = page; }); } // Method to handle page change events (e.g., from a paginator component) onPageChange(page: number): void { this.loadPage(page); } } In this base component:​ data: Holds the current page's data items.​ currentPage: Tracks the current page number.​ pageSize: Defines the number of items per page.​ totalItems: Stores the total number of items across all pages.​ isLoading: Indicates whether a data fetch operation is in progress.​ fetchData: An abstract method that must be implemented by subclasses to fetch data for a given page and page size.​ loadPage: Handles the logic for loading data for a specific page.​ onPageChange: A helper method to be called when the page changes, which in turn calls loadPage.​ Implementing the PaginationControlsComponent import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; @Component({ selector: 'app-pagination-controls', templateUrl: './pagination-controls.component.html' }) export class PaginationControlsComponent implements OnChanges { @Input() currentPage: number = 1; @Input() totalItems: number = 0; @Input() pageSize: number = 10; @Output() pageChange: EventEmitter = new EventEmitter(); totalPages: number = 0; pages: number[] = []; ngOnChanges(changes: SimpleChanges): void { this.totalPages = Math.ceil(this.totalItems / this.pageSize); this.pages = Array.from({ length: this.totalPages }, (_, i) => i + 1); } changePage(page: number): void { if (page >= 1 && page

Apr 14, 2025 - 22:11
 0
Simplifying Pagination in Angular with a Reusable Base Component

Our objective is to abstract the repetitive aspects of pagination—such as tracking the current page, page size, total items, and loading state—into a base component. This base component will provide a standardized way to handle pagination, which can be extended by other components to implement their specific data-fetching logic.

Creating the BasePaginationComponent
Let's define a generic BasePaginationComponent class that handles the core pagination functionality:​

import { finalize } from 'rxjs/operators';
import { Observable } from 'rxjs';

export abstract class BasePaginationComponent {
  data: T[] = [];
  currentPage = 1;
  pageSize = 10;
  totalItems = 0;
  isLoading = false;

  // Abstract method to fetch data; must be implemented by subclasses
  protected abstract fetchData(page: number, size: number): Observable<{ items: T[]; total: number }>;

  // Method to load data for a specific page
  loadPage(page: number): void {
    this.isLoading = true;
    this.fetchData(page, this.pageSize)
      .pipe(finalize(() => (this.isLoading = false)))
      .subscribe(response => {
        this.data = response.items;
        this.totalItems = response.total;
        this.currentPage = page;
      });
  }

  // Method to handle page change events (e.g., from a paginator component)
  onPageChange(page: number): void {
    this.loadPage(page);
  }
}

In this base component:​

  • data: Holds the current page's data items.​
  • currentPage: Tracks the current page number.​
  • pageSize: Defines the number of items per page.​
  • totalItems: Stores the total number of items across all pages.​
  • isLoading: Indicates whether a data fetch operation is in progress.​
  • fetchData: An abstract method that must be implemented by subclasses to fetch data for a given page and page size.​
  • loadPage: Handles the logic for loading data for a specific page.​
  • onPageChange: A helper method to be called when the page changes, which in turn calls loadPage.​

Implementing the PaginationControlsComponent

import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-pagination-controls',
  templateUrl: './pagination-controls.component.html'
})
export class PaginationControlsComponent implements OnChanges {
  @Input() currentPage: number = 1;
  @Input() totalItems: number = 0;
  @Input() pageSize: number = 10;
  @Output() pageChange: EventEmitter = new EventEmitter();

  totalPages: number = 0;
  pages: number[] = [];

  ngOnChanges(changes: SimpleChanges): void {
    this.totalPages = Math.ceil(this.totalItems / this.pageSize);
    this.pages = Array.from({ length: this.totalPages }, (_, i) => i + 1);
  }

  changePage(page: number): void {
    if (page >= 1 && page <= this.totalPages && page !== this.currentPage) {
      this.pageChange.emit(page);
    }
  }
}

Extending the Base Component
Now, let's create a component that extends BasePaginationComponent to display a list of users:​

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { BasePaginationComponent } from './base-pagination.component';
import { UserService } from './user.service';
import { User } from './user.model';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html'
})
export class UserListComponent extends BasePaginationComponent implements OnInit {
  constructor(private userService: UserService) {
    super();
  }

  ngOnInit(): void {
    this.loadPage(this.currentPage);
  }

  protected fetchData(page: number, size: number): Observable<{ items: User[]; total: number }> {
    return this.userService.getUsers(page, size);
  }
}


In this example:​

  • UserListComponent extends BasePaginationComponent, specifying User as the data type.​
  • The fetchData method is implemented to fetch users from the UserService.​
  • The template displays the list of users and includes a custom app-pagination-controls component to handle pagination UI.

Handling Edge Cases in Your Pagination System

While the basic implementation works well, a production-ready pagination system should address these common edge cases:

  • Large datasets: Display a limited range of pages (first, last, and a few around current) with ellipses instead of showing all page numbers
  • Navigation boundaries: Disable Previous/Next buttons at first/last pages, and hide pagination entirely for single-page results
  • Empty results: Provide user feedback when no items exist instead of showing empty pagination controls
  • Page size changes: Reset to first page when page size changes to ensure consistent user experience
  • URL integration: Consider syncing pagination state with URL parameters for bookmarking and sharing specific pages
  • Responsive design: Adapt controls for smaller screens with simplified mobile-friendly navigation
  • Accessibility: Implement keyboard navigation and proper ARIA labels for inclusive user experience
  • Error handling: Gracefully manage network failures during server-side pagination operations