Angular URL State management with Query Params and Route Params

This blog post explains the process to maintain state using browser URL. In Angular there are couple of different ways to manage the state

  1. Angular Services
  2. State Management Library like NgRx, Akita, Elf etc.

But in some cases, you may want to share URL and let others see the exact list of items (same order and filters applied). For example, when search for a flight in Travel website and find a good deal on certain dates, when you share the URL with your friend, for them it would load web page with default selection. But with URL state management, all changes are updated in URL and anyone using the URL would see the same selection.

Query Params vs Route Params

This can be archived in one of the 2 ways

  1. Query Parameters
  2. Route Parameters

Query Parameters are a defined set of parameters attached to the end of a url and follow the standard syntax, starts with “?” then each extra parameter followed by “&”

https://DOMAIN_NAME/employee/all?pageIndex=5&pageSize=10&sortDirection=asc&sortBy=salary

Route Parameters are similar Query Parameters except they only have single parameter separator like “;” and this can be changed

https://DOMAIN_NAME/employee/all;pageIndex=5;pageSize=10;sortDirection=asc;sortBy=salary

You can use either one in your project. I typically gravitate towards Query Parameters since they are most common

Overview

In this blog post we preserve angular materiel table sorting column, sorting order, page size, page index, search text etc.. selections in URL. When user goes to another page by clicking on URL and then later come back to the old page by clicking on browser back button, user will see same results. But this can also be applied to inputs, tables, tab selection etc.

As defined in this example, we have all-employees component that displays list of employees in Mat table. It supports following operations

  1. Sorting
  2. Filtering
  3. Pagination
<mat-form-field appearance="fill">
  <mat-label>Filter</mat-label>
  <input
    [formControl]="searchTextControl"
    matInput
    (keyup)="applyFilter($event)"
    placeholder="Ex. Mia"
    #input
  />
</mat-form-field>

<div class="mat-elevation-z8">
  <table
    (matSortChange)="updateRouteParameters(null)"
    mat-table
    [dataSource]="dataSource"
    matSort
  >
    <!-- ID Column -->
    <ng-container matColumnDef="id">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>ID</th>
      <td mat-cell *matCellDef="let row">{{ row.id }}</td>
    </ng-container>

    <!-- Progress Column -->
    <ng-container matColumnDef="salary">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Salary</th>
      <td mat-cell *matCellDef="let row">${{ row.salary }}</td>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="name">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
      <td mat-cell *matCellDef="let row">{{ row.name }}</td>
    </ng-container>

    <!-- Department Column -->
    <ng-container matColumnDef="department">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Department</th>
      <td mat-cell *matCellDef="let row">{{ row.department }}</td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>

    <!-- Row shown when there is no matching data. -->
    <tr class="mat-row" *matNoDataRow>
      <td class="mat-cell" colspan="4">
        No data matching the filter "{{ input.value }}"
      </td>
    </tr>
  </table>

  <mat-paginator
    (page)="updateRouteParameters($event)"
    [pageSizeOptions]="[10, 25, 100]"
    aria-label="Select page of users"
    showFirstLastButtons
  ></mat-paginator>
</div>

Update the URL:

The all employees component defines table data source, adds data, assigns paginator and sorter. See the complete component source code here. To store the user selections in the URL the methodupdateRouteParameters() listens for all events on table

  /**
   * Update parameters after user enters search text or changes page selection
   *
   * @author Pavan Kumar Jadda
   * @since 2.0.0
   */
  updateRouteParameters($event: PageEvent | null) {
    const params = {
      pageIndex: this.paginator.pageIndex,
      pageSize: this.paginator.pageSize,
      searchText: this.searchTextControl?.value?.trim() ?? '',
      sortBy: this.sort.active,
      sortDirection: this.sort.direction,
    };
    const urlTree = this.router.createUrlTree(['/employee/all'], {
      relativeTo: this.route,
      queryParams: params,
      queryParamsHandling: 'merge',
    });

    //Update route with Query Params
    this.location.go(urlTree.toString());
  }

When user changes sorting order of a column, enters a filter text or changes page size or page index, this method updates the URL. And it will look like below

https://DOMAIN_NAME/employee/all?pageIndex=5&pageSize=10&searchText=&sortDirection=asc&sortBy=salary

if you want to use Route Parameters, you can make a small change in above code at line 15.

const urlTree = this.router.createUrlTree(['/employee/all',params]);

then URL becomes

https://DOMAIN_NAME/employee/all;pageIndex=5;pageSize=10;searchText=;sortDirection=asc;sortBy=salary

Load Webpage with Selected Options:

After changing sorting, filtering, pagination options and reload the web page or opening this URL in different browser tab, should load the table with exact selections.


  /**
   * Maps Employees to mat-table data format
   *
   * @author Pavan Kumar Jadda
   * @since 1.0.0
   */
  private mapTableData(data: UserData[]) {
    this.dataSource = new MatTableDataSource<UserData>(data);
    if (this.optionalParamsPresent()) {
      this.sort.active =
        this.route.snapshot.queryParamMap.get('sortBy') ?? 'id';
      //@ts-ignore
      this.sort.direction =
        this.route.snapshot.queryParamMap.get('sortDirection') ?? 'desc';
      this.paginator.pageIndex =
        Number.parseInt(
          this.route.snapshot.queryParamMap.get('pageIndex') as string,
          10
        ) ?? 0;
      this.paginator.pageSize =
        Number.parseInt(
          this.route.snapshot.queryParamMap.get('pageSize') as string,
          10
        ) ?? 10;
      this.searchTextControl.patchValue(
        this.route.snapshot.queryParamMap.get('searchText') ?? ''
      );
      this.dataSource.filter =
        this.route.snapshot.queryParamMap.get('searchText') ?? '';
      this.dataSource.sort = this.sort;
      this.dataSource.paginator = this.paginator;
      this.cdr.detectChanges();
    } else {
      this.dataSource.sort = this.sort;
      this.dataSource.paginator = this.paginator;
    }
  }

The mapTableData() method assigns data to table data source and formats table if any of the table options present in the URL.

For example, when user access the link https://DOMAIN_NAME/employee/all?pageIndex=5&pageSize=10&searchText=&sortDirection=asc&sortBy=salary, the table should show 5th page with page size 10 sort by Salary column in Ascending order. To achieve this, we need to check for these parameters and modify the table data source.


Code uploaded to Stackblitz for reference. Happy Coding 🙂

Pavan Kumar Jadda
Pavan Kumar Jadda
Articles: 10

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.