Custom sorting and filtering for Angular material table

Pratiyush Prakash
Dev Genius
Published in
5 min readSep 12, 2022

--

In this article I will covering how to create custom filtering and sorting for Angular material table.

Final product — Angular material table

I am assuming you already the know basics of Angular and JavaScript. If not please go through it and then come back to this article later.

Okay, so let’s jump right in.

Requirement Discussion

We will be creating a material table with Student data and we will have sorting on each column and will have filter by subject.

Student interface

Our student interface will have name, class, section, subjects and marks. One thing to note here is that subjects and marks are array while others are primitive data types.

export interface Student {
name: string;
subjects: string[];
marks: number[];
class: string;
section: string;
}

Filtering requirements

We should have a filter, where on type it should filter the students by the subjects they are having. So for example if we start searching for “MAT” then it should return all the students who have any subjects whose substring is “MAT”.

Sorting requirements

For primitive data types columns we will have normal string comparison. But for subjects sorting we will compare with the number of subjects a student have. And for marks we will compare with the average of the marks.

Implementation

Okay, so let’s start with implementation.

First let’s create a simple table without filter and sort.

First the typescript part

  • We will be creating interface for the student.
  • Then we will define the displayedColumns which are the keys we are going to show in the template
  • Then we define columns which will contain columnDef , header and cell for each of our columns. The thing to note here is the cell where you can format your data the way you want to show in the template.
  • Then we have the dataSource and once you get the data you can update the dataSource . Here we are using constants so we can update it in ngOnIntit only.
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
export interface Student {
name: string;
subjects: string[];
marks: number[];
class: string;
section: string;
}
const ELEMENT_DATA: Student[] = [
{
name: 'Tony',
subjects: ['MATH', 'PHY', 'CHEM'],
marks: [90, 95, 97],
class: '12',
section: 'A',
},
{
name: 'Rita',
subjects: ['MATH', 'PHY', 'BIO'],
marks: [97, 92, 96],
class: '12',
section: 'A',
},
{
name: 'Monty',
subjects: ['MATH', 'PHY', 'BIO'],
marks: [80, 99, 100],
class: '12',
section: 'B',
},
{
name: 'Pintu',
subjects: ['GEOLOGY', 'HISTORY'],
marks: [90, 95],
class: '12',
section: 'C',
},
{
name: 'Sarah',
subjects: ['PAINTING', 'DANCE'],
marks: [97, 100],
class: '12',
section: 'C',
},
];
@Component({
selector: 'table-filtering-example',
styleUrls: ['table-filtering-example.css'],
templateUrl: 'table-filtering-example.html',
})
export class TableFilteringExample implements OnInit{
displayedColumns: string[] = [
'name',
'class',
'section',
'subjects',
'marks',
];
columns = [
{
columnDef: 'name',
header: 'Name',
cell: (element: Student) => `${element.name}`,
},
{
columnDef: 'class',
header: 'Class',
cell: (element: Student) => `${element.class}`,
},
{
columnDef: 'section',
header: 'Section',
cell: (element: Student) => `${element.section}`,
},
{
columnDef: 'subjects',
header: 'Subjects',
cell: (element: Student) => `${element.subjects.join(', ')}`,
},
{
columnDef: 'marks',
header: 'Marks',
cell: (element: Student) => `${element.marks.join(', ')}`,
},
];
dataSource: MatTableDataSource<Student>;ngOnInit() {
this.dataSource = new MatTableDataSource(ELEMENT_DATA);
}
}

Now the template

  • As we already have columns defined in the the typescript file we don’t have to define all the columns here specifically. We can just use *ngFor that will take care of all the columns.
  • Apart from that it is pretty much straightforward.
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<th mat-header-cell *matHeaderCellDef >{{column.header}}</th>
<td mat-cell *matCellDef="let row">{{column.cell(row)}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

Custom filtering

Angular material already allows to have normal filtering where we can pass a key and it will filter among all rows data and then return the result. But it won’t work for complex data like ours or special features like ours.

To override the Angular material’s filter method we can override the filterPredicate of dataSource by our own function.

ngOnInit() {
// In the ngOnInit we can override the filterPredicate
// by our function
this.dataSource.filterPredicate = this.filterBySubject();
}
// This is the method which get called from your filter input
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toUpperCase();
}
// This is our filter method.
// It has to return a function
// Method of the function should be
// (data: InterfaceName, filter: string): boolean
filterBySubject() {
let filterFunction =
(data: Student, filter: string): boolean => {
if (filter) {
const subjects = data.subjects;
for (let i = 0; i < subjects.length; i++) {
if (subjects[i].indexOf(filter) != -1) {
return true;
}
}
return false;
} else {
return true;
}
};
return filterFunction;
}

In the template we can just call applyFilter with the search term.

<mat-form-field appearance="standard">
<mat-label>
Filter by subject
</mat-label>
<input
matInput
(keyup)="applyFilter($event)"
placeholder="Filter by subject"
#input
/>
</mat-form-field>
</mat-form-field>

Custom Sorting

Similar to filtering, Angular material allows normal sorting with some easy steps. But that doesn’t work for special cases and complex data. For that we have to override the sortData method of dataSource by our own function.

@ViewChild(MatSort) sort: MatSort;ngOnInit() {
// here we override the sortData with our custom sort function
this.dataSource.sortData = this.sortData();
}
// datasource sort has to be updated with the
// template's sort.
ngAfterViewInit() {
this.dataSource.sort = this.sort;
}
// custom sort function
sortData() {
let sortFunction =
(items: Student[], sort: MatSort): Student[] => {
if (!sort.active || sort.direction === '') {
return items;
}
return items.sort((a: Student, b: Student) => {
let comparatorResult = 0;
switch (sort.active) {
case 'name':
comparatorResult = a.name.localeCompare(b.name);
break;
case 'class':
comparatorResult = a.class.localeCompare(b.class);
break;
case 'section':
comparatorResult = a.section.localeCompare(b.section);
break;
case 'subjects':
comparatorResult = a.subjects.length - b.subjects.length;
break;
case 'marks':
comparatorResult =
a.marks.reduce((prev, curr) => prev + curr) / a.marks.length
-
b.marks.reduce((prev, curr) => prev + curr) / b.marks.length;
break;
default:
comparatorResult = a.name.localeCompare(b.name);
break;
}
return comparatorResult * (sort.direction == 'asc' ? 1 : -1);
});
};
return sortFunction;
}

If you want to go through entire source code and look at the working preview you can check it out here

--

--

Full stack Dev and Lead @ Texas Instruments. Follow me for short articles on software development.