Server Side Pagination Using NodeJS, MongoDB and Angular Material Tables

Normally, we use Angular Material’s data tables with pagination (mat-paginator) to list the results. This is not an efficient and optimal approach particularly if there are lots of records to show in the table. Angular Material’s pagination just divides the received records into the paginated records. So, if we need to display lots of records in data tables, then the efficient and optimal approach is to use Server Side Pagination. Following, sections will demonstrate the server-side pagination using NodeJS (API), MongoDB (database), and Angular Material data tables.

API using NodeJS and MongoDB

Following is a sample code, written in NodeJS with MongoDB model which query records from a MongoDB collection based on the current page (page), number of records per page (pageSize), and page type (pageType) (first, last, page). Depending on the type of page, MongoDB query is prepared which then returns the total records (count) matching the query, paginated records (depending on pageSize) (data), and the current page (currentPage).

router.post('/paginated/', (req, res) => {
    // get page from query params or default to first page
    const page = parseInt(req.query.page) || 1;
    const pageType = req.query.pageType || 'first';
    const pageSize = parseInt(req.query.pageSize) || 5;
    if(pageType == 'first'){
        PermissionUsage.countDocuments({}).exec(function(err, count) {
            if (err) return res.status(500).json({success: false, message: err.message});
            PermissionUsage.find({}).limit(pageSize).exec(function(err, permissionUsages) {
                if (err) return res.status(500).json({success: false, message: err.message});
                res.status(200).send(JSON.stringify({success: true, data: permissionUsages, pager: {total: count, currentPage: page}}));
            });
        });
    }else if(pageType == 'page'){
        PermissionUsage.countDocuments({}).exec(function(err, count) {
            if (err) return res.status(500).json({success: false, message: err.message});
            PermissionUsage.find({}).skip((page-1)*pageSize).limit(pageSize).exec(function(err, permissionUsages) {
                if (err) return res.status(500).json({success: false, message: err.message});
                res.status(200).send(JSON.stringify({success: true, data: permissionUsages, pager: {total: count, currentPage: page}}));
            });
        });
    }else{
        PermissionUsage.countDocuments({}).exec(function(err, count) {
            if (err) return res.status(500).json({success: false, message: err.message});
            PermissionUsage.find({}).skip((page-1)*pageSize).limit(pageSize).exec(function(err, permissionUsages) {
                if (err) return res.status(500).json({success: false, message: err.message});
                res.status(200).send(JSON.stringify({success: true, data: permissionUsages, pager: {total: count, currentPage: page}}));
            });
        });
    }
});

Requesting Paginated Data From Angular Application

myService.service.ts

getPaginatedPermissionUsage(page, pageType, pageSize){
    return this.http.get('http://localhost:3000/permission-usages/paginated/?page='+page+'&pageType='+pageType+'&pageSize='+pageSize);
  }

In a service we have created a method which calls our API’s endpoint to receive the paginated records. Here page parameter represents the current page, pageType represents whether the page is first, last or normal page and pageSize represents the number of records per page.

myComponent.component.ts

pager:any = {currentPage: 1, total: 0};
page: number = 1;
pageType: string = 'first';
totalPages: number = 1;
pageSize: number = 5;
....
....
....
getPaginatedResults(pageNumber = 1){
this.page = pageNumber;
if(this.page == 1){
      this.pageType = 'first';
}else if(this.page == window.Math.ceil(this.pager.total/this.pageSize)){
      this.pageType = 'last';
}else{
      this.pageType = 'page';
}
this.myService.getPaginatedPermissionUsage(this.page, this.pageType, this.pageSize).subscribe((res:any) => {
      this.pager = res.pager;
      this.totalPages = window.Math.ceil(this.pager.total/this.pageSize);
      this.dataSource.data = res.data;
});
}

myComponent.component.html

<ul *ngIf="pager.total && pager.total > pageSize" class="pagination">
        <li class="page-item first-item">
            <a routerLink="/reports" [queryParams]="{ page: 1 }" [ngClass]="{disabled:pager.currentPage == 1}" *ngIf="pager.currentPage != 1" (click)="getPaginatedResults(1)" class="page-link">First</a>
        </li>
        <li class="page-item previous-item">
            <a routerLink="/reports" [queryParams]="{ page: pager.currentPage - 1 }" [ngClass]="{disabled:pager.currentPage == 1}" *ngIf="pager.currentPage != 1" (click)="getPaginatedResults(pager.currentPage - 1)" class="page-link">&laquo;</a>
        </li>
        <li *ngFor="let dummy of ' '.repeat(totalPages).split(''), let x = index" class="page-item number-item">
            <a routerLink="/reports" [queryParams]="{ page: x+1 }" [ngClass]="{active:pager.currentPage == x+1}" class="page-link" (click)="getPaginatedResults(x+1)">{{x+1}}</a>
        </li>
        <li class="page-item next-item">
            <a routerLink="/reports" [queryParams]="{ page: pager.currentPage + 1 }" *ngIf="pager.currentPage != totalPages" (click)="getPaginatedResults(pager.currentPage + 1)" class="page-link">&raquo;</a>
        </li>
        <li class="page-item last-item">
            <a routerLink="/reports" [queryParams]="{ page: totalPages }" *ngIf="pager.currentPage != totalPages" class="page-link" (click)="getPaginatedResults(totalPages)">Last</a>
        </li>
</ul>

This might not be the best implementation of the server-side pagination with MongoDB and Angular Material data tables but it works. Please feel free to share any alternate or better implementation of the server-side pagination.

Leave a Comment

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