DEV Community

Dharmen Shah for Angular Material Dev

Posted on • Originally published at angular-material.dev

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;
  resultsLength = signal(0);
  isLoadingResults = signal(false);
  isRateLimitReached = signal(false);

  /** 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;
    this.resultsLength = this.dataSource.resultsLength;
    this.isLoadingResults = this.dataSource.isLoadingResults;
    this.isRateLimitReached = this.dataSource.isRateLimitReached;
  }
}



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 (isLoadingResults() || isRateLimitReached()) {
    <div class="example-loading-shade">
      @if (isLoadingResults()) {
        <mat-spinner></mat-spinner>
      }
      @if (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]="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 a similar approach as sorting and pagination.

1. MatTextFiler directive

We will create a MatTextFiler directive which we can add it to the text field to filter the table.

Create a file src\app\table\mat-text-filter.directive.ts with below content:



import { EventEmitter, HostListener } from '@angular/core';
import { Directive } from '@angular/core';

export class MatTextFilter {
  private _term = '';
  public get term() {
    return this._term;
  }
  public set term(value) {
    this._term = value;
    this.textFilterChange.emit(value);
  }
  textFilterChange = new EventEmitter<string>();
}

@Directive({
  selector: 'input[matTextFilter]',
  exportAs: 'matTextFilter',
  standalone: true,
})
export class MatTextFilterDirective {
  matTextFilter = new MatTextFilter();

  @HostListener('change', ['$event.target.value'])
  onChange(value: string) {
    this.matTextFilter.term = value;
  }
}



Enter fullscreen mode Exit fullscreen mode

Let's understand the code above.

  1. We have created a MatTextFilter class which has a term property and a textFilterChange event emitter.
  2. We have created a MatTextFilterDirective directive which we can add it to the text field to filter the table.
  3. We have used @HostListener to listen to the change event of the text field and emit the textFilterChange event.

2. Using MatTextFilterDirective in template

We will add a text field to the table to filter the table. And attach MatTextFilterDirective to the text field.



<div class="example-table-container">
  <mat-form-field><!-- [!code ++] -->
    <input matTextFilter matInput placeholder="Filter" #textFilter="matTextFilter"><!-- [!code ++] -->
  </mat-form-field><!-- [!code ++] -->
</div>


Enter fullscreen mode Exit fullscreen mode

3. Using MatTextFilterDirective in component

Not let's see how to use MatTextFilterDirective in our table component.



@Component({
  selector: 'app-table',
  templateUrl: './table.component.html',
  styleUrl: './table.component.scss',
  standalone: true,
  imports: [
    // other imports
    MatFormFieldModule,// [!code ++]
    MatInputModule,// [!code ++]
    MatTextFilterDirective,// [!code ++]
  ],
})
export class TableComponent implements AfterViewInit {   
  @ViewChild(MatTextFilterDirective) textFilter!: MatTextFilterDirective;// [!code ++]

  ngAfterViewInit(): void {
    this.dataSource.sort = this.sort;
    this.dataSource.paginator = this.paginator;
    this.dataSource.textFilter = this.textFilter.matTextFilter;// [!code ++]
    this.dataSource.database = this.database;

    // other code
  }
}


Enter fullscreen mode Exit fullscreen mode

4. Using MatTextFilterDirective in datasource

We will use MatTextFilter 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: MatTextFilter | undefined;

  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.textFilterChange// [!code highlight]
      ).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!.term// [!code highlight]
          ).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

Top comments (1)

Collapse
 
jangelodev profile image
João Angelo

Hi Dharmen Shah,
Thanks for sharing.