Angular Material 2 table server-side pagination
I figured out this problem following Table retrieving data through HTTP from angular material docs.
What the example says is, use ngAfterViewInit()
plus observables to handle everything on the table, pagination, sorting and other stuff that you need, code:
import {Component, AfterViewInit, ViewChild} from '@angular/core';
import {HttpClient} from '@angular/common/http';
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';
/**
* @title Table retrieving data through HTTP
*/
@Component({
selector: 'table-http-example',
styleUrls: ['table-http-example.css'],
templateUrl: 'table-http-example.html',
})
export class TableHttpExample implements AfterViewInit {
displayedColumns = ['created', 'state', 'number', 'title'];
exampleDatabase: ExampleHttpDao | null;
dataSource = new MatTableDataSource();
resultsLength = 0;
isLoadingResults = false;
isRateLimitReached = false;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(private http: HttpClient) {}
ngAfterViewInit() {
this.exampleDatabase = new ExampleHttpDao(this.http);
// 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.exampleDatabase!.getRepoIssues(
this.sort.active, this.sort.direction, this.paginator.pageIndex);
}),
map(data => {
// Flip flag to show that loading has finished.
this.isLoadingResults = false;
this.isRateLimitReached = false;
this.resultsLength = data.total_count;
return data.items;
}),
catchError(() => {
this.isLoadingResults = false;
// Catch if the GitHub API has reached its rate limit. Return empty data.
this.isRateLimitReached = true;
return observableOf([]);
})
).subscribe(data => this.dataSource.data = data);
}
}
export interface GithubApi {
items: GithubIssue[];
total_count: number;
}
export interface GithubIssue {
created_at: string;
number: string;
state: string;
title: string;
}
/** An example database that the data source uses to retrieve data for the table. */
export class ExampleHttpDao {
constructor(private http: HttpClient) {}
getRepoIssues(sort: string, order: string, page: number): Observable<GithubApi> {
const href = 'https://api.github.com/search/issues';
const requestUrl =
`${href}?q=repo:angular/material2&sort=${sort}&order=${order}&page=${page + 1}`;
return this.http.get<GithubApi>(requestUrl);
}
}
Look that everything is handled inside the ngAfterViewInit thanks to observables. the line this.resultsLength = data.total_count;
is expecting that your service is returning the data with total register counts, in my case I'm using springboot and its returned everything that I need.
If you need more clarification, write any comment and I gonna update the answer, but checking the example from the docs you'll figure it.
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:
- Make sure you import Modules:
MatTableModule
,MatPaginatorModule
andMatSortModule
along other modules from Material Design. - I decided to populate
resultsLength
(total) from Response-Headerx-total-count
which I populate through Spring Boot@ControllerAdvice
. Alternatively you can get this information from the object returned fromEntityService
(e.gPage
for Spring Boot) although that implies you will need to useany
as return type or declare wrapper class objects for all of the entities in your project if you want to be "type-safe".