0% found this document useful (0 votes)
146 views

Angular Material Data Table - A Complete Example

This document provides a step-by-step tutorial on using the Angular Material Data Table component. It covers common use cases like server-side pagination, sorting, and filtering. Various Angular Material modules are imported and the data table is configured with column definitions and directives. Material design styling is then applied.

Uploaded by

OULED SAHEL
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
146 views

Angular Material Data Table - A Complete Example

This document provides a step-by-step tutorial on using the Angular Material Data Table component. It covers common use cases like server-side pagination, sorting, and filtering. Various Angular Material modules are imported and the data table is configured with column definitions and directives. Material design styling is then applied.

Uploaded by

OULED SAHEL
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 45

BLOG COURSES FREE COURSE NEWSLETTER

ANGULAR MATERIAL

Angular Material Data Table:


A Complete Example
(Server Pagination,
Filtering, Sorting)
ANGULAR UNIVERSITY
17 DEC 2020

In this post, we are going to go through a complete example of how to


use the Angular Material Data Table.

We are going to cover many of the most common use cases that revolve
around the Angular Material Data Table component, such as: server-side
pagination, sorting, and filtering.

This is a step-by-step tutorial, so I invite you to code along as we are


going to start with a simple initial scenario. We will then progressively
add BLOG
featuresCOURSES
one by one FREE COURSE
and explain NEWSLETTER
everything along the way (including
gotchas).
We will learn in detail all about the reactive design principles involved in
the design of the Angular Material Data Table and an Angular CDK Data
Source.

The end result of this post will be:

a complete example of how to implement an Angular Material


Data Table with server-side pagination, sorting and filtering using
a custom CDK Data Source

a running example available on Github, which includes a small


backend Express server that serves the paginated data

Table Of Contents
In this post, we will cover the following topics:

The Angular Material Data Table - not only for Material Design

The Material Data Table Reactive Design

The Material Paginator and Server-side Pagination

Sortable Headers and Server-side Sorting

Server-side Filtering with Material Input Box

A Loading Indicator

A Custom Angular Material CDK Data Source

Source Code (on Github) with the complete example

Conclusions

So without further ado, let's get started with our Material Data Table
Guided Tour!

Importing Angular Material modules


In order to run our example, let's first import all the Angular Material
BLOG COURSES FREE COURSE NEWSLETTER
modules that we will need:

2 import { MatInputModule, MatPaginatorModule, MatProgressSpinnerModule,

3 MatSortModule, MatTableModule } from "@angular/material";

5 @NgModule({

6 declarations: [

7 ...
8 ],

9 imports: [

10 BrowserModule,

11 BrowserAnimationsModule,

12 HttpClientModule,

13 MatInputModule,

14 MatTableModule,

15 MatPaginatorModule,

16 MatSortModule,

17 MatProgressSpinnerModule

18 ],

19 providers: [

20 ...
21 ],

22 bootstrap: [AppComponent]

23 })
24 export class AppModule {

25

26 }

01.ts
hosted with ❤ by GitHub view raw
HereBLOG COURSESof the
is a breakdown FREE COURSEof each
contents NEWSLETTER
Material module:

MatInputModule: this contains the components and directives


for adding Material design Input Boxes to our application (needed
for the search input box)

MatTableModule: this is the core data table module, which


includes the mat-table component and many related components
and directives

MatPaginatorModule: this is a generic pagination module, that


can be used to paginate data in general. This module can also be
used separately from the Data table, for example for
implementing Detail pagination logic in a Master-Detail setup

MatSortModule: this is an optional module that allows adding


sortable headers to a data table

MatProgressSpinnerModule: this module includes the progress


indicator component that we will be using to indicate that data is
being loaded from the backend

Introduction to the Angular Material Data Table


The Material Data Table component is a generic component for
displaying tabulated data. Although we can easily give it a Material
Design look and feel, this is actually not mandatory.

In fact, we can give the Angular Material Data table an alternative UI


design if needed. To see that this is so, let's start by creating a Data
Table where the table cells are just plain divs with no custom CSS
applied.

This data table will display a list of course lessons, and has 3 columns
(sequence number, description and duration):
1

2 BLOG COURSES
<mat-table FREE COURSE
class="lessons-table NEWSLETTER
mat-elevation-z8" [dataSource]="dataSource"

3
4 <ng-container matColumnDef="seqNo">

5 <div *matHeaderCellDef>#</div>

6 <div *matCellDef="let lesson">{{lesson.seqNo}}</div>

7 </ng-container>

8
9 <ng-container matColumnDef="description">

10 <div *matHeaderCellDef>Description</div>

11 <div class="description-cell"

12 *matCellDef="let lesson">{{lesson.description}}</div>

13 </ng-container>

14
15 <ng-container matColumnDef="duration">
16 <div *matHeaderCellDef>Duration</div>
17 <div class="duration-cell"

18 *matCellDef="let lesson">{{lesson.duration}}</div>
19 </ng-container>
20
21 <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>

22
23 <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
24
25 </mat-table>

02.html
hosted with ❤ by GitHub view raw

Material Data Table Column Definitions


As we can see, this table defines 3 columns, each inside its own ng-cont
ainer element. The ng-container element will NOT be rendered to the
screen (see this post for more details), but it will provide an element for
applying the matColumnDef directive.

The matColumnDef directive uniquely identifies a given column with a key:


seqNo, description or duration. Inside the ng-container element, we will
have all the configuration for a given column.

Notice that the order of the ng-container elements does NOT


