Angular Material Table - With Sorting and API Data
In this post you will learn how to implement how to implement Angular Material Table inside your project.
Now so long ago I made a post where we created a custom table inside Angular and we implemented there sorting and filtering.
It is completely fine to have a custom implementation but maybe you don't want to write it on your own and you want to use a library. Of maybe you have lots of tables and you really need that reusable library. Or you just want to get a table with ready CSS. In these cases it is better to just pick a library.
The question is what library we must pick together with Angular because we have lots of different libraries which implement table. And actually I highly recommend you to look on Angular Material. Why?
It is a library of components which is supported by Angular team.
Which actually means that it will work 100% and it is implemented in a scalable but maybe a bit complex way. And the goal of this post is to build a table by using Angular material with typical features like fetching data from API and sorting columns.
As you can see here on the official website we will use mat-table
and this is the styled data table. It is important to understand. This is a wrapper around CDK data table.
Which means if you don't need any representational logic and CSS then you can use just cdk and not mat-table.
But actually it doesn't make any difference because it is simply a wrapper around business logic which is inside CDK. It is important to remember this because we must install not only angular-material but also angular-cdk package.
yarn add @angular/material
yarn add @angular/cdk
But it is also super important that you check your Angular version. In my project I had Angular version 14 and installed material and CDK version 14. If you have older project like Angular 12 then it won't work out of the box with latest Material and CDK.
Adding module
So we successfully installed our dependencies. My project is completely empty. Now let's create a new module. We want to do that in order to fully isolate our table inside that module. And the idea of our module is to render a list of users from the API inside a table.
// src/app/usersTable/usersTable.module.ts
@NgModule({
imports: [CommonModule],
})
export class UsersTableModule {}
Now we need to inject this module in our app module.
// src/app/app.component.ts
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { UsersTableModule } from './usersTable/usersTable.module';
@NgModule({
imports: [
...
BrowserAnimationsModule,
UsersTableModule,
],
...
})
export class AppModule {}
But actually we need an additional impot here. As you can see I added BrowserAnimationsModule
to our imports. To use Angular-material you must add the animations module to your project.
Now let's create our component.
// src/app/usersTable/components/usersTable/usersTable.component.ts
@Component({
selector: 'users-table',
templateUrl: './usersTable.component.html',
styleUrls: ['./usersTable.component.css'],
})
export class UsersTableComponent {}
And add some html
// src/app/usersTable/components/usersTable/usersTable.component.html
<div>Users table</div>
Now we must register our component in a module.
// src/app/usersTable/usersTable.module.ts
import { UsersTableComponent } from './components/usersTable/usersTable.component';
@NgModule({
declarations: [UsersTableComponent],
exports: [UsersTableComponent],
})
export class UsersTableModule {}
And the last step is to render our component in app.component.
// src/app/app.component.html
<users-table></users-table>
As you can see our initial component is rendered.
Markup for the table
Now let's write some markup to render a table.
<table
mat-table
>
<ng-container matColumnDef="id">
<th
mat-header-cell
*matHeaderCellDef
>
ID
</th>
<td mat-cell *matCellDef="let element">
{{ element.id }}
</td>
</ng-container>
<ng-container matColumnDef="name">
<th
mat-header-cell
*matHeaderCellDef
>
Name
</th>
<td mat-cell *matCellDef="let element">
{{ element.name }}
</td>
</ng-container>
<ng-container matColumnDef="age">
<th
mat-header-cell
*matHeaderCellDef
>
Age
</th>
<td mat-cell *matCellDef="let element">
{{ element.age }}
</td>
</ng-container>
</table>
So here we defined our table and added mat-table
directive to it. Inside we defined ng-container
for every header cell that we render. As you can see we have a special matColumnDef
and matHeaderCellDef
to define our columns.
This is just a schema of our table so Angular Material knows what do we want to render.
<table>
...
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
Here we rendered a row for our header and rows of our table with mat-header-row
and mat-row
. As you can see we used displayedColumns
property here which is not defined yet.
export class UsersTableComponent implements OnInit {
displayedColumns: string[] = ['id', 'name', 'age'];
}
If we open a browser it will be all broken because we didn't add Angular Material Table as a module inside our usersTable
module.
...
import { MatTableModule } from '@angular/material/table';
@NgModule({
imports: [CommonModule, MatTableModule],
...
})
export class UsersTableModule {}
Inside Angular Material every single module is packed in additional path.
Our table is rendered but we don't have any data inside.
Working with data
Now you for sure want to say "But where is our table?". This happens because we didn't provide any data inside our table. And in order to do that we have such thing which is called DataSource
.
The basic usage of it is just to provide static data to our table.
<table [dataSource]="dataSource">
export class UsersTableComponent {
dataSource = [
{
"id": "1",
"name": "Jack",
"age": 25
},
{
"id": "2",
"name": "John",
"age": 20
},
{
"id": "3",
"name": "Mike",
"age": 30
},
]
}
As you can see our static data from the component are rendered in the table. But actually these styles are not looking great so let's change it a little bit.
// src/app/usersTable/usersTable.module.css
.users-table {
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
.users-table-cell {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
Now let's add this classes to our table.
<table
mat-table
class="users-table"
>
<ng-container matColumnDef="id">
<th
mat-header-cell
*matHeaderCellDef
class="users-table-cell"
>
ID
</th>
<td mat-cell *matCellDef="let element" class="users-table-cell">
{{ element.id }}
</td>
</ng-container>
<ng-container matColumnDef="name">
<th
mat-header-cell
*matHeaderCellDef
class="users-table-cell"
>
Name
</th>
<td mat-cell *matCellDef="let element" class="users-table-cell">
{{ element.name }}
</td>
</ng-container>
<ng-container matColumnDef="age">
<th
mat-header-cell
*matHeaderCellDef
class="users-table-cell"
>
Age
</th>
<td mat-cell *matCellDef="let element" class="users-table-cell">
{{ element.age }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
As you can see our table is styled nicely.
So now you know that we can provide inside our dataSource
just static data. But in real application we normally use API and not static data. Which means that we must know how to provide the data from the API in our table.
But before we start let's add a UserInterface
. It will help us to define our logic better.
// src/app/usersTable/types/user.interface.ts
export interface UserInterface {
id: string;
name: string;
age: number;
}
Our interface is fully ready. Now we can create a service to work with API.
// src/app/usersTable/services/users.service.ts
@Injectable()
export class UsersService {
constructor(private http: HttpClient) {}
fetchUsers(sort: Sort): Observable<UserInterface[]> {
return this.http.get<UserInterface[]>('http://localhost:3004/users');
}
}
Here we defined fetchUsers
method which will get a list of users for us.
Now we need too define a custom dataSource class. This is a typical way to define how to pass data inside the table in Angular Material.
// src/app/usersTable/services/users.dataSource.ts
@Injectable()
export class UsersDataSource extends DataSource<UserInterface> {
users$ = new BehaviorSubject<UserInterface[]>([]);
isLoading$ = new BehaviorSubject<boolean>(false);
constructor(private usersService: UsersService) {
super();
}
connect(): Observable<UserInterface[]> {
return this.users$.asObservable();
}
disconnect(): void {
this.users$.complete();
}
loadUsers(): void {
this.isLoading$.next(true);
this.usersService.fetchUsers().subscribe((users) => {
this.users$.next(users);
this.isLoading$.next(false);
});
}
}
Here we defined a class which is extended from DataSource
. Also we have 2 BehaviorSubject
inside so we can have a state inside this class. We store here a list of users and a loading property.
For DataSource
class it is mandatory to define connect
and disconnect
methods. connect
method must return an Observable
for our table. disconnect
happens when we remove the dataSource or the table.
Additionally we defined loadUsers
method. It's a custom method which fetches our users and updates the state.
@NgModule({
...
providers: [UsersService, UsersDataSource],
})
export class UsersTableModule {}
We also should not forget to register both services on our module.
Now let's use our dataSource and load data to our component.
export class UsersTableComponent implements OnInit {
displayedColumns: string[] = ['id', 'name', 'age'];
dataSource = new UsersDataSource(this.usersService);
constructor(private usersService: UsersService) {}
ngOnInit(): void {
this.dataSource.loadUsers();
}
}
Here we called our UsersDataSource
and provided it inside Material Table like previously. Also we called loadUsers
on initialize of our component.
As you can see in browser our data is there and we loaded it from the API.
Sorting
And the last thing that I want to show you is sorting. First of all we must add this module to our UsersTableModule
.
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
@NgModule({
imports: [CommonModule, MatTableModule, MatSortModule],
...
})
export class UsersTableModule {}
Now we must add sorting directive to our table, header and rows.
<table
mat-table
matSort
matSortActive="id"
matSortDirection="asc"
matSortDisableClear
(matSortChange)="sortUsers($event)"
[dataSource]="dataSource"
class="users-table"
>
<ng-container matColumnDef="id">
<th
mat-header-cell
mat-sort-header
*matHeaderCellDef
class="users-table-cell"
>
ID
</th>
<td mat-cell *matCellDef="let element" class="users-table-cell">
{{ element.id }}
</td>
</ng-container>
<ng-container matColumnDef="name">
<th
mat-header-cell
mat-sort-header
*matHeaderCellDef
class="users-table-cell"
>
Name
</th>
<td mat-cell *matCellDef="let element" class="users-table-cell">
{{ element.name }}
</td>
</ng-container>
<ng-container matColumnDef="age">
<th
mat-header-cell
mat-sort-header
*matHeaderCellDef
class="users-table-cell"
>
Age
</th>
<td mat-cell *matCellDef="let element" class="users-table-cell">
{{ element.age }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
Here we used mat-sort-header
and mat-sort
directives. Additionally we added matSortChange
event which called sortUsers
function. It will happen every time when we click on one of our headers.
export class UsersTableComponent implements OnInit {
...
ngOnInit(): void {
this.dataSource.loadUsers({ active: 'id', direction: 'asc' });
}
sortUsers(sort: Sort): void {
this.dataSource.loadUsers(sort);
}
}
Now we passed default sorting in initialize and the sorting on change. But our dataSource is not ready to accept sort
property.
loadUsers(sort: Sort): void {
this.isLoading$.next(true);
this.usersService.fetchUsers(sort).subscribe((users) => {
this.users$.next(users);
this.isLoading$.next(false);
});
}
Here we just accepted sort as an argument and passed it inside fetchUsers
.
Our last step is to change sorting API request in our service.
@Injectable()
export class UsersService {
constructor(private http: HttpClient) {}
fetchUsers(sort: Sort): Observable<UserInterface[]> {
const params = new HttpParams()
.set('_sort', sort.active)
.set('_order', sort.direction);
return this.http.get<UserInterface[]>('http://localhost:3004/users', {
params,
});
}
}
Here we create new parameters and attach them to the request.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done