Angular Material Table - Server Side Filtering

shhdharmen

Dharmen Shah

Posted on October 7, 2024

Angular Material Table - Server Side Filtering

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 {}
}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

If you look at the output, you will see sorting and pagination working as expected.

Angular Material Table Server Side Filtering

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's understand the code above.

  1. We have created a FormGroup class which has a textFilter FormControl.
  2. We also make sure that events are emitted only on submit, like when user presses Enter.
  3. Lastly, we are attaching textFilter to our dataSource. We will make changes in dataSource 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>
Enter fullscreen mode Exit fullscreen mode

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.'
      );
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it. We have implemented server side filtering in Angular Material Table.

Angular Material Table Server Side Filtering

Live Playground

💖 💪 🙅 🚩
shhdharmen
Dharmen Shah

Posted on October 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related