determine the column visual order
TheBLOG
Material Data FREE
COURSES Table Auxiliary
COURSE Definition
NEWSLETTER
Directives
The Material Data Table has a series of auxiliary structural directives
(applied using the *directiveName syntax) that allow us to mark certain
template sections has having a certain role in the overall data table
design.

These directives always end with the Def postfix, and they are used to
assign a role to a template section. The first two directives that we will
cover are matHeaderCellDef and matCellDef .

The matHeaderCellDef and matCellDef Directives


Inside of each ng-container with a given column definition, there are a
couple of configuration elements:

we have the template that defines how to display the header of a


given column, identified via the matHeaderCellDef structural
directive

we also have another template that defines how to display the


data cells of a given column, using the matCellDef structural
directive

These two structural directives only identify which template elements


have a given role (cell template, header template), but they do not
attach any styling to those elements.

For example, in this case, matCellDef and matHeaderCellDef are being


applied to plain divs with no styling, so this is why this table does not
have a Material design yet.

Applying a Material Design to the Data Table


Let's now see what it would take to give this Data Table a Material Look
and Feel. For that, we will use a couple of built-in components in our
header and cell template definitions:
1

BLOG COURSES FREE COURSE NEWSLETTER


2 <mat-table class="lessons-table mat-elevation-z8" [dataSource]="dataSource"

4 <ng-container matColumnDef="seqNo">

5 <mat-header-cell *matHeaderCellDef>#</mat-header-cell>

6 <mat-cell *matCellDef="let lesson">{{lesson.seqNo}}</mat-cell>

7 </ng-container>

9 <ng-container matColumnDef="description">

10 <mat-header-cell *matHeaderCellDef>Description</mat-header-cell>

11 <mat-cell class="description-cell"

12 *matCellDef="let lesson">{{lesson.description}}</mat-cell

13

14 </ng-container>

15

16 <ng-container matColumnDef="duration">
17 <mat-header-cell *matHeaderCellDef>Duration</mat-header-cell>

18 <mat-cell class="duration-cell"
19 *matCellDef="let lesson">{{lesson.duration}}</mat-cell>
20 </ng-container>
21

22 <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
23

24 <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>

25

26 </mat-table>

03.html
hosted with ❤ by GitHub view raw

This template is almost the same as the one we saw before, but now we
are using the mat-header-cell and mat-cell components inside our column
definition instead of plain divs.

Using these components, lets now have a look at what the Data Table
looks like with this new Material Design:
BLOG COURSES FREE COURSE NEWSLETTER

Notice that the table already has some data! We will get to the data
source in a moment, right now let's continue exploring the rest of the
template.

The matCellDef Directive


The data cell template has access to the data that is being displayed. In
this case, our data table is displaying a list of lessons, so the lesson
object in each row is accessible via the let lesson syntax, and can be used
in the template just like any component variable.
TheBLOG
mat-header-row
COURSES component
FREE COURSE and the
NEWSLETTER
matHeaderRowDef directive
This combination of related component / directive works in the following
way:

the matHeaderRowDef identifies a configuration element for the


table header row, but it does not apply any styling to the element

The mat-header-row on the other hand applies some minimal


Material stying

The matHeaderRowDef directive also defines in which order the columns


should be displayed. In our case, the directive expression is pointing to a
component variable named displayedColumns .

Here is what the displayedColumns component variable will look like:

2 displayedColumns = ["seqNo", "description", "duration"];

04.ts
hosted with ❤ by GitHub view raw

The values of this array are the column keys, which need to be identical
to the names of the ng-container column sections (specified via the matCo
lumnDef directive).

Note: It's this array that determines the visual order of the
columns!

The mat-row component and the matRowDef directive


This component / directive pair also works in a similar way to what we
have seen in previous cases:

matRowDef identifies which element inside mat-table provides


configuration for how a data row should look like, without
providing any specific styling
on the other hand, mat-row will provide some Material stying to
BLOG COURSES FREE COURSE NEWSLETTER
the data row

With mat-row , we also have a variable exported that we have named ro


w , containing the data of a given data row, and we have to specify the c
olumns property, which contains the order on which the data cells should
be defined.

Interacting with a given table data row


We can even use the element identified by the matRowDef directive to
interact with a given data row. For example, this is how we can detect if
a given data row was clicked:

2 <mat-row *matRowDef="let row; columns: displayedColumns"

3 (click)="onRowClicked(row)">

4 </mat-row>

05.html
hosted with ❤ by GitHub view raw

When a row is clicked, we will call the onRowClicked() component method,


that will then log the row data to the console:

2 onRowClicked(row) {

3 console.log('Row clicked: ', row);

4 }

06.ts
hosted with ❤ by GitHub view raw

If we now click on the first row of our data table, here is what the result
will look like on the console:
BLOG COURSES FREE COURSE NEWSLETTER

As we can see the data for the first row is being printed to the console,
as expected! But where is this data coming from?

To answer that, let's then talk about the data source that is linked to this
data table, and go over the Material Data Table reactive design.

Data Sources and the Data Table Reactive Design


The data table that we have been presenting receives the data that it
displays from a Data Source that implements an Observable-based API
and follows common reactive design principles.

This means for example that the data table component does not know
where the data is coming from. The data could be coming for example
from the backend, or from a client-side cache, but that is transparent to
the Data table.

The Data table simply subscribes to an Observable provided by the Data


Source. When that Observable emits a new value, it will contain a list of
lessons that then get's displayed in the data table.

Data Table core design principles


With this Observable-based API, not only the Data table does not know
where the data is coming from, but the data table also does not know
what triggered the arrival of new data.

