DEV Community

Cover image for Managing Angular Material table states with query params: a comprehensive guide
Abdun Nahid
Abdun Nahid

Posted on

Managing Angular Material table states with query params: a comprehensive guide

I am asuming you already played around with Angular Material or atleast know about it. Material Table(MatTable) is an Angular Material module used for visualizing data as a table view. We can make MatTable more featureful by implementing sorting & pagination using MatSort & MatPaginator from Angular Material. I must say, MatTable is full of features.

Along with all these features, if I don't add another one it feels incomplete to me. And that feature is having the table states back even if we refresh the page. Angular Material did not put in place this feature, but they have some useful API's that we will use to build the feature by ourselves. We are going to build a Directive that will take care of all the query params and table states.

Demo

Demo gif

Live Preview: https://ng-hack.web.app/mat-table-query-reflector

Note for Angular experts: Though this guide is not meant for the Angular experts, I still urge you to go through and bless me with your valuable feedback šŸ˜‡.

Don't like bla bla bla?

  • Find the everything we coded here on GitHub.
  • The directive is also built as an Angular Library: Here
  • The library is also available on NPM: @nghacks/ngmat-table-query-reflector
  • This is a part of GitHub repo:

    GitHub logo abdunnahid / nghacks

    Useful reusable pieces built with Angular, Angular Material and some sort of Hack

    NgHacks

    Useful reusable pieces built with Angular, Angular Material and some sort of Hack.

    We developers always stuck into problems. We spend hours on finding solutions. Sometimes it seems impossible. But somehow we come up with a solution, does not need to be the perfect one. Everyone knows the term ā€œhackā€ or ā€œworkaroundā€. Almost every developer does the practical implementation of this term. It can be because of the deadline, tight task estimation, lack of knowledge, or maybe the bug in the technology itself. I also did some hacky implementation with Angular. This repo is about those hacks.

    Note: The implemtations of this repo uses hacks & workaround solutions. And also may contain bad practices. Please take it with that concern on your mind. Your opinion can be different on things and that's fine. These are the hacks that worked for me.

    Let the hacking begin.

    ngmat-table-query-reflector

    Stores and retrievesā€¦

  • Live on web

So let's jump into coding and build a query param manager directive for MatTable.

Initial Setup

  1. Install Angular & create a new project

To follow along use angular 10.x

npm install -g @angular/cli
ng new MaterialTableQueryParamExample
? Would you like to add Angular routing? (y/N) n
? Which stylesheet format would you like to use? scss
  1. Add Angular Material to the created project
cd MaterialTableQueryParamExample
ng add @angular/material
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink
? Set up global Angular Material typography styles? Yes
? Set up browser animations for Angular Material? Yes
  1. Import RouterModule, MatTableModule, MatSortModule,MatPaginatorModule

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';

import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatPaginatorModule } from '@angular/material/paginator';
import { NgMatTableQueryReflectorDirective } from './directives/ng-mat-table-query-reflector.directive';

