Published: December 13 2022

Angular 14 - Paging and Sorting Table Data Example & Tutorial

Example built with Angular 14.2.12

This tutorial shows how to implement client-side paging and sorting of table data in Angular 14.

Angular Example App

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.

Styled with Bootstrap 5

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/.

Code on GitHub

The example project is available on GitHub at https://github.com/cornflourblue/angular-14-paging-sorting-example.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-14-paging-sorting-example)


Pagination Logic

The following pagination logic is used which is the same as what you see in Google search results:

  • There are 10 page links shown at any time (e.g. 1 2 3 4 5 6 7 8 9 10) unless there are less than 10 total pages.
  • The active link (current page) is in the 6th position, except for when the active link is below 6 or less than 4 from the last position.


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] 


Run the Angular Paging/Sorting Example Locally

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the Angular project source code from https://github.com/cornflourblue/angular-14-paging-sorting-example
  3. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  4. Start the app by running npm start from the command line in the project root folder, this will compile the Angular app and automatically launch it in the browser on the URL http://localhost:4200.

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.


Angular 14 Project Structure

The Angular CLI was used to generate the base project structure with the ng new <project name> 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.

Folder Naming Convention

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.

Barrel Files

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.

 

Pagination Component Template

Path: /src/app/_components/pagination.component.html

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.

<ul *ngIf="pager && pager.pages?.length" class="pagination justify-content-center">
    <li [ngClass]="{ disabled: pager.currentPage === 1 }" class="page-item">
        <a (click)="setPage(1)" class="page-link">First</a>
    </li>
    <li [ngClass]="{ disabled: pager.currentPage === 1 }" class="page-item">
        <a (click)="setPage(pager.currentPage - 1)" class="page-link">Previous</a>
    </li>
    <li *ngFor="let page of pager.pages" [ngClass]="{ active: pager.currentPage === page }" class="page-item">
        <a (click)="setPage(page)" class="page-link">{{page}}</a>
    </li>
    <li [ngClass]="{ disabled: pager.currentPage === pager.totalPages }" class="page-item">
        <a (click)="setPage(pager.currentPage + 1)" class="page-link">Next</a>
    </li>
    <li [ngClass]="{ disabled: pager.currentPage === pager.totalPages }" class="page-item">
        <a (click)="setPage(pager.totalPages)" class="page-link">Last</a>
    </li>
</ul>
 

Pagination Component

Path: /src/app/_components/pagination.component.ts

The pagination component encapsulates all the logic for paging through a collection of items in Angular.

Component properties

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. <pagination [items]="items" (changePage)="onChangePage($event)"></pagination>).

  • items (required) - the complete array of items to be paged.
  • changePage (required) - the callback function to handle the custom changePage event (e.g. onChangePage(pageOfItems: Array<any>)).
  • initialPage (optional) - sets the initial page when first loaded (default value = 1).
  • pageSize (optional) - sets the number of items per page (default value = 10).
  • maxPages (optional) - sets the maximum number of page links to display in the pagination nav bar (default value = 10).

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.


Core pagination logic

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<any>;
    @Output() changePage = new EventEmitter<any>(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 <= maxPages) {
            // total pages less than max so show all pages
            startPage = 1;
            endPage = 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 <= maxPagesBeforeCurrentPage) {
                // current page near the start
                startPage = 1;
                endPage = maxPages;
            } 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 {
    totalItems: number;
    currentPage: number;
    pageSize: number;
    totalPages: number;
    startPage: number;
    endPage: number;
    startIndex: number;
    endIndex: number;
    pages: number[];
}
 

Fake Backend API

Path: /src/app/_helpers/fake-backend.ts

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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        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
};
 

Fake Backend Data

Path: /src/app/_helpers/fake-backend-data.ts

An array of 150 test users that are returned by the fake backend API to the Angular app for paging and sorting.

The data was generated using https://json-generator.com/.

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;
 

App Component Template

Path: /src/app/app.component.html

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 <pagination> 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.

<!-- main app container -->
<div class="card m-3">
    <h3 class="card-header text-center">Angular 14 - Paging and Sorting Table Data Example</h3>
    <div class="card-body">
        <table class="table table-striped table-sm">
            <thead>
                <tr>
                    <th><a (click)="sortBy('id')">Id {{sortIcon('id')}}</a></th>
                    <th><a (click)="sortBy('firstName')">First Name {{sortIcon('firstName')}}</a></th>
                    <th><a (click)="sortBy('lastName')">Last Name {{sortIcon('lastName')}}</a></th>
                    <th><a (click)="sortBy('age')">Age {{sortIcon('age')}}</a></th>
                    <th><a (click)="sortBy('company')">Company {{sortIcon('company')}}</a></th>
                </tr>
            </thead>
            <tbody>
                <tr *ngFor="let item of pageOfItems">
                    <td>{{item.id}}</td>
                    <td>{{item.firstName}}</td>
                    <td>{{item.lastName}}</td>
                    <td>{{item.age}}</td>
                    <td>{{item.company}}</td>
                </tr>
                <tr *ngIf="loading">
                    <td colspan="5" class="text-center py-5">
                        <span class="spinner-border spinner-border-lg align-center"></span>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
    <div class="card-footer pb-0 pt-3">
        <pagination [items]="items" (changePage)="onChangePage($event)"></pagination>
    </div>
</div>
 

App Component

Path: /src/app/app.component.ts

The app component contains the logic for the pageable sortable table of data.

Component methods

ngOnInit() - fetches an array of items from the API.

onChangePage() - callback function bound to the <pagination> 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 <pagination> 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<any>;
    sortProperty: string = 'id';
    sortOrder = 1;
    loading = false;

    constructor(private http: HttpClient) { }

    ngOnInit() {
        // fetch items from the backend api
        this.loading = true;
        this.http.get<any[]>('/items')
            .subscribe(x => {
                this.items = x;
                this.loading = false;
            });
    }

    onChangePage(pageOfItems: Array<any>) {
        // 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 '';
    }
}
 

App Module

Path: /src/app/app.module.ts

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 { }
 

Main Index Html File

Path: /src/index.html

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.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Angular 14 - Paging and Sorting Table Data Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap css -->
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <app-root></app-root>
</body>
</html>
 

Angular Main File

Path: /src/main.ts

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

Global LESS/CSS Styles

Path: /src/styles.less

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 { cursor: pointer; }

 


Need Some Angular 14 Help?

Search fiverr for freelance Angular 14 developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by