Here are some possible causes for the emission of new data:

the data table is initially displayed


the user clicks on a paginator button
BLOG COURSES FREE COURSE NEWSLETTER
the user sorts the data by clicking on a sortable header

the user types a search using an input box

Again, the Data Table has no information about exactly which event
caused new data to arrive, which allows the Data Table components and
directives to focus only on displaying the data, and not fetching it.

Let's then see how can we implement such a reactive data source.

Why not use MatTableDataSource ?


In this example, we will not be using the built-in MatTableDataSource
because its designed for filtering, sorting and pagination of a client-side
data array.

In our case, all the filtering, sorting and pagination will be happening on
the server, so we will be building our own Angular CDK reactive data
source from first principles.

Fetching Data from the backend


In order to fetch data from the backend, our custom Data Source is
going to be using the LessonsService . This is a standard Observable-based
stateless singleton service that is built internally using the Angular HTTP
Client.

Let's have a look at this service, and break down how its implemented:
1

2 BLOG COURSES
@Injectable() FREE COURSE NEWSLETTER
3 export class CoursesService {

5 constructor(private http:HttpClient) {}
6

7 findLessons(

8 courseId:number, filter = '', sortOrder = 'asc',


9 pageNumber = 0, pageSize = 3): Observable<Lesson[]> {

10

11 return this.http.get('/api/lessons', {
12 params: new HttpParams()

13 .set('courseId', courseId.toString())

14 .set('filter', filter)
15 .set('sortOrder', sortOrder)

16 .set('pageNumber', pageNumber.toString())

17 .set('pageSize', pageSize.toString())
18 }).pipe(

19 map(res => res["payload"])

20 );
21 }

22 }
23
24

07.ts
hosted with ❤ by GitHub view raw

Breaking down the LessonsService implementation


As we can see, this service is completely stateless, and every method
forwards calls to the backend using the HTTP client, and returns an
Observable to the caller.

Our REST API is available in URLs under the /api directory, and multiple
services are available (here is the complete implementation).

In this snippet, we are just showing the findLessons() method, that allows
to obtain one filtered and sorted page of lessons data for a given course.

Here are the arguments that we can pass to this function:


BLOG COURSES
courseId: FREE
This identifies a COURSE NEWSLETTER
given course, for which we want to
retrieve a page of lessons

filter: This is a search string that will help us filter the results. If
we pass the empty string '' it means that no filtering is done on
the server

sortOrder: our backend allows us to sort based on the seqNo


column, and with this parameter, we can specify is the sort order
is ascending (which is the default asc value), or descending by
passing the value desc

pageNumber: With the results filtered and sorted, we are going to


specify which page of that full list of results we need. The default
is to return the first page (with index 0)

pageSize: this specifies the page size, which defaults to a


maximum of 3 elements

With this arguments, the loadLessons() method will then build an HTTP
GET call to the backend endpoint available at /api/lessons .

Here is what an HTTP GET call that fetches the lessons for the first page
looks like:

http://localhost:4200/api/lessons?courseId=1&filter=&sortOrder=asc&pageNumber=0&p

As we can see, we are appending a series of HTTP query parameters to


the GET URL using the HTTPParams fluent API.

This loadLessons() method will be the basis of our Data Source, as it will
allow us to cover the server pagination, sorting and filtering use cases.

Implementing a Custom Angular CDK Data Source


Using the LessonsService , let's now implement a custom Observable-
BLOG COURSES FREE COURSE NEWSLETTER
based Angular CDK Data Source. Here is some initial code, so that we
can discuss its Reactive design (the full version is shown in a moment):

2 import {CollectionViewer, DataSource} from "@angular/cdk/collections";

4 export class LessonsDataSource implements DataSource<Lesson> {


5

6 private lessonsSubject = new BehaviorSubject<Lesson[]>([]);

8 constructor(private coursesService: CoursesService) {}


9

10 connect(collectionViewer: CollectionViewer): Observable<Lesson[]> {


11 ...

12 }

13

14 disconnect(collectionViewer: CollectionViewer): void {

15 ...
16 }
17
18 loadLessons(courseId: number, filter: string,
19 sortDirection: string, pageIndex: number, pageSize: number)
20 ...
21 }

22 }
23

08.ts
hosted with ❤ by GitHub view raw
BLOG COURSES FREE COURSE NEWSLETTER

Breaking down the design of an Angular CDK Data


Source
Has we can see, in order to create a Data Source we need to create a
class that implements DataSource . This means that this class needs to
implement a couple of methods: connect() and disconnect() .

Note that these methods provide an argument which is a CollectionViewer ,


which provides an Observable that emits information about what data is
being displayed (the start index and the end index).

We would recommend for now not to focus so much on the CollectionView


er at this moment, but on something much more important for
understanding the whole design: the return value of the connect()
method.

How to implement the DataSource connect() method


This method will be called once by the Data Table at table bootstrap
time. The Data Table expects this method to return an Observable, and
the values of that observable contain the data that the Data Table needs
to display.

In this case, this observable will emit a list of Lessons. As the user clicks
on the paginator and changes to a new page, this observable will emit a
new value with the new lessons page.

We will implement this method by using a subject that is going to be


invisible outside this class. That subject (the lessonsSubject ) is going to be
emitting the values retrieved from the backend.

The lessonsSubject is a BehaviorSubject , which means its subscribers will


always get its latest emitted value (or an initial value), even if they
subscribed late (after the value was emitted).
BLOG COURSES FREE COURSE NEWSLETTER
Why use BehaviorSubject ?
Using BehaviorSubject is a great way of writing code that works
independently of the order that we use to perform asynchronous
operations such as: calling the backend, binding the data table to the
data source, etc.

