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

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