Angular Material Table - With Sorting and API Data
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.

Finished project

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.

Angular material website

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>

Empty module

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.

Basic material table

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
    },
  ]
}

Static data

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>

Styled 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.

Real data

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.

Finished project

And actually if you want to learn Angular with NgRx from empty folder to a fully functional production application make sure to check my Angular and NgRx - Building Real Project From Scratch course.

📚 Source code of what we've done