For example, in this design, the Data Source is not aware of the data
table or at which moment the Data Table will require the data. Because
the data table subscribed to the connect() observable, it will eventually
get the data, even if:

the data is still in transit coming from the HTTP backend

or if the data was already loaded

Custom Material CDK Data Source - Full


Implementation Review
Now that we understand the reactive design of the data source, let's
have a look at the complete final implementation and review it step-by-
step.

Notice that in this final implementation, we have also included the


notion of a loading flag, that we will use to display a spinning loading
indicator to the user later on:
1

2 BLOG COURSES
export class FREE COURSE
LessonsDataSource NEWSLETTER
implements DataSource<Lesson> {

4 private lessonsSubject = new BehaviorSubject<Lesson[]>([]);


5 private loadingSubject = new BehaviorSubject<boolean>(false);

7 public loading$ = this.loadingSubject.asObservable();


8

9 constructor(private coursesService: CoursesService) {}


10

11 connect(collectionViewer: CollectionViewer): Observable<Lesson[]> {

12 return this.lessonsSubject.asObservable();
13 }
14

15 disconnect(collectionViewer: CollectionViewer): void {


16 this.lessonsSubject.complete();
17 this.loadingSubject.complete();

18 }
19

20 loadLessons(courseId: number, filter = '',

21 sortDirection = 'asc', pageIndex = 0, pageSize = 3) {


22

23 this.loadingSubject.next(true);

24

25 this.coursesService.findLessons(courseId, filter, sortDirection,


26 pageIndex, pageSize).pipe(

27 catchError(() => of([])),


28 finalize(() => this.loadingSubject.next(false))
29 )

30 .subscribe(lessons => this.lessonsSubject.next(lessons));


31 }
32 }

09.ts
hosted with ❤ by GitHub view raw

Data Source Loading Indicator Implementation


Breakdown
Let's start breaking down this code, we will start first with the
implementation of the loading indicator. Because this Data Source class
has a reactive design, let's implement the loading flag by exposing a
boolean observable called loading$ .
ThisBLOG
observable will emitFREE
COURSES as first value
COURSE false (which is defined in the Beha
NEWSLETTER
viorSubject constructor), meaning that no data is loading initially.

The loading$ observable is derived using asObservable() from a subject


that is kept private to the data source class. The idea is that only this
class knows when data is loading, so only this class can access the
subject and emit new values for the loading flag.

The connect() method implementation


Let's now focus on the implementation of the connect method:

2 connect(collectionViewer: CollectionViewer): Observable<Lesson[]> {


3 return this.lessonsSubject.asObservable();

4 }

20.ts
hosted with ❤ by GitHub view raw

This method will need to return an Observable that emits the lessons
data, but we don't want to expose the internal subject lessonsSubject
directly.

Exposing the subject would mean yielding control of when and what
data gets emitted by the data source, and we want to avoid that. We
want to ensure that only this class can emit values for the lessons data.

So we are also going to return an Observable derived from lessonsSubject


using the asObservable() method. This gives the data table (or any other
subscriber) the ability to subscribe to the lessons data observable,
without being able to emit values for that same observable.

The disconnect() method implementation


Let's now break down the implementation of the disconnect method:
1

2 BLOG COURSES FREE COURSE


disconnect(collectionViewer: NEWSLETTER
CollectionViewer): void {
3 this.lessonsSubject.complete();
4 this.loadingSubject.complete();

5 }

This method
21.ts
hosted is called
with ❤ by GitHubonce by the data table at component destruction
view raw

time. In this method, we are going to complete any observables that we


have created internally in this class, in order to avoid memory leaks.

We are going to complete both the lessonsSubject and the loadingSubject ,


which are then going to trigger the completion of any derived
observables.

The loadLessons() method implementation


Finally, let's now focus on the implementation of the loadLessons
method:

2 loadLessons(courseId: number, filter = '',


3 sortDirection = 'asc', pageIndex = 0, pageSize = 3) {

5 this.loadingSubject.next(true);
6

7 this.coursesService.findLessons(courseId, filter, sortDirection,


8 pageIndex, pageSize).pipe(
9 catchError(() => of([])),

10 finalize(() => this.loadingSubject.next(false))


11 )
12 .subscribe(lessons => this.lessonsSubject.next(lessons));

13 }

22.ts
hosted with ❤ by GitHub view raw

The Data Source exposes this public method named loadLessons() . This
method is going to be called in response to multiple user actions
(pagination, sorting, filtering) to load a given data page.

Here is how this method works:


the first thing that we will do is to report that some data is being
BLOG COURSES FREE COURSE NEWSLETTER
loaded, by emitting true to the loadingSubject , which will cause
loading$ to also emit true

the LessonsService is going to be used to get a data page from the


REST backend

a call to findLessons() is made, that returns an Observable

by subscribing to that observable, we trigger an HTTP request

if the data arrives successfully from the backend, we are going to


emit it back to the data table, via the connect() Observable

for that, we will call next() on the lessonsSubject with the lessons
data

the derived lessons observable returned by connect() will then


emit the lessons data to the data table

Handling Backend Errors


Let's now see, still in the loadLessons() method, how the Data Source
handles backend errors, and how the loading indicator is managed:

if an error in the HTTP request occurs, the Observable returned


by findLessons() will error out

If that occurs, we are going to catch that error using catchError()


and we are going to return an Observable that emits the empty
array using of

