Angular Material 2 table server-side pagination

Vijender Reddy Chintalapudi picture Vijender Reddy Chintalapudi · Dec 18, 2017 · Viewed 35.7k times · Source

I am trying to Achieve Angular Material 2, MatPaginator server side paging. How can I achieve that?

Below is the code example:

  <div class="example-container mat-elevation-z8">
  <mat-table #table [dataSource]="dataSource">

    <!-- Position Column -->
    <ng-container matColumnDef="position">
      <mat-header-cell *matHeaderCellDef> No. </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.position}} </mat-cell>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
    </ng-container>

    <!-- Weight Column -->
    <ng-container matColumnDef="weight">
      <mat-header-cell *matHeaderCellDef> Weight </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.weight}} </mat-cell>
    </ng-container>

    <!-- Symbol Column -->
    <ng-container matColumnDef="symbol">
      <mat-header-cell *matHeaderCellDef> Symbol </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.symbol}} </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>

  <mat-paginator #paginator
                 [pageSize]="10"
                 [pageSizeOptions]="[5, 10, 20]">
  </mat-paginator>
</div>

Pagination Component:

import {Component, ViewChild} from '@angular/core';
import {MatPaginator, MatTableDataSource} from '@angular/material';

/**
 * @title Table with pagination
 */
@Component({
  selector: 'table-pagination-example',
  styleUrls: ['table-pagination-example.css'],
  templateUrl: 'table-pagination-example.html',
})
export class TablePaginationExample {
  displayedColumns = ['position', 'name', 'weight', 'symbol'];
  dataSource = new MatTableDataSource<Element>(ELEMENT_DATA);

  @ViewChild(MatPaginator) paginator: MatPaginator;

  /**
   * Set the paginator after the view init since this component will
   * be able to query its view for the initialized paginator.
   */
  ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
  }
}

export interface Element {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}

