This tutorial shows how to implement client-side paging and sorting of table data in Angular 14.
The example app contains a table with data for 150 test users split across 15 pages. The test data is fetched from a fake backend API that's implemented using an Angular HTTP intercepter. All columns are sortable ascending and descending by clicking the column header, by default the table is sorted by id asc.
The example paging and sorting app is styled with the CSS from Bootstrap 5.2, for more info about Bootstrap see https://getbootstrap.com/docs/5.2/getting-started/introduction/.
The following pagination logic is used which is the same as what you see in Google search results:
How the pagination controls appear for each page if there are 15 total pages:
[1] 2 3 4 5 6 7 8 9 10
1 [2] 3 4 5 6 7 8 9 10
1 2 [3] 4 5 6 7 8 9 10
1 2 3 [4] 5 6 7 8 9 10
1 2 3 4 [5] 6 7 8 9 10
1 2 3 4 5 [6] 7 8 9 10
2 3 4 5 6 [7] 8 9 10 11
3 4 5 6 7 [8] 9 10 11 12
4 5 6 7 8 [9] 10 11 12 13
5 6 7 8 9 [10] 11 12 13 14
6 7 8 9 10 [11] 12 13 14 15
6 7 8 9 10 11 [12] 13 14 15
6 7 8 9 10 11 12 [13] 14 15
6 7 8 9 10 11 12 13 [14] 15
6 7 8 9 10 11 12 13 14 [15]
NOTE: You can also start the app with the Angular CLI command ng serve --open . To do this first install the Angular CLI globally on your system with the command npm install -g @angular/cli .
The Angular CLI was used to generate the base project structure with the ng new command, the CLI is also used to build and serve the application. For more info about the Angular CLI see https://angular.io/cli.
The app and code structure of the tutorial mostly follow the best practice recommendations in the official Angular Style Guide, with a few of my own tweaks here and there.
Shared/common code (helpers & components) are placed in folders prefixed with an underscore _ to easily differentiate them from features and group them together at the top of the folder structure. This example doesn't have any routed features (e.g. home, products etc) but if it did they would each have their own folder in the /app directory.
The index.ts files in each folder are barrel files that group the exported modules from each folder together so they can be imported using only the folder path instead of the full module path, and to enable importing multiple modules in a single import (e.g. import < ComponentOne, ComponentTwo >from '../_components' ).
Here are the main project files that contain the application logic, I didn't include files that were generated by the Angular CLI ng new command that I left unchanged.
The pagination component template contains links to navigate to different pages, it uses data from the pager object property to construct the pagination controls and highlight the current active page. The setPage() method is called on link (click) to navigate to the selected page.
The pagination component encapsulates all the logic for paging through a collection of items in Angular.
The following @Input and @Output properties are defined by the component, properties are passed via attributes from a parent component when creating a pagination component (e.g. ).
The ngOnChanges() method is an Angular lifecycle hook that is called when an @Input property changes. It is used here to set the initial page when the items are first loaded or to reset the initial page when the items are changed, for example when the table data is sorted.
The setPage() method sets which page of items to display. It executes the paginate() method to get the pager properties for the specified page, slices out the current page of items into a new array ( pageOfItems ), then emits the changePage event to the parent component with the current page of items.
The brains of the pagination logic is located in the paginate() method, it calculates and returns all the pager properties required to construct the pagination navigation based on the input numbers provided. The Pager interface defines the pager properties returned.
The pagination logic is also available as a standalone npm package jw-paginate . For more info see JavaScript - Pure Pagination Logic in Vanilla JS / TypeScript.
import < Component, Input, Output, EventEmitter, OnChanges, SimpleChanges >from '@angular/core'; @Component(< selector: 'pagination', templateUrl: 'pagination.component.html' >) export class PaginationComponent implements OnChanges < @Input() items?: Array; @Output() changePage = new EventEmitter(true); @Input() initialPage = 1; @Input() pageSize = 10; @Input() maxPages = 10; pager?: Pager; ngOnChanges(changes: SimpleChanges) < // set page when items array first set or changed if (changes.items.currentValue !== changes.items.previousValue) < this.setPage(this.initialPage); >> setPage(page: number) < if (!this.items?.length) return; // get new pager object for specified page this.pager = this.paginate(this.items.length, page, this.pageSize, this.maxPages); // get new page of items from items array const pageOfItems = this.items.slice(this.pager.startIndex, this.pager.endIndex + 1); // call change page function in parent component this.changePage.emit(pageOfItems); >paginate(totalItems: number, currentPage: number = 1, pageSize: number = 10, maxPages: number = 10): Pager < // calculate total pages let totalPages = Math.ceil(totalItems / pageSize); // ensure current page isn't out of range if (currentPage < 1) < currentPage = 1; >else if (currentPage > totalPages) < currentPage = totalPages; >let startPage: number, endPage: number; if (totalPages else < // total pages more than max so calculate start and end pages let maxPagesBeforeCurrentPage = Math.floor(maxPages / 2); let maxPagesAfterCurrentPage = Math.ceil(maxPages / 2) - 1; if (currentPage else if (currentPage + maxPagesAfterCurrentPage >= totalPages) < // current page near the end startPage = totalPages - maxPages + 1; endPage = totalPages; >else < // current page somewhere in the middle startPage = currentPage - maxPagesBeforeCurrentPage; endPage = currentPage + maxPagesAfterCurrentPage; >> // calculate start and end item indexes let startIndex = (currentPage - 1) * pageSize; let endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1); // create an array of pages to ng-repeat in the pager control let pages = Array.from(Array((endPage + 1) - startPage).keys()).map(i => startPage + i); // return object with all pager properties required by the view return < totalItems, currentPage, pageSize, totalPages, startPage, endPage, startIndex, endIndex, pages >; > > export interface Pager
The fake backend API intercepts HTTP requests sent from the Angular app and sends back a fake/mock response if the request URL ends with /items , non-matching requests are passed through as real HTTP requests by calling next.handle(request); . The RxJS delay() operator adds a half sencond delay to simulate a real API request.
The test data returned from the /items route is a hard coded array of users located in fake-backend-data.ts.
An Angular HttpInterceptor is used to implement the fake back end, it is configured in the providers section of the app module.
import < Injectable >from '@angular/core'; import < HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS >from '@angular/common/http'; import < Observable, of >from 'rxjs'; import < delay >from 'rxjs/operators'; import items from './fake-backend-data'; @Injectable() export class FakeBackendInterceptor implements HttpInterceptor < intercept(request: HttpRequest, next: HttpHandler): Observable < const < url, method >= request; return handleRoute(); function handleRoute() < switch (true) < case url.endsWith('/items') && method === 'GET': return ok(items); default: // pass through any requests not handled above return next.handle(request); >> // helper functions function ok(body?: any) < return of(new HttpResponse(< status: 200, body >)) .pipe(delay(500)); // delay observable to simulate server api call > > > export const fakeBackendProvider = < // use fake backend in place of Http service for backend-less development provide: HTTP_INTERCEPTORS, useClass: FakeBackendInterceptor, multi: true >;
An array of 150 test users that are returned by the fake backend API to the Angular app for paging and sorting.
const items: any[] = [ < "id": 1, "firstName": "Diana", "lastName": "Mercado", "age": 31, "company": "INSOURCE" >, < "id": 2, "firstName": "Louise", "lastName": "Luna", "age": 54, "company": "ZENTHALL" >, < "id": 3, "firstName": "Alvarez", "lastName": "Graves", "age": 25, "company": "PATHWAYS" >, . ]; export default items;
The app component template contains the pageable and sortable table of data. Sorting is triggered by clicking any of the table header columns which are bound to the sortBy() method. The sortIcon() displays an icon to indicate which column is sorted and in which direction (☝️ or 👇).
An *ngFor directive is used to loop through the current pageOfItems and render a table row for each item. *ngIf is used to display a loading spinner while the data is loading.
Pagination logic and controls are encapsulated in the component. The items array of the app component is bound to the items property of the pagination component with the Angular data binding attribute [items]="items" . The onChangePage() method of the app component is bound to the changePage event of the pagination component with the Angular event binding attribute (changePage)="onChangePage($event)" . The $event argument is the current page of items ( pageOfItems ) when the event is emitted from the setPage() method of the pagination component.
Angular 14 - Paging and Sorting Table Data Example
Id > First Name > Last Name > Age > Company > > > > > >
The app component contains the logic for the pageable sortable table of data.
ngOnInit() - fetches an array of items from the API.
onChangePage() - callback function bound to the component that populates the current page of items. It is called when items are first loaded, the page is changed or the data is sorted.
sortBy() - sorts the items array by the specified property , if the same sort property is the same the sort order is reversed. The javascript Array sort() method is used to sort the contents of the array with a comparison function, the sort() function sorts the array in place which doesn't trigger the ngOnChanges() method of the component. To resolve this I copied the result into a new array with the spread operator ( [. this.items.sort()] .
sortIcon() - returns an icon to indicate if the specified column is sorted and in which direction (☝️ or 👇).
import < Component >from '@angular/core'; import < HttpClient >from '@angular/common/http'; @Component(< selector: 'app-root', templateUrl: './app.component.html' >) export class AppComponent < items: any[] = []; pageOfItems?: Array; sortProperty: string = 'id'; sortOrder = 1; loading = false; constructor(private http: HttpClient) < >ngOnInit() < // fetch items from the backend api this.loading = true; this.http.get('/items') .subscribe(x => < this.items = x; this.loading = false; >); > onChangePage(pageOfItems: Array) < // update current page of items this.pageOfItems = pageOfItems; >sortBy(property: string) < this.sortOrder = property === this.sortProperty ? (this.sortOrder * -1) : 1; this.sortProperty = property; this.items = [. this.items.sort((a: any, b: any) => < // sort comparison function let result = 0; if (a[property] < b[property]) < result = -1; >if (a[property] > b[property]) < result = 1; >return result * this.sortOrder; >)]; > sortIcon(property: string) < if (property === this.sortProperty) < return this.sortOrder === 1 ? '☝️' : '👇'; >return ''; > >
The app module defines the root module of the application along with metadata about the module. For more info about angular modules see https://angular.io/docs/ts/latest/guide/ngmodule.html.
This is where the fake backend provider is added to the application, to switch to a real backend simply remove the fakeBackendProvider located below the comment // provider for fake backend api .
import < NgModule >from '@angular/core'; import < BrowserModule >from '@angular/platform-browser'; import < HttpClientModule >from '@angular/common/http'; import < AppComponent >from './app.component'; import < PaginationComponent >from './_components'; import < fakeBackendProvider >from './_helpers'; @NgModule(< imports: [ BrowserModule, HttpClientModule ], declarations: [ AppComponent, PaginationComponent ], providers: [ // provider for fake backend api fakeBackendProvider ], bootstrap: [AppComponent] >) export class AppModule
The main index.html file is the initial page loaded by the browser that kicks everything off. The Angular CLI (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.
Angular 14 - Paging and Sorting Table Data Example [email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
The main file is the entry point used by angular to launch and bootstrap the application.
import < enableProdMode >from '@angular/core'; import < platformBrowserDynamic >from '@angular/platform-browser-dynamic'; import < AppModule >from './app/app.module'; import < environment >from './environments/environment'; if (environment.production) < enableProdMode(); >platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err));
The global styles file contains LESS/CSS styles that are applied globally throughout the application.
The cursor style is explicity set to pointer for all a tags including links without an href attribute such as the paging and sorting links.
/* You can add global styles to this file, and also import other style files */ // set cursor to pointer for links without an href attribute a
Search fiverr for freelance Angular 14 developers.
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!