we could complementary also use another MessagesService to


show a closable error popup to the user

wether the call to the backend succeeds or fails, we will in both


cases have the loading$ Observable emit false by using finalize()
(which works like finally in plain Javascript try/catch/finally)

And with this last bit, we have completed the review of our custom Data
Source!

This version of the data source will support all our use cases: pagination,
sorting and filtering. As we can see, the design is all about providing
dataBLOG COURSES
transparently to theFREE
DataCOURSE NEWSLETTER
Table using an Observable-based API.

Let's now see how we can take this Data Source and plug it into the Data
Table.

Linking a Data Source with the Data Table


The Data Table will be displayed as part of the template of a
component. Let's write an initial version of that component, that displays
the first page of lessons:

2 @Component({

3 selector: 'course',

4 templateUrl: './course.component.html',

5 styleUrls: ['./course.component.css']

6 })
7 export class CourseComponent implements OnInit {
8

9 dataSource: LessonsDataSource;

10 displayedColumns= ["seqNo", "description", "duration"];

11

12 constructor(private coursesService: CoursesService) {}


13

14 ngOnInit() {

15 this.dataSource = new LessonsDataSource(this.coursesService);

16 this.dataSource.loadLessons(1);

17 }

18 }

10.ts
hosted with ❤ by GitHub view raw
BLOG COURSES FREE COURSE NEWSLETTER

This component contains a couple of properties:

the displayedColumns array defines the visual order of the columns

The dataSource property defines an instance of LessonsDataSource ,


and that is being passed to mat-table via the template

Breaking down the ngOnInit method


In the ngOnInit method, we are calling the Data Source loadLessons()
method to trigger the loading of the first lessons page. Let's detail what
happens as a result of that call:

The Data Source calls the LessonsService , which triggers an HTTP


request to fetch the data

The Data Source then emits the data via the lessonsSubject , which
causes the Observable returned by connect() to emit the lessons
page

The mat-table Data Table component has subscribed to the


connect() observable and retrieves the new lessons page

The Data Table then displays the new lessons page, without
knowing where the data came from or what triggered its arrival

And with this "glue" component in place, we now have a working Data
Table that displays server data!

The problem is that this initial example is always loading only the first
page of data, with a page size of 3 and with no search criteria.

Let's use this example as a starting point, and starting adding: a loading
indicator, pagination, sorting, and filtering.

Displaying a Material Loading Indicator


In order to display the loading indicator, we are going to be using the loa
BLOG COURSES FREE COURSE NEWSLETTER
ding$ observable of the Data Source. We will be using the mat-spinner
Material component:

2 <div class="course">

4 <div class="spinner-container" *ngIf="dataSource.loading$ | async">

5 <mat-spinner></mat-spinner>
6 </div>

8 <mat-table class="lessons-table mat-elevation-z8" [dataSource]="dataSou


9 ....
10 </mat-table>

11 </div>

11.ts
hosted with ❤ by GitHub view raw

As we can see, we are using the async pipe and ngIf to show or hide the
material loading indicator. Here is what the table looks like while the
data is loading:
BLOG COURSES FREE COURSE NEWSLETTER

We will also be using the loading indicator when transitioning between


two data pages using pagination, sorting or filtering.

Adding a Data Table Material Paginator


The Material Paginator component that we will be using is a generic
paginator that comes with an Observable-based API. This paginator
could be used to paginate anything, and it's not specifically linked to the
Data Table.

For example, on a Master-Detail component setup, we could use this


paginator to navigate between two detail elements.

This is how the mat-paginator component can be used in a template:


1

2 BLOG
<div COURSES
class="course"> FREE COURSE NEWSLETTER
3

4 <div class="spinner-container" *ngIf="dataSource.loading$ | async">

5 <mat-spinner></mat-spinner>

6 </div>

8 <mat-table class="lessons-table mat-elevation-z8" [dataSource]="dataSou

As9we can see, there is nothing in the template linking the paginator with
....

10 </mat-table>
either the Data Source or the Data Table - that connection will be done at
11

the level of the CourseComponent .


<mat-paginator [length]="course?.lessonsCount" [pageSize]="3"
12
13 [pageSizeOptions]="[3, 5, 10]"></mat-paginator>
The
14 paginator

only needs to know how many total items are being
paginated
15 </div>(via the length property), in order to know how many total

pages there are!


12.ts
hosted with ❤ by GitHub view raw

Its based on that information (plus the current page index) that the
paginator will enable or disable the navigation buttons.

In order to pass that information to the paginator, we are using the lesson
sCount property of a new course object.

How to Link the Material Paginator to the Data


Source
Let's now have a look at the CourseComponent , to see where course is
coming from and how the paginator is linked to the Data Source:
1

2 BLOG COURSES
@Component({ FREE COURSE NEWSLETTER
3 selector: 'course',
4 templateUrl: './course.component.html',

5 styleUrls: ['./course.component.css']

6 })