const ELEMENT_DATA: Element[] = [
  {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
  {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
  {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
  {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
  {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
  {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
  {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
  {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
  {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
  {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
  {position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na'},
  {position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg'},
  {position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al'},
  {position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si'},
  {position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P'},
  {position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S'},
  {position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl'},
  {position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar'},
  {position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K'},
  {position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca'},
];

How can I achieve server side pagination, which would trigger on change event of next page click or page size changes to get next set of records.

https://stackblitz.com/angular/qxxpqbqolyb?file=app%2Ftable-pagination-example.ts

Answer

Michail Michailidis picture Michail Michailidis · Jan 27, 2018

Based on Wilfredo's answer (https://stackoverflow.com/a/47994113/986160) I compiled a full working example since some pieces were missing from question as well. Here is a more general case for server-side pagination and sorting using Angular 5 and Material Design (still need to plug in filtering) - hopefully it will be helpful to someone:

Paging Component:

import { ViewChild, Component, Inject, OnInit, AfterViewInit } from '@angular/core';
import { EntityJson } from './entity.json';
import { EntityService } from './entity.service';
import { MatPaginator, MatSort, MatTableDataSource } from '@angular/material';
import { Observable } from 'rxjs/Observable';
import { merge } from 'rxjs/observable/merge';
import { of as observableOf } from 'rxjs/observable/of';
import { catchError } from 'rxjs/operators/catchError';
import { map } from 'rxjs/operators/map';
import { startWith } from 'rxjs/operators/startWith';
import { switchMap } from 'rxjs/operators/switchMap';

@Component({
    selector: 'entity-latest-page',
    providers: [EntityService],
    styles: [`
        :host mat-table {
           display: flex;
           flex-direction: column;
           min-width: 100px;
           max-width: 800px;
           margin: 0 auto;
        }
    `],
    template:
    `<mat-card>
        <mat-card-title>Entity List 
        <button mat-button [routerLink]="['/create/entity']">
            CREATE
        </button>
        </mat-card-title>
        <mat-card-content>
            <mat-table #table matSort [dataSource]="entitiesDataSource" matSort class="mat-elevation-z2">
                <ng-container matColumnDef="id">
                    <mat-header-cell *matHeaderCellDef mat-sort-header> Id </mat-header-cell>
                    <mat-cell *matCellDef="let element"> {{element.id}} </mat-cell>
                </ng-container>
                <ng-container matColumnDef="name">
                    <mat-header-cell *matHeaderCellDef  mat-sort-header> Name </mat-header-cell>
                    <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
                </ng-container>
                <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
                <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
            </mat-table>
        </mat-card-content>
        <mat-card-content>
            <mat-paginator #paginator [length]="resultsLength"
                [pageSize]="5"
                [pageSizeOptions]="[5, 10, 20]">
            </mat-paginator>
        </mat-card-content>
    </mat-card>
    `
})
export class EntityLatestPageComponent implements AfterViewInit {

    private entities: EntityJson[];
    private entitiesDataSource: MatTableDataSource<EntityJson> = new MatTableDataSource();
    private displayedColumns = ['id', 'name'];

    resultsLength = 0;
    isLoadingResults = false;
    isRateLimitReached = false;

    @ViewChild(MatPaginator) paginator: MatPaginator;
    @ViewChild(MatSort) sort: MatSort;

    public constructor( @Inject(EntityService) private entityService: EntityService) {
    }

    public ngAfterViewInit() {

        // If the user changes the sort order, reset back to the first page.
        this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
        merge(this.sort.sortChange, this.paginator.page)
        .pipe(
            startWith({}),
            switchMap(() => {
            this.isLoadingResults = true;
            return this.entityService.fetchLatest(this.sort.active, this.sort.direction, 
                  this.paginator.pageIndex + 1, this.paginator.pageSize, 
                  (total) =>  this.resultsLength = total);
            }),
            map(data => {
            this.isLoadingResults = false;
            this.isRateLimitReached = false;
            //alternatively to response headers;
            //this.resultsLength = data.total;
            return data;
            }),
            catchError(() => {
            this.isLoadingResults = false;
            this.isRateLimitReached = true;
            return observableOf([]);
            })
        ).subscribe(data => this.entitiesDataSource.data = data);
    } 
}

Service:

import { EntityJson } from './entity.json';
import { ApiHelper } from '../common/api.helper';
import { Http, Headers, Response, RequestOptions } from '@angular/http';
import { Inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AuthenticationService } from '../auth/authentication.service';
import { stringify } from 'query-string';

@Injectable()
export class EntityService {

    private options: RequestOptions;
    private apiPrefix: string;
    private apiEndpoint: string;

    constructor(
        @Inject(Http) private http: Http,
        @Inject(AuthenticationService) private authService: AuthenticationService) {

        this.options = authService.prepareRequestHeaders();
        this.apiPrefix = 'http://localhost:4200/api/v1/';
        this.apiEndpoint = this.apiPrefix + 'entities';
    }

    public fetchLatest(sort: string = '', order: string = '', page: number = 1, perPage: number = 5, initTotal: Function = () => {}): Observable<EntityJson[]> {
        return this.http.get(this.apiEndpoint +'?' + EntityService.createUrlQuery({sort: {field: sort, order: order}, pagination: { page, perPage }}), this.options)
            .map((res) => {
                const total = res.headers.get('x-total-count').split('/').pop();
                initTotal(total);
                return JSON.parse(res.text()).content
            });
    }

    //should be put in a util
    static createUrlQuery(params: any) {
        if (!params) {
            return "";
        }

        let page;
        let perPage;
        let field;
        let order;
        let query: any = {};
        if (params.pagination) {
             page = params.pagination.page;
             perPage =  params.pagination.perPage;
             query.range = JSON.stringify([
                page,
                perPage,
            ]);
        }
        if (params.sort) {
            field = params.sort.field;
            order = params.sort.order;
            if (field && order) {
                query.sort = JSON.stringify([field, order]);
            }
            else {
                query.sort = JSON.stringify(['id', 'ASC']);
            }
        }
        if (!params.filter) {
            params.filter = {};
        }
        if (Array.isArray(params.ids)) {
            params.filter.id = params.ids;
        }

        if (params.filter) {
            query.filter = JSON.stringify(params.filter)
        }
        console.log(query, stringify(query));
        return stringify(query);
    }
}

Spring Boot Rest Controller Endpoint

@GetMapping("entities")
public Iterable<Entity> filterBy(
        @RequestParam(required = false, name = "filter") String filterStr,
        @RequestParam(required = false, name = "range") String rangeStr, @RequestParam(required = false, name="sort") String sortStr) {
    //my own helpers - for source: https://github.com/zifnab87/react-admin-java-rest
    //FilterWrapper wrapper = filterService.extractFilterWrapper(filterStr, rangeStr, sortStr);
    //return filterService.filterBy(wrapper, repo);
}

Some notes:

  1. Make sure you import Modules: MatTableModule, MatPaginatorModule and MatSortModule along other modules from Material Design.
  2. I decided to populate resultsLength (total) from Response-Header x-total-count which I populate through Spring Boot @ControllerAdvice. Alternatively you can get this information from the object returned from EntityService (e.g Page for Spring Boot) although that implies you will need to use any as return type or declare wrapper class objects for all of the entities in your project if you want to be "type-safe".