@NgModule({
  declarations: [
    AppComponent,
    NgMatTableQueryReflectorDirective
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    RouterModule.forRoot([]),
    MatTableModule,
    MatSortModule,
    MatPaginatorModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

  1. Table templating

app.component.html

<div class="table-container">
  <table mat-table [dataSource]="dataSource" matSort>
    <!-- Position Column -->
    <ng-container matColumnDef="position">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> Position </th>
      <td mat-cell *matCellDef="let element"> {{element.position}} </td>
    </ng-container>
    <!-- Name Column -->
    <ng-container matColumnDef="name">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
      <td mat-cell *matCellDef="let element"> {{element.name}} </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
  <mat-paginator [pageSizeOptions]="[10, 5, 3]" showFirstLastButtons></mat-paginator>
</div>
  1. Initializing table data source

app.component.ts

import { Component, OnInit, ViewChild } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
  @ViewChild(MatSort, { static: true }) sort: MatSort;

  displayedColumns: string[] = ['position', 'name'];
  dataSource: MatTableDataSource<PeriodicElement>;

  constructor() { }

  ngOnInit(): void {
      this.dataSource = new MatTableDataSource<PeriodicElement>(ELEMENT_DATA);
      this.dataSource.paginator = this.paginator;
      this.dataSource.sort = this.sort;
  }
}
export interface PeriodicElement {
  name: string;
  position: number;
}
const ELEMENT_DATA: PeriodicElement[] = [
  { position: 1, name: 'Hydrogen' },
  { position: 2, name: 'Helium' },
  { position: 3, name: 'Lithium' },
  { position: 4, name: 'Beryllium' },
  { position: 5, name: 'Boron' },
  { position: 6, name: 'Carbon' },
  { position: 7, name: 'Nitrogen' },
  { position: 8, name: 'Oxygen' },
  { position: 9, name: 'Fluorine' },
  { position: 10, name: 'Neon' }
];
  1. Add some styling to make it look not too bad

app.component.scss

.table-container {
  padding: 50px;
  table {
    width: 100%;
    margin: 10px;
    box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12);
  }
}
  1. Run the project
ng serve --o

The initial setup is done. It should render a table. You can change page index, page size, and sorting. If you refresh the page all your changes are gone. That's the problem we are going to fix next.

Cut the crap, show me the real thing

As planned we will solve them with a directive. Let's first create one.

Creating the directive

Create a directive using angular CLI. Let's name it NgMatTableQueryReflector and place it under /directives directory.

ng generate directive directives/NgMatTableQueryReflector

This command created a directive and registered it to AppModule declarations. The directive should look something like,

import { Directive } from '@angular/core';

@Directive({
  selector: '[appNgMatTableQueryReflector]'
})
export class NgMatTableQueryReflectorDirective {
  constructor() { }
}

Put this directive on table tag on template.

<table  mat-table  [dataSource]="dataSource"  matSort  appNgMatTableQueryReflector>

Now we have access to all the other directives and input properties in our NgMatTableQueryReflectorDirective.

Identifying the requirements

Think about the requirements. What behavior we actually want? Let's focus on the sorting behavior for now,

  1. If a user changes the active sort column or the sort direction, changes should reflect on the URL with query params.

For example user sorted the table by position in descending order. Our URL should change and look something like:

http://localhost:4200/?sort_active=position&sort_direction=desc

  1. If the user reloads the page with those changes, the table should render sorted by position in descending order.

Building the logic

What are the things we actually need to fulfill the requirements?

  1. We need a sort change event source.

Why? That source will let us know when the user changes the sorting then we can update our URL.

  1. Another thing we need is to set sorting states.

Why? Because, on the app load, if we find anything for the sorting on URL, we need to update the sorting.

And think what? Angular Material already has that API's to make our life easier.
What is that API? It is MatTableDataSource that we get as an input from the template named as dataSource.

Implementation

Listen to sort change event & update URL

Take dataSource as an input. Inject the routing dependencies to constructor

@Input() dataSource: MatTableDataSource<any>;

constructor(
  private _router: Router,
  private _activatedRoute: ActivatedRoute
) { }

First, listen to the sort change events on ngAfterViewInit lifecycle hook.

ngAfterViewInit(): void {
  this.listenToStateChangeEvents();
}
private listenToStateChangeEvents(): void {
  this.dataSource.sort.sortChange.subscribe((sortChange: Sort) => {
    // Update URL with the changed sort states
  });
}

Then update the URL query params

private listenToStateChangeEvents(): void {
  this.dataSource.sort.sortChange.subscribe((sortChange: Sort) => {
    this._applySortChangesToUrlQueryParams(pageChange);
  });
}
private _applySortChangesToUrlQueryParams(sortChange: Sort): void {
  const sortingAndPaginationQueryParams = {
    sort_active: sortChange.active,
    sort_direction: sortChange.direction,
  };
  this._router.navigate([], { 
   queryParams: sortingAndPaginationQueryParams, 
   queryParamsHandling: 'merge' 
  });
}

Note: We used queryParamsHandling: 'merge'. We don't want our sort query params to break other query params on the URL.

Check that out changing the sorting states. It reflects on the URL, right?
Cool! let's make the URL reflects on the table too.

Apply initial sort query params to table

Read query params on app load in ngAfterViewInit lifecycle hook.

  ngAfterViewInit(): void {
    this._initialSetup();
    this.listenToStateChangeEvents();
  }

  private initialSetup(): void {
    const activeSortQuery = this.activeSortQuery;
    console.log("NgMatTableQueryReflectorDirective -> _initialSetup -> activeSortQuery", activeSortQuery);
  }

  private get activeSortQuery(): { sort_active: string, sort_direction: 'asc' | 'desc' } {
    const queryParams = this._activatedRoute.snapshot.queryParams;
    if (queryParams.hasOwnProperty('sort_active') && queryParams.hasOwnProperty('sort_direction')) {
      return {
        sort_active: queryParams.sort_active,
        sort_direction: queryParams.sort_direction
      };
    }
    return;
  }

Now we should get our sort query params inside initialSetup(). But if you try reloading the page, you can see undefined on the console though we have query params present on the URL!

Because we used this._activatedRoute.snapshot to get query params. snapshot.queryParams returns query params present at that moment when it is called. The moment we called snapshot, Angular router is not aware of the query params. So it returns undefined instead of the query params. We could use this._activatedRoute.paramMap. But this emits value on every URL param change. We don't need that, we only need to know the query params on app load. So let's fix it.

Here comes the Hack

We will fix this by waiting until the angular router initializes the query params.

  private waitForQueryParamsToLoad(): Promise<void> {

    if (!window.location.search) { return; }

    const titleCheckingInterval$ = interval(500);
    let subscription: Subscription;

    return new Promise((resolve) => {
      subscription = titleCheckingInterval$.subscribe(val => {
        if (Object.values(this._activatedRoute.snapshot.queryParams).length > 0) {
          subscription.unsubscribe();
          return resolve();
        }
      });
    });
  }

We decided to wait only if we find query params in window.location, otherwise no need to wait. window.location is available even before angular loads. So no tension about loading window.location this time. We used a handy tool interval from rxjs to check with an interval.

Note: Please consider using the window object directly on Angular. Directly using window is not a good idea!

Call this method we just wrote before getting the query params. await for waitForQueryParamsToLoad() and async for initialSetup().

  private async initialSetup(): Promise<void> {
    // HACK
    await this.waitForQueryParamsToLoad();
    const activeSortQuery = this.activeSortQuery;
    console.log("NgMatTableQueryReflectorDirective -> _initialSetup -> activeSortQuery", activeSortQuery);
  }

Now our activeSortQuery getter returns an expected value. Nice!

The last thing we need is to apply our sort states from query param to the MatSort.

  private async initialSetup(): Promise<void> {
    // HACK
    await this.waitForQueryParamsToLoad();

    const activeSortQuery = this.activeSortQuery;
    if (!activeSortQuery) { return; }

    const sortable: MatSortable = {
      id: activeSortQuery.sort_active,
      start: activeSortQuery.sort_direction,
      disableClear: true
    };

    this.dataSource.sort.sort(sortable);
  }

Now everything should work fine but no it doesn't! There is an issue with the MatSort. We need to do another hack! This hack was invented on that issue.

[Sort] Programatically setting active/direction does not update UI #10242

Bug, feature request, or proposal:

I'm programmatically setting the active and direction on matSort, but it's not updating the UI

What is the expected behavior?

programmatically setting the active and direction on matSort updates the UI

What is the current behavior?

programmatically setting the active and direction on matSort, but it's not updating the UI

What are the steps to reproduce?

https://stackblitz.com/edit/angular-material2-issue-mc4cve?file=app/app.component.ts

What is the use-case or motivation for changing an existing behavior?

I believe this is a regression. This was working in 5.1.0

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Is there anything else we should know?

Comment for #10242

adgoncal avatar
adgoncal commented on

I spent some time debugging this. Here are my observations:

When you click on a mat-sort-header that is not currently active, eventually it will call _setAnimationTransitionState({fromState: '<direction>', toState: 'active'}) on the clicked mat-sort-header.

However, when you programmatically call matSort.sort({...}), the mat-sort-header that should become active never calls _setAnimationTransitionState().

I believe this is an issue on _rerenderSubscription().

I managed to make it work with the following ugly hack:

    this.matSort.sort({
      id: <value>,
      start: <direction>,
      disableClear: true,
    });

    // ugly hack!
    const activeSortHeader = this.matSort.sortables.get(value);
    activeSortHeader['_setAnimationTransitionState']({
      fromState: <direction>,
      toState: 'active',
    });

https://stackblitz.com/edit/angular-vuvckf?file=app%2Ftable-overview-example.ts


References:

https://github.com/angular/material2/blob/9ab2c905e2e81999347742f880bfd851f9e32022/src/lib/sort/sort-header.ts#L230

https://github.com/angular/material2/blob/9ab2c905e2e81999347742f880bfd851f9e32022/src/lib/sort/sort-header.ts#L145-L159

https://github.com/angular/material2/blob/9ab2c905e2e81999347742f880bfd851f9e32022/src/lib/sort/sort-header.ts#L204-L212

Let's apply that hack.

  private async initialSetup(): Promise<void> {
    // HACK 1
    await this.waitForQueryParamsToLoad();

    const activeSortQuery = this.activeSortQuery;
    if (!activeSortQuery) { return; }

    const sortable: MatSortable = {
      id: activeSortQuery.sort_active,
      start: activeSortQuery.sort_direction,
      disableClear: true
    };

    this.dataSource.sort.sort(sortable);

    // HACK 2
    const activeSortHeader = this.dataSource.sort.sortables.get(activeSortQuery.sort_active);
    activeSortHeader['_setAnimationTransitionState']({
      fromState: this.dataSource.sort.direction,
      toState: 'active',
    });
  }

At last, we are done with the sorting.

Pagination is similar to the sorting we just implemented. One thing that is different from sorting is that we don't need any special hack for pagination. If you followed along with the guide and understood every piece of it, it will be bread and butter for you to implement pagination. Also, think about some scenarios like an advanced filter for the table. You can manage advance filters from query params with angular reactive forms using a similar idea.

If you couldn't build the pagination by yourself, the resources below may help you:

  • Find the everything we coded here on GitHub.
  • The directive is also built as an Angular Library: Here
  • The library is also available on NPM: @nghacks/ngmat-table-query-reflector
  • This is a part of GitHub repo:

    GitHub logo abdunnahid / nghacks

    Useful reusable pieces built with Angular, Angular Material and some sort of Hack

    NgHacks

    Useful reusable pieces built with Angular, Angular Material and some sort of Hack.

    We developers always stuck into problems. We spend hours on finding solutions. Sometimes it seems impossible. But somehow we come up with a solution, does not need to be the perfect one. Everyone knows the term ā€œhackā€ or ā€œworkaroundā€. Almost every developer does the practical implementation of this term. It can be because of the deadline, tight task estimation, lack of knowledge, or maybe the bug in the technology itself. I also did some hacky implementation with Angular. This repo is about those hacks.

    Note: The implemtations of this repo uses hacks & workaround solutions. And also may contain bad practices. Please take it with that concern on your mind. Your opinion can be different on things and that's fine. These are the hacks that worked for me.

    Let the hacking begin.

    ngmat-table-query-reflector

    Stores and retrievesā€¦

  • Live on web

That's it for now. Let me know what you think about the hacks I used. If you know any better solution you must let me know. You can also contribute to the repo with your improvements or content.

For any queries, please comment or DM on Twitter.

Top comments (2)

Collapse
 
ratanparai profile image
Ratan Sunder Parai • Edited

Thanks for sharing this wonderful blog with us.

Collapse
 
abdunnahid profile image
Abdun Nahid

Thanks that you liked it.