7 export class CourseComponent implements AfterViewInit, OnInit {

9 course:Course;

10 dataSource: LessonsDataSource;

11 displayedColumns= ["seqNo", "description", "duration"];

12

13 @ViewChild(MatPaginator) paginator: MatPaginator;

14

15 constructor(private coursesService: CoursesService, private route: Acti


16

17 ngOnInit() {

18 this.course = this.route.snapshot.data["course"];
19 this.dataSource = new LessonsDataSource(this.coursesService);
20 this.dataSource.loadLessons(this.course.id, '', 'asc', 0, 3);
21 }

22
23 ngAfterViewInit() {
24 this.paginator.page

25 .pipe(

26 tap(() => this.loadLessonsPage())


27 )
28 .subscribe();

29 }

30

31 loadLessonsPage() {

32 this.dataSource.loadLessons(

33 this.course.id,
34 '',

35 'asc',

36 this.paginator.pageIndex,
37 this.paginator.pageSize);

38 }

39 }

14.ts
hosted with ❤ by GitHub view raw
BLOG COURSES FREE COURSE NEWSLETTER

Breaking down the ngOnInit() method


Let's start with the course object: as we can see this object is available
at component construction time via the router.

This data object was retrieved from the backend at router navigation
time using a router Data Resolver (see an example here).

This is a very common design, that ensures that the target navigation
screen already has some pre-fetched data ready to display.

We are also loading the first page of data directly in this method (on line
20).

How is the Paginator linked to the Data Source?


We can see in the code above that the link between the paginator and
the Data Source is done in the ngAfterViewInit() method, so let's break it
down:
1

2 BLOG COURSES{
ngAfterViewInit() FREE COURSE NEWSLETTER
3 this.paginator.page
4 .pipe(

We
5 are using the AfterViewInit
tap(() lifecycle hook because we need to make
=> this.loadLessonsPage())

sure
6 that the) paginator component queried via @ViewChild is already
7 .subscribe();
available.
8 }

23.ts
hosted with ❤ by GitHub view raw
The paginator also has an Observable-based API, and it exposes a page
Observable. This observable will emit a new value every time that the
user clicks on the paginator navigation buttons or the page size
dropdown.

So in order to load new pages in response to a pagination event, all we


have to do is to subscribe to this observable, and in response to a
pagination event, we are going to make a call to the Data Source loadLes
sons() method, by calling loadLessonsPage() .

In that call to loadLessons() , we are going to pass to the Data Source what
page index we would like to load, and what page size, and that
information is taken directly from the paginator.

Why have we used the tap() operator?


We could also have done the call to the data source from inside a subscri
be() handler, but in this case, we have implemented that call using the
pipeable version of the RxJs do operator called tap .

View the Paginator in Action


And with this in place, we now have a working Material Paginator! Here
is what the Material Paginator looks like on the screen, while displaying
page 2 of the lessons list:
BLOG COURSES FREE COURSE NEWSLETTER

Let's now continue to add more features to our example, let's add
another very commonly needed feature: sortable table headers.

Adding Sortable Material Headers


In order to add sortable headers to our Data Table, we will need to
annotate it with the matSort directive. In this case, we will make only one
column in the table sortable, the seqNo column.
BLOG COURSES FREE COURSE NEWSLETTER
Here is what the template with all the multiple sort-related directives
looks like:

2 <mat-table class="lessons-table mat-elevation-z8" [dataSource]="dataSource"

3 matSort matSortActive="seqNo" matSortDirection="asc" matSortDisa


4

5 <ng-container matColumnDef="seqNo">

6 <mat-header-cell *matHeaderCellDef mat-sort-header>#</mat-header-ce

7 <mat-cell *matCellDef="let lesson">{{lesson.seqNo}}</mat-cell>


8 </ng-container>
9

10 ....
11

12 </mat-table>

15.html
hosted with ❤ by GitHub view raw

Besides the matSort directive, we are also adding a couple of extra


auxiliary sort-related directives to the mat-table component:

matSortActive: When the data is passed to the Data Table, its


usually already sorted. This directive allows us to inform the Data
Table that the data is already initally sorted by the seqNo column,
so the seqNo column sorting icon will be displayed as an upwards
arrow

matSortDirection: This is a companion directive to matSortActive ,


it specifies the direction of the initial sort. In this case, the data is
initially sorted by the seqNo column in ascending order, and so
the column header will adapt the sorting icon accordingly
(screenshot below)

matSortDisableClear: Sometimes, besides ascending and


descending order we might want a third "unsorted" state for the
sortable column header, where we can clear the sorting order. In
this case, we want to disable that to make sure the seqNo
column always shown either the ascending or descending states
ThisBLOG
is the sort configuration
COURSES FREEfor the whole
COURSE data table, but we also need
NEWSLETTER
to identify exactly what table headers are sortable!

In our case, only the seqNo column is sortable, so we are annotating the
column header cell with the mat-sort-header directive.

And this covers the template changes, let's now have a look at the
changes we made to the CourseComponent in order to enable table header
sorting.

Linking the Sortable column header to the Data


Source
Just like the case of pagination, the sortable header will expose an
Observable that emits values whenever the user clicks on the sortable
column header.

The MatSort directive then exposes a sort Observable, that can trigger a
new page load in the following way:
1

2 BLOG COURSES
@Component({ FREE COURSE NEWSLETTER
3 selector: 'course',
4 templateUrl: './course.component.html',

5 styleUrls: ['./course.component.css']

6 })

7 export class CourseComponent implements AfterViewInit, OnInit {

9 course:Course;

10 dataSource: LessonsDataSource;

11 displayedColumns= ["seqNo", "description", "duration"];

12

13 @ViewChild(MatPaginator) paginator: MatPaginator;

14 @ViewChild(MatSort) sort: MatSort;

15

16 constructor(private coursesService: CoursesService, private route: Acti


17

18 ngOnInit() {
19 this.course = this.route.snapshot.data["course"];
20 this.dataSource = new LessonsDataSource(this.coursesService);
21 this.dataSource.loadLessons(this.course.id, '', 'asc', 0, 3);

22 }
23
24 ngAfterViewInit() {

25
26 // reset the paginator after sorting
27 this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
28
29 merge(this.sort.sortChange, this.paginator.page)

30 .pipe(

31 tap(() => this.loadLessonsPage())

32 )

33 .subscribe();
34 }

35

As
36we can see, the sort Observable
loadLessonsPage() { is now being merged with the page
observable!
37 Now a new page load will be triggered in two cases:
this.dataSource.loadLessons(

38 this.course.id, '', this.sort.direction,

39 this.paginator.pageIndex, this.paginator.pageSize);
when a pagination event occurs
40 }

41 when
} a sort event occurs

16.ts
hosted with ❤ by GitHub view raw
The sort direction of the seqNo column is now taken from the sort
directive (injected via @ViewChild() ) to the backend.
Notice that after
BLOG each sort
COURSES weCOURSE
FREE are also resetting the paginator, by forcing
NEWSLETTER
the first page of the sorted data to be displayed.

The Material Sort Header In Action


Here is what the Data Table with sortable headers looks like, after
loading the data and clicking the sortable header (triggering a
descending sort by seqNo ):
BLOG COURSES FREE COURSE NEWSLETTER

Notice the sort icon on the seqNo column

At this point, we have server pagination and sorting in place. We are now
ready to add the final major feature: server-side filtering.

Adding Server-Side Filtering


In order to implement server-side filtering, the first thing that we need to
do is to add a search box to our template.

And because this is the final version, let's then display the complete
template with all its features: pagination, sorting and also server-side
filtering:
1

2 BLOG
<div COURSES
class="course"> FREE COURSE NEWSLETTER
3

4 <!-- New part: this is the search box -->

5 <mat-input-container>

6 <input matInput placeholder="Search lessons" #input>

7 </mat-input-container>

9 <div class="spinner-container" *ngIf="dataSource.loading$ | async">

10 <mat-spinner></mat-spinner>

11 </div>

12

13 <mat-table class="lessons-table mat-elevation-z8" [dataSource]="dataSou

14 matSort matSortActive="seqNo" matSortDirection="asc" matSort

15

16 <ng-container matColumnDef="seqNo">
17 <mat-header-cell *matHeaderCellDef mat-sort-header>#</mat-heade

18 <mat-cell *matCellDef="let lesson">{{lesson.seqNo}}</mat-cell>


19 </ng-container>
20

21 <ng-container matColumnDef="description">

22 <mat-header-cell *matHeaderCellDef>Description</mat-header-cell
23 <mat-cell class="description-cell"
24 *matCellDef="let lesson">{{lesson.description}}</mat-

25 </ng-container>

26

27 <ng-container matColumnDef="duration">
28 <mat-header-cell *matHeaderCellDef>Duration</mat-header-cell>

29 <mat-cell class="duration-cell"

30 *matCellDef="let lesson">{{lesson.duration}}</mat-cel

31 </ng-container>

32

Breaking
33
down the Search Box implementation
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row
34 <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
As
35we
can see, the only new part in this final template version is the mat-
input-container
36 , containing the
</mat-table> Material Input box where the user types the

search
37
query.
38 <mat-paginator [length]="course?.lessonsCount" [pageSize]="3"

39 [pageSizeOptions]="[3, 5, 10]"></mat-paginator>
This
40
input box follows a common pattern found in the Material library:
</div>
The mat-input-container is wrapping a plain HTML input and projecting it.
17.html
hosted with ❤ by GitHub view raw

This gives us full access to all standard input properties including for
example all the Accessibility-related properties. This also gives us
compatibility with Angular Forms, as we can apply Form directives
BLOG
directly in theCOURSES FREE COURSE
input HTML element. NEWSLETTER

Read more about how to build a similar component in this post: Angular
ng-content and Content Projection: The Complete Guide.

Notice that there is not even an event handler attached to this input box
! Let's then have a look at the component and see how this works.

Final Component with Server Pagination, Sorting and


Filtering
This is the final version of CourseComponent with all features included:

2 @Component({

3 selector: 'course',

4 templateUrl: './course.component.html',
5 styleUrls: ['./course.component.css']
6 })

7 export class CourseComponent implements OnInit, AfterViewInit {


8

9 course:Course;

10 dataSource: LessonsDataSource;
11 displayedColumns= ["seqNo", "description", "duration"];

12

13 @ViewChild(MatPaginator) paginator: MatPaginator;


14 @ViewChild(MatSort) sort: MatSort;

15 @ViewChild('input') input: ElementRef;


16

17 constructor(

18 private route: ActivatedRoute,


19 private coursesService: CoursesService) {}
20

21 ngOnInit() {

22 this.course = this.route.snapshot.data["course"];
23 this.dataSource = new LessonsDataSource(this.coursesService);

24 this.dataSource.loadLessons(this.course.id, '', 'asc', 0, 3);

25 }

26

27 ngAfterViewInit() {

28

29 // server-side search

30 fromEvent(this.input.nativeElement,'keyup')
30 fromEvent(this.input.nativeElement, keyup )
31 .pipe(
BLOG COURSES FREE COURSE
debounceTime(150),
NEWSLETTER
32
33 distinctUntilChanged(),
34 tap(() => {

35 this.paginator.pageIndex = 0;

36 this.loadLessonsPage();
37 })

38 )

39 .subscribe();
40

41 // reset the paginator after sorting

42 this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);

43

44 // on sort or paginate events, load a new page


45 merge(this.sort.sortChange, this.paginator.page)

46 .pipe(

47 tap(() => this.loadLessonsPage())


48 )

49 .subscribe();

50 }
51

52 loadLessonsPage() {
53 this.dataSource.loadLessons(

54 this.course.id,

55 this.input.nativeElement.value,
56 this.sort.direction,

57 this.paginator.pageIndex,

58 this.paginator.pageSize);
59 }

60 }

18.ts
hosted with ❤ by GitHub view raw

Let's then focus on breaking down the server filtering part.

Getting a reference to the Search Input Box


We can see that we have injected a DOM reference to the <input>
element using @ViewChild('input') . Notice that this time around, the
injection mechanism gave us a reference to a DOM element and not to a
component.
With that DOM reference, here is the part that triggers a server-side
BLOG
search whenCOURSES FREE COURSE
the user types NEWSLETTER
in a new query:

2 // server-side search
3 fromEvent(this.input.nativeElement,'keyup')

4 .pipe(

5 debounceTime(150),

6 distinctUntilChanged(),

7 tap(() => {
8 this.paginator.pageIndex = 0;
9 this.loadLessonsPage();

10 })

11 )

12 .subscribe();

19.ts
hosted with ❤ by GitHub view raw

What we are doing in this is snippet is: we are taking the search input
box and we are creating an Observable using fromEvent .

This Observable will emit a value every time that a new keyUp event
occurs. To this Observable we will then apply a couple of operators:

debounceTime(150) : The user can type quite quickly in the input box,
and that could trigger a lot of server requests. With this operator,
we are limiting the amount of server requests emitted to a
maximum of one every 150ms.

distinctUntilChanged() : This operator will eliminate duplicate values

And with these two operators in place, we can now trigger a page load
by passing the query string, the page size and page index to the the
Data Source via the tap() operator.

Let's now have a look at the what the screen would look like if the user
types the search term "hello":
BLOG COURSES FREE COURSE NEWSLETTER

And with this in place, we have completed our example! We now have a
complete solution for how to implement an Angular Material Data Table
with server-side pagination, sorting and filtering.
Let'sBLOG
now quickly summarize
COURSES FREE what we have
COURSE learned.
NEWSLETTER

Conclusions
The Data Table, the Data Source and related components are a good
example of a reactive design that uses an Observable-based API. Let's
highlight the key points of the design:

the Material Data Table expects to receive the data from the
Data Source via an Observable

The Data Source main role is to build and provide an Observable


that emits new versions of the tabular data to the Data Table

A component class like CourseService will then "glue" everything


together

This reactive design helps to ensure the loose coupling of the multiple
elements involved, and provides a strong separation of concerns.

Source Code + Github Running Example


A running example of the complete code is available here on this branch
on Github, and it includes a small backend Express server that serves
the data and does the server-side sorting/pagination/filtering.

I hope that this post helps with getting started with the Angular Material
Data Table and that you enjoyed it!

If you would like to learn a lot more about Angular Material, we


recommend checking the Angular Material Course, where we cover
different types of widgets in much more detail.

If you have some questions or comments please let me know in the


comments below and I will get back to you.

To get notified of upcoming posts on Angular Material and other Angular


topics, I invite you to subscribe to our newsletter:
BLOG COURSES FREE COURSE NEWSLETTER

Angular University
Watch 25% of all Angular Video Courses, get timely Angular News and PDFs: 

Email*

Subscribe & Get Free Course

If you are just getting started learning Angular, have a look at the
Angular for Beginners Course:

Other Angular Material posts:


Angular Material Dialog: A Complete Example
BLOG COURSES FREE COURSE NEWSLETTER

ALSO ON ANGULAR UNIVERSITY

Angular Reactive RxJs Error Handling: Angular Dependency


Templates with ngIf … Complete Practical … Injection: Complete …

4 years ago • 13 comments 3 years ago • 31 comments 3 months ago • 4 comments

Learn about the Angular Error handling is an Everything that you need to
ngIf else syntax in detail, essential part of RxJs, as we know in practice to use the
including how it … will need it in just about … Angular dependency …

1 Comment Angular University 🔒 Disqus' Privacy Policy 


1 Login

 Favorite 3 t Tweet f Share Sort by Best

Join the discussion…

LOG IN WITH
OR SIGN UP WITH DISQUS ?

Name

Eduard Hasanaj • 4 years ago


These blog posts are very helpful. Sorry for not bing able to donate to this wonderful blog
△ ▽ • Reply • Share ›

✉ Subscribe d Add Disqus to your siteAdd DisqusAdd ⚠ Do Not Sell My Data

MORE IN ANGULAR MATERIAL

Angular Material Dialog: A Complete Example


22 Feb 2018 –
5 min read

1 post

ANGULAR CORE
BLOG COURSES FREE COURSE NEWSLETTER
Angular Debugging "Expression has changed after it was
checked": Simple Explanation (and Fix)
In this post, we will cover in detail an error message that you will occasionally come across
while building Angular applications: "Expression has changed after it was checked" -
ExpressionChangedAfterItHasBeenCheckedError. We are going to give a complete
explanation about this error. We

ANGULAR UNIVERSITY
17 DEC 2020 • 9 MIN READ

ANGULAR PWA

Angular Service Worker - Step-By-Step Guide for turning your


Application into a PWA
With the Angular Service Worker and the Angular CLI built-in PWA support, it's now simpler
than ever to make our web application downloadable and installable, just like a native
mobile application. In this post, we will cover how we can configure the Angular CLI
ANGULAR UNIVERSITY
17 DEC 2020 • 17 MIN READ

Angular University © 2021


Latest Posts Facebook Twitter Ghost
BLOG COURSES FREE COURSE NEWSLETTER

You might also like