Tuesday 4 December 2018

Angular Material Table with Server Side Data

In this post we will build an Angular Material Table with paging and sorting from server side api. The codes are using Angular Material 6.

We will need to create a service and data source that can be consumed by Angular component to populate and refresh the Material table. We will start with basic codes without paging and sorting initially then add the features once the basic is already working.

1. Basic table with server side data
1.1. First, we create a service:
import { Injectable }   from '@angular/core';
import { HttpClient }   from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from './models/user.model';

@Injectable()
export class UserService {
  private serviceUrl = 'http://myserviceurl';
  
  constructor(private http: HttpClient) { }
  
  getUser(): Observable<User[]> {
    return this.http.get<User[]>(this.serviceUrl);
  }  
}

1.2. Then a data source that inherits from Angular DataSource class:
import { CollectionViewer, DataSource } from "@angular/cdk/collections";
import { Observable } from 'rxjs';
import { UserService } from "./user.service";
import { User } from './models/user.model';

export class UserDataSource extends DataSource<any> {
  constructor(private userService: UserService) {
    super();
  }
  connect(): Observable<User[]> {
    return this.userService.getUser();
  }
  disconnect() { }
}

1.3. Then the Angular component that will consume the data source:
import { Component, ViewChild, AfterViewInit, OnInit} from '@angular/core';
import { MatPaginator, MatTableDataSource } from '@angular/material';
import { MatSort } from '@angular/material';

import { UserService } from './user.service';
import { UserDataSource } from './user.datasource';
import { User } from './models/user.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit, OnInit{
  displayedColumns: string[] = ['studentId', 'studentNumber', 'firstName', 'lastName'];
  user: User;
  dataSource: UserDataSource;
   

  constructor(private userService:UserService) {
  }

  ngAfterViewInit() {
  }

  ngOnInit() {
    this.dataSource = new UserDataSource(this.userService);
  }
}

1.4. Finally the HTML template:
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
   <ng-container matColumnDef="studentId">
     <th mat-header-cell *matHeaderCellDef> Student Id </th>
     <td mat-cell *matCellDef="let user"> {{user.studentId}} </td>
   </ng-container>
   <ng-container matColumnDef="studentNumber">
     <th mat-header-cell *matHeaderCellDef> Student Number </th>
     <td mat-cell *matCellDef="let user"> {{user.studentNumber}} </td>
   </ng-container>
   <ng-container matColumnDef="firstName"> 
     <th mat-header-cell *matHeaderCellDef> First Name </th>
     <td mat-cell *matCellDef="let user"> {{user.firstName}} </td>
   </ng-container>
     <ng-container matColumnDef="lastName">
     <th mat-header-cell *matHeaderCellDef> Last Name </th>
     <td mat-cell *matCellDef="let user"> {{user.lastName}} </td>
   </ng-container>

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

Once this is working, we can add the paging feature.

2. Add paging feature
2.1. Import paging module to the app module
import { MatPaginatorModule } from '@angular/material/paginator';
@NgModule({
  . . .,
  imports: [
    . . .
    MatPaginatorModule
  ],
 . . .
})

2.2. Add paginator element on HTML template:
<mat-paginator [pageSizeOptions]="[100, 500]"></mat-paginator> 

2.3. Modify getUser() method in the service class:
getUser(pageIndex : number =1, pageSize : number): Observable<User[]> {

   return this.http.get<User[]>(this.serviceUrl, {
      params: new HttpParams()
        .set('pageIndex', pageIndex.toString())
        .set('pageSize', pageSize.toString())
   });
}  

2.3. Modify the data source class:
export class UserDataSource implements DataSource<User> {
   // add variables to hold the data and number of total records retrieved asynchronously
   // BehaviourSubject type is used for this purpose
   private usersSubject = new BehaviorSubject<User[]>([]);

   // to show the total number of records
   private countSubject = new BehaviorSubject<number>(0);
   public counter$ = this.countSubject.asObservable();

   constructor(private userService: UserService) {
  
   }
  
   loadUsers(pageIndex: number, pageSize: number) {
  
      // use pipe operator to chain functions with Observable type
      this.userService.getUser(pageIndex, pageSize)
      .pipe(
         catchError(() => of([])),
         finalize()
      )
      // subscribe method to receive Observable type data when it is ready
      .subscribe((result : any) => {
         this.usersSubject.next(result.data);
         this.countSubject.next(result.total);
        }
      );
   }
  
   connect(collectionViewer: CollectionViewer): Observable<User[]> {
      console.log("Connecting data source");
      return this.usersSubject.asObservable();
   }

   disconnect(collectionViewer: CollectionViewer): void {
      this.usersSubject.complete();
      this.countSubject.complete();
   }
}

2.4. Modify component class:
// import ViewChild, MatPaginator and MatTableDataSource
import { ViewChild } from '@angular/core';
import { MatPaginator, MatTableDataSource } from '@angular/material';

export class AppComponent implements AfterViewInit, OnInit{

   . . .

   @ViewChild(MatPaginator) paginator: MatPaginator;

   ngAfterViewInit() {

      this.dataSource.counter$
      .pipe(
         tap((count) => {
            this.paginator.length = count;
         })
      )
      .subscribe();

      // when paginator event is invoked, retrieve the related data
      this.paginator.page
      .pipe(
         tap(() => this.dataSource.loadUsers(this.paginator.pageIndex, this.paginator.pageSize))
      )
      .subscribe();
   }  

   ngOnInit() { 
      // set paginator page size
      this.paginator.pageSize = 100;

      this.dataSource = new UserDataSource(this.userService);
      this.dataSource.loadUsers(this.paginator.pageIndex, this.paginator.pageSize);  
   }
}

For the server side api, we need to use a class that can hold data and records count like:
public class PagingResult<T>
{
   public IEnumerable<T> Data { get; set; }

   public int Total { get; set; }
}

Then the codes to retrieve data will look something like:
return new PagingResult<Student>()
{
   Data = query.Skip(pageIndex * pageSize).Take(pageSize).ToList(),
   Total = query.Count()
};

Once the paging works, we can move to the sorting functionality.

3. Add sorting functionality
3.1. Import sorting module to the app module
import { MatSortModule } from '@angular/material/sort;
@NgModule({
   . . .,
   imports: [
      . . .
      MatSortModule
   ],
   . . .
})

3.2. Add sorting element to the table and headers on HTML template:
<table mat-table [dataSource]="dataSource" matSort matSortDisableClear>

. . .

<th mat-header-cell *matHeaderCellDef mat-sort-header> Student Id </th>

<th mat-header-cell *matHeaderCellDef mat-sort-header> First Name </th>

3.3 Modify the component class:
import { MatSort } from '@angular/material';

. . .

export class AppComponent implements AfterViewInit, OnInit{
   . . .
  
   @ViewChild(MatSort) sort: MatSort;
   ngAfterViewInit() {
      . . .

      merge(this.paginator.page, this.sort.sortChange)
      .pipe(
         tap(() => this.dataSource.loadUsers(this.paginator.pageIndex, this.paginator.pageSize, this.sort.active, this.sort.direction))
      )
      .subscribe();
   }

   . . .

   ngOnInit() {
      this.paginator.pageSize = 100;
      this.sort.active = 'firstName';
      this.sort.direction = 'asc';

      this.dataSource = new UserDataSource(this.userService);
      this.dataSource.loadUsers(this.paginator.pageIndex, this.paginator.pageSize, 'firstName', 'asc');
   }
}

Reference:
Angular Material Data Table: A Complete Example (Server Pagination, Filtering, Sorting)