Angular Material Data Table - A Complete Example
Angular Material Data Table - A Complete Example
ANGULAR MATERIAL
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.
Table Of Contents
In this post, we will cover the following topics:
The Angular Material Data Table - not only for Material Design
A Loading Indicator
Conclusions
So without further ado, let's get started with our Material Data Table
Guided Tour!
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:
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>
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
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 .
4 <ng-container matColumnDef="seqNo">
5 <mat-header-cell *matHeaderCellDef>#</mat-header-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
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.
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!
3 (click)="onRowClicked(row)">
4 </mat-row>
05.html
hosted with ❤ by GitHub view raw
2 onRowClicked(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.
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.
Here are some possible causes for the emission of new data:
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.
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.
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(
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(
20 );
21 }
22 }
23
24
07.ts
hosted with ❤ by GitHub view raw
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.
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
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
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.
12 }
13
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
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.
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:
2 BLOG COURSES
export class FREE COURSE
LessonsDataSource NEWSLETTER
implements DataSource<Lesson> {
12 return this.lessonsSubject.asObservable();
13 }
14
18 }
19
23 this.loadingSubject.next(true);
24
09.ts
hosted with ❤ by GitHub view raw
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.
5 }
This method
21.ts
hosted is called
with ❤ by GitHubonce by the data table at component destruction
view raw
5 this.loadingSubject.next(true);
6
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.
for that, we will call next() on the lessonsSubject with the lessons
data
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.
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;
11
14 ngOnInit() {
16 this.dataSource.loadLessons(1);
17 }
18 }
10.ts
hosted with ❤ by GitHub view raw
BLOG COURSES FREE COURSE NEWSLETTER
The Data Source then emits the data via the lessonsSubject , which
causes the Observable returned by connect() to emit the 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.
2 <div class="course">
5 <mat-spinner></mat-spinner>
6 </div>
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
2 BLOG
<div COURSES
class="course"> FREE COURSE NEWSLETTER
3
5 <mat-spinner></mat-spinner>
6 </div>
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
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.
2 BLOG COURSES
@Component({ FREE COURSE NEWSLETTER
3 selector: 'course',
4 templateUrl: './course.component.html',
5 styleUrls: ['./course.component.css']
6 })
9 course:Course;
10 dataSource: LessonsDataSource;
12
14
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(
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
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).
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.
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.
Let's now continue to add more features to our example, let's add
another very commonly needed feature: sortable table headers.
5 <ng-container matColumnDef="seqNo">
10 ....
11
12 </mat-table>
15.html
hosted with ❤ by GitHub view raw
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.
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 })
9 course:Course;
10 dataSource: LessonsDataSource;
12
15
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(
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(
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.
At this point, we have server pagination and sorting in place. We are now
ready to add the final major feature: server-side filtering.
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
5 <mat-input-container>
7 </mat-input-container>
10 <mat-spinner></mat-spinner>
11 </div>
12
15
16 <ng-container matColumnDef="seqNo">
17 <mat-header-cell *matHeaderCellDef mat-sort-header>#</mat-heade
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.
2 @Component({
3 selector: 'course',
4 templateUrl: './course.component.html',
5 styleUrls: ['./course.component.css']
6 })
9 course:Course;
10 dataSource: LessonsDataSource;
11 displayedColumns= ["seqNo", "description", "duration"];
12
17 constructor(
21 ngOnInit() {
22 this.course = this.route.snapshot.data["course"];
23 this.dataSource = new LessonsDataSource(this.coursesService);
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
43
46 .pipe(
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
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.
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
This reactive design helps to ensure the loose coupling of the multiple
elements involved, and provides a strong separation of concerns.
I hope that this post helps with getting started with the Angular Material
Data Table and that you enjoyed it!
Angular University
Watch 25% of all Angular Video Courses, get timely Angular News and PDFs:
Email*
If you are just getting started learning Angular, have a look at the
Angular for Beginners Course:
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 …
LOG IN WITH
OR SIGN UP WITH DISQUS ?
Name
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