1. Overview
Angular Material is most often useful and saves you work a lot building directives and components from scratch, but sometimes we need a little more control over behaviors and events. In this article we'll learn how we can do a custom sorting on a material table.
2.Setup
First, we'll create a new Angular project.
npm install -g @angular/cli
ng new mat-table-custom-sort
cd mat-table-custom-sort
ng serve
Then we'll use the Angular CLI's install schematic to set up the Angular Material project by running:
ng add @angular/material
We'll build the project and open it in the browser (the default port is 4200, but you can change it if you want to)
ng serve
The last step is to delete the whole HTML from app.component.html and leave it empty. Now that we have an empty project, we'll add a simple table with a hardcoded dataSource (of course we can use an API to fetch data from a server, but that is not the purpose of this article, so we'll keep it simple). For that we must import and export MatTableModule and MatSortModule in app.component.ts:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {MatTableModule} from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatTableModule,
MatSortModule
],
exports: [
MatTableModule,
MatSortModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Now, we'll create a simple table in the app.component.html file:
<mat-table [dataSource]="dataSource" matSort class="mat-elevation-z8">
<ng-container matColumnDef="hasVoucher">
<mat-header-cell *matHeaderCellDef mat-sort-header> hasVoucher </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.hasVoucher}} </mat-cell>
</ng-container>
<ng-container matColumnDef="total">
<mat-header-cell *matHeaderCellDef mat-sort-header> total </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.total}} </mat-cell>
</ng-container>
<ng-container matColumnDef="lastName">
<mat-header-cell *matHeaderCellDef mat-sort-header> lastName </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.lastName}} </mat-cell>
</ng-container>
<ng-container matColumnDef="firstName">
<mat-header-cell *matHeaderCellDef mat-sort-header> firstName </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.firstName}} </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
matColumnDef name and *matCellDef actual value name should be same!
In the typescript file of the component(app.component.ts), we'll declare the dataSource, the array of the columns we want to display, the model of the object we display in our table. So, we declared our Customer interface:
export interface Customer{
firstName: string;
lastName: string;
address: string;
hasVoucher: boolean;
total: number;
}
Then we initialize our dataSource and matSort reference:
@ViewChild(MatSort)
sort: MatSort = new MatSort();
dataSource: MatTableDataSource<Customer>;
displayedColumns = [ 'firstName', 'lastName', 'total', 'hasVoucher'];
constructor(){
this.dataSource = new MatTableDataSource<Customer>(this.customers);
}
ngOnInit(){
setTimeout(() => {
this.dataSource.sort = this.sort;
});
}
We need the timeout for sort, sometimes there are problems with mat-sort and this is a quick fix for that.
You can see below how the table looks, when you hover over a field name from the header you'll see the arrow, when you click it you'll have the sort.
Now we have the default behavior for matSort, there are 3 states:
- Default (items are displayed in the order they are fetched into the dataSource)
- Ascending (by the field you clicked on)
- Descending (also by the field you clicked on)
The main problem for this is the default state, usually we only want ascending-descending sorting because the default one is unused. For this we'll use matSortChange event:
<mat-table class="tableLayout" matSort (matSortChange)="sortData()" [dataSource]="dataSource">
And for the default case we're going to return the ascending order, so now if you toggle the sorting, you're going to have: ascending-descending-ascending-descending-... (no default sorting)
public sortData(): void {
switch (this.sort.direction) {
case 'asc':
this.sort.direction = 'asc';
break;
case 'desc':
this.sort.direction = 'desc';
break;
default:
this.sort.direction = 'asc';
}
}
Now, what if I'll add one more customer, but his first name will be written with lowercase:
{
firstName: 'Joe',
lastName: 'Backer',
address: '',
hasVoucher: true,
total: 546.996
}
We can see now that when we sort by first name ascending, our customer "joe" won't be under "Joe", but he'll be the last customer displayed in the table. Why? Because matSort in case sensitive. To make sure that our sort works fine, we must overwrite sortingDataAccessor.
ngOnInit() {
setTimeout(() => {
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (item, property) => {
return item[property].toLocaleLowerCase();
};
});
}
To make sure that we get no error(such as: "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Customer'. No index signature with a parameter of type 'string' was found on type 'Customer'."), we'll change the model:
export interface Customer {
[key: string]: any;
firstName: string;
lastName: string;
address: string;
hasVoucher: boolean;
total: number;
}
Now the case with "joe" and "Joe" customers is solved, but we can see in our dataSource that we have 3 customers whose first name are Joe so what if we want to sort them by last name? So in case we have two customers with the same first Name, we'll want to sort them by last Name. We have to overwrite sortData method for our dataSource.
In our ngOnInit method, under sortingDataAccesor:
this.dataSource.sortData = (data: Customer[], sort: MatSort) => {
const active = sort.active;
const direction = sort.direction;
return data.sort((a: Customer, b: Customer) => {
const valueA = this.dataSource.sortingDataAccessor(a, active);
const valueB = this.dataSource.sortingDataAccessor(b, active);
let comparatorResult = this.compareFunction(valueA, valueB);
// if they are equals
if (comparatorResult == 0)
{
const valueALastName = this.dataSource.sortingDataAccessor(
a,
'lastName'
);
const valueBLastName = this.dataSource.sortingDataAccessor(
b,
'lastName'
);
comparatorResult = this.compareFunction(
valueALastName,
valueBLastName
);
}
return comparatorResult * (direction == 'asc' ? 1 : -1);
});
};
And we need a new method with Comparator role:
private compareFunction(a: any, b: any): number {
let comparatorResult = 0;
if (a != null && b != null) {
// Check if one value is greater than the other; if equal, comparatorResult should remain 0.
if (a > b) {
comparatorResult = 1;
} else if (a < b) {
comparatorResult = -1;
}
} else if (a != null) {
comparatorResult = 1;
} else if (b != null) {
comparatorResult = -1;
}
return comparatorResult;
}
Now, for these 3 customers named "Joe" we'll have a second sort based on their last Name, of course now you can change this method as it fits for you, maybe you need sorting based on more fields or a more complex logic, you can change it however it's best for your project.
A fully functional demo is available here.