Angular Material Table - Server Side Filtering
Dharmen Shah
Posted on October 7, 2024
Overview
The Angular Material Table component is used to display data in a tabular format. It provides a flexible and customizable way to display data, including features like sorting, pagination, and filtering.
Angular Material team provides sorting and pagination using MatSort
and MatPaginator
respectively. For server side filtering, we need to implement a custom logic. Let's see how to do that.
Creating a table with server side data
Let's create a table with server side data.
1. Sample database
We will use GutHub API for this example. Create a file src\app\table\database.ts
with below content:
import { HttpClient } from '@angular/common/http';
import { SortDirection } from '@angular/material/sort';
import { Observable } from 'rxjs';
export interface GithubApi {
items: GithubIssue[];
total_count: number;
}
export interface GithubIssue {
created_at: string;
number: string;
state: string;
title: string;
}
export class ExampleHttpDatabase {
constructor(private _httpClient: HttpClient) {}
getRepoIssues(
sort: string,
order: SortDirection,
page: number,
pageSize = 10,
query = ''
): Observable<GithubApi> {
const href = 'https://api.github.com/search/issues';
const requestUrl = `${href}?q=${encodeURIComponent(
query + ' ' + 'repo:angular/components'
)}&sort=${sort}&order=${order}&page=${page + 1}&per_page=${pageSize}`;
return this._httpClient.get<GithubApi>(requestUrl);
}
}
2. Table Data-source
Create a file src\app\table\data-source.ts
with below content:
import { DataSource } from '@angular/cdk/collections';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { Observable, of as observableOf, merge } from 'rxjs';
import { GithubIssue, ExampleHttpDatabase } from './database';
import { signal } from '@angular/core';
// TODO: Replace this with your own data model type
export interface TableItem extends GithubIssue {}
/**
* Data source for the Table view. This class should
* encapsulate all logic for fetching and manipulating the displayed data
* (including sorting, pagination, and filtering).
*/
export class TableDataSource extends DataSource<TableItem> {
data: TableItem[] = [];
paginator: MatPaginator | undefined;
sort: MatSort | undefined;
database: ExampleHttpDatabase | undefined;
resultsLength = signal(0);
isLoadingResults = signal(true);
isRateLimitReached = signal(false);
constructor() {
super();
}
/**
* Connect this data source to the table. The table will only update when
* the returned stream emits new items.
* @returns A stream of the items to be rendered.
*/
connect(): Observable<TableItem[]> {
if (this.paginator && this.sort && this.database) {
// Combine everything that affects the rendered data into one update
// stream for the data-table to consume.
return merge(this.paginator.page, this.sort.sortChange).pipe(
startWith({}),
switchMap(() => {
this.isLoadingResults.set(true);
return this.database!.getRepoIssues(
this.sort!.active,
this.sort!.direction,
this.paginator!.pageIndex,
this.paginator!.pageSize
).pipe(
catchError(() => observableOf({ items: [], total_count: 0 })),
map((data) => {
// Flip flag to show that loading has finished.
this.isLoadingResults.set(false);
this.isRateLimitReached.set(data === null);
this.resultsLength.set(data.total_count);
return data.items;
})
);
})
);
} else {
throw Error(
'Please set the paginator, sort and database on the data source before connecting.'
);
}
}
/**
* Called when the table is being destroyed. Use this function, to clean up
* any open connections or free any held resources that were set up during connect.
*/
disconnect(): void {}
}
3. Table component
Create a file src\app\table\table.component.ts
with below content:
import {
AfterViewInit,
Component,
inject,
signal,
ViewChild,
computed,
} from '@angular/core';
import { MatTableModule, MatTable } from '@angular/material/table';
import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator';
import { MatSortModule, MatSort } from '@angular/material/sort';
import { TableDataSource, TableItem } from './table-datasource';
import { ExampleHttpDatabase } from './database';
import { HttpClient } from '@angular/common/http';
import { DatePipe } from '@angular/common';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@Component({
selector: 'app-table',
templateUrl: './table.component.html',
styleUrl: './table.component.scss',
standalone: true,
imports: [
MatTableModule,
MatPaginatorModule,
MatSortModule,
DatePipe,
MatProgressSpinnerModule,
],
})
export class TableComponent implements AfterViewInit {
private _httpClient = inject(HttpClient);
private database = new ExampleHttpDatabase(this._httpClient);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatTable) table!: MatTable<TableItem>;
dataSource: TableDataSource;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns: string[] = ['created', 'state', 'number', 'title'];
constructor() {
this.dataSource = new TableDataSource();
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
this.dataSource.database = this.database;
this.table.dataSource = this.dataSource;
}
}
4. Table component template
Create a file src\app\table\table.component.html
with below content:
<div class="example-container mat-elevation-z8">
@if (dataSource.isLoadingResults() || dataSource.isRateLimitReached()) {
<div class="example-loading-shade">
@if (dataSource.isLoadingResults()) {
<mat-spinner></mat-spinner>
}
@if (dataSource.isRateLimitReached()) {
<div class="example-rate-limit-reached">
GitHub's API rate limit has been reached. It will be reset in one minute.
</div>
}
</div>
}
<div class="example-table-container">
<table mat-table class="example-table"
matSort matSortActive="created" matSortDisableClear matSortDirection="desc">
<!-- Number Column -->
<ng-container matColumnDef="number">
<th mat-header-cell *matHeaderCellDef>#</th>
<td mat-cell *matCellDef="let row">{{row.number}}</td>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>Title</th>
<td mat-cell *matCellDef="let row">{{row.title}}</td>
</ng-container>
<!-- State Column -->
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef>State</th>
<td mat-cell *matCellDef="let row">{{row.state}}</td>
</ng-container>
<!-- Created Column -->
<ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear>
Created
</th>
<td mat-cell *matCellDef="let row">{{row.created_at | date}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<mat-paginator [length]="dataSource.resultsLength()" [pageSize]="30" aria-label="Select page of GitHub search results"></mat-paginator>
</div>
5. Table component style
Create a file src\app\table\table.component.scss
with below content:
.example-container {
position: relative;
}
.example-table-container {
position: relative;
min-height: 200px;
max-height: 400px;
overflow: auto;
}
table {
width: 100%;
}
.example-loading-shade {
position: absolute;
top: 0;
left: 0;
bottom: 56px;
right: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.example-rate-limit-reached {
max-width: 360px;
text-align: center;
}
/* Column Widths */
.mat-column-number,
.mat-column-state {
width: 64px;
}
.mat-column-created {
width: 124px;
}
If you look at the output, you will see sorting and pagination working as expected.
Server side filtering
To implement server side filtering, we will use angular forms.
1. Creating form
We will create a FormGroup
to handle value entered by user and perform action on events.
Make changes in src\app\table\table.component.ts
:
@Component({
imports: [
// rest remains same...
MatFormFieldModule,// [!code ++]
MatInputModule,// [!code ++]
ReactiveFormsModule,// [!code ++]
],
})
export class TableComponent implements AfterViewInit {
formGroup = new FormGroup(// [!code ++]
{// [!code ++]
textFilter: new FormControl(''),// [!code ++]
},// [!code ++]
{ updateOn: 'submit' }// [!code ++]
);// [!code ++]
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
this.dataSource.textFilter = this.formGroup.controls.textFilter;// [!code ++]
this.dataSource.database = this.database;
this.table.dataSource = this.dataSource;
}
}
Let's understand the code above.
- We have created a
FormGroup
class which has atextFilter
FormControl
. - We also make sure that events are emitted only on submit, like when user presses
Enter
. - Lastly, we are attaching
textFilter
to ourdataSource
. We will make changes indataSource
in a bit.
2. Using form in template
We will make changes in html template so render the form field for search and attaching it to the formGroup
:
<div class="example-table-container">
<form [formGroup]="formGroup"><!-- [!code ++] -->
<mat-form-field class="w-100"><!-- [!code ++] -->
<input formControlName="textFilter" matInput placeholder="Filter"><!-- [!code ++] -->
</mat-form-field><!-- [!code ++] -->
</form><!-- [!code ++] -->
</div>
3. Using FormControl
in datasource
We will use FormControl
in our datasource to filter the data, so that we can listen to the text filter change event and make a call to the server to filter the data.
export class TableDataSource extends DataSource<TableItem> {
textFilter: FormControl<string | null> | undefined;// [!code ++]
connect(): Observable<TableItem[]> {
if (this.paginator && this.sort && this.database && this.textFilter) {// [!code highlight]
// Combine everything that affects the rendered data into one update
// stream for the data-table to consume.
return merge(
this.paginator.page,
this.sort.sortChange,
this.textFilter.valueChanges// [!code ++]
).pipe(
startWith({}),
switchMap(() => {
this.isLoadingResults.set(true);
return this.database!.getRepoIssues(
this.sort!.active,
this.sort!.direction,
this.paginator!.pageIndex,
this.paginator!.pageSize,
this.textFilter!.value ?? ''// [!code ++]
).pipe(
catchError(() => observableOf({ items: [], total_count: 0 })),
map((data) => {
// Flip flag to show that loading has finished.
this.isLoadingResults.set(false);
this.isRateLimitReached.set(data === null);
this.resultsLength.set(data.total_count);
return data.items;
})
);
})
);
} else {
throw Error(
'Please set the paginator, sort and database on the data source before connecting.'
);
}
}
}
That's it. We have implemented server side filtering in Angular Material Table.
Live Playground
Posted on October 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.