Handling route parameters

We have configured pretty basic paths in our routes so far, but what if we want to build dynamic paths with support for parameters or values created at runtime? Creating (and navigating to) URLs that load specific items from our data stores is a common action we need to confront on a daily basis. For instance, we might need to provide a master-detail browsing functionality, so each generated URL living in the master page contains the identifiers required to load each item once the user reaches the detail page.

We are basically tackling a double trouble here: creating URLs with dynamic parameters at runtime and parsing the value of such parameters. No problem, the Angular router has got our back and we will see how using a real example.

Passing dynamic parameters in our routes

We updated the tasks list to display a button leading to the timer component page when clicked. But we just load the timer component with no context whatsoever of what task we are supposed to work on once we get there. Let's extend the component to display the task we picked prior to jumping to this page.

First, let's get back to the tasks list component template and update the signature of the button that triggers the navigation to the timer component in order to include the index of the task item corresponding to that loop iteration:

app/tasks/tasks.component.html

...
<button type="button"
    class="btn btn-default btn-xs"
    *ngIf="task.queued"
    (click)="workOn(i)">
    <i class="glyphicon glyphicon-expand"></i> Start
  </button>
...

Remember that such an index was generated at every iteration of the NgFor directive that rendered the table rows. Now that the call incorporates the index in its signature, we just need to modify the payload of the navigate method:

workOn(index: number): void {
  this.router.navigate(['Timer', { id: index }]);
}

If this had been a routerLink directive, the parameters would have been defined in the same way: a hash object following the path name string (or strings, as we will see while tapping into the child routers) inside the array. This is the way parameters are added to the generated link. However, if we click on any button now, we will see that the dynamic ID values are appended as query string parameters. While this might suffice in some scenarios, we are after a more elegant workaround for this. So, let's update our route definition to include the parameter in the path. Go back to our top root component and update the route inside the RouteConfig decorator as follows:

app/app.component.ts

...
}, {
  path: 'timer/:id',
  name: 'TimerComponent',
  component: TimerComponent
}
...

Refresh the application, schedule the last task on the table, and click on the Start button. You will see how the browser loads the Timer component under a URL like /timer/3.

Each path can contain as many tokens prefixed by a colon as required. These tokens will be translated to the actual values when we act on a routerLink directive or execute the navigate method of the Router class by passing a hash of the key/value pairs, matching each token with its corresponding key. So, in a nutshell, we can define route paths as follows:

{
  path: '/products/:category/:id',
  name: 'ProductsByCategoryComponent',
  component: ProductsByCategoryComponent 
}

Then, we can execute any given route such as the one depicted earlier as follows:

<a [routerLink]="['ProductsByCategoryComponent', { 
  category: 'toys',
  id: 452 
}]">See Toy</a>

The same applies to the routes called imperatively:

router.navigate(['ProductsByCategoryComponent', {
  category: 'toys',
  id: 452 
}]);

Parsing route parameters with the RouteParams service

Great! Now, we are passing the index of the task item we want to work on loading the timer, but how do we parse that parameter from the URL? The Angular router provides a convenient injectable type (already included in ROUTER_PROVIDERS) named RouteParams that we can use from the components handled by the router to fetch the parameters defined in the route definition path.

Open our timer component and import it with the following import statement. Also, let's inject the TaskService provider, so we can retrieve information from the task item requested:

app/timer/timer-widget.component.ts

import { Component, OnInit } from '@angular/core';
import { SettingsService, TaskService } from '../shared/shared';
import { RouteParams } from '@angular/router-deprecated';
...

We need to alter the component's definition in order to assign the TaskService as an annotated dependency for this component, so the injector can properly perform the provider lookup.

Note

The new Release Candidate router has deprecated the RouteParams class, favoring the new RouteSegments class, which exposes more and more useful methods and helpers. Please refer to the official documentation for broader insights on its API.

We will also leverage this action to insert the interpolated title corresponding to the requested task in the component template:

app/timer/timer-widget.component.ts

...
@Component({
  selector: 'pomodoro-timer-widget',
  template: `
    <div class="text-center">
      <img src="/app/shared/assets/img/pomodoro.png">
      <h3><small>{{ taskName }}</small></h3>
      <h1> {{ minutes }}:{{ seconds  | number: '2.0' }} </h1>
      <p>
        <button (click)="togglePause()" class="btn btn-danger">
        {{ buttonLabelKey | i18nSelect: buttonLabelsMap }}
        </button>
      </p>
    </div>`
})
...

The taskName variable is the placeholder we will be using to interpolate the name of the task. With all this in place, let's update our constructor to bring both the RouteParams type and the TaskService classes to the game as private class members injected from the constructor:

app/timer/timer-widget.component.ts

...
constructor(
  private settingsService: SettingsService,
  private routeParams: RouteParams,
  private taskService: TaskService) {
    this.buttonLabelsMap = settingsService.labelsMap.timer;
}
...

With these types now available in our class, we can leverage the ngOnInit hook to fetch the task details of the item in the tasks array corresponding to the index passed as a parameter. Waiting for the OnInit stage is not easy, since we will find issues when trying to access the properties contained in routeParams before that stage:

app/timer/timer-widget.component.ts

ngOnInit(): void {
  this.resetPomodoro();
  setInterval(() => this.tick(), 1000);

  let taskIndex = parseInt(this.routeParams.get('id'));
  if (!isNaN(taskIndex)) {
    this.taskName = this.taskService.taskStore[taskIndex].name;
  }
}

How do we fetch the value from that id parameter? The RouteParams object exposes a get(param: string) method we can use to address parameters by name. In our example, we retrieved the value of the id parameter by executing the routeParams.get('id') command in the ngOnInit() hook method. Basically, this is how we get parameter values from our routes. First, we grab an instance of the RouteParams class through the component injector and then we retrieve values by executing its getter function, which will expect a string parameter with the name of the token corresponding to the parameter we need.

Note

It is important to note that we are fetching the data already persisted in the taskStore property of our TaskService provider. Since it is a singleton, available throughout the entire application by means of the Angular DI machinery, which had been already populated at TaskComponent, all the information we require is already there. Things would become trickier if we load directly each timer URL. In those cases, the information would have not been fetched yet, so we would have to subscribe to the service in order to force it to load the data through its underlying Http client. We saw this in Chapter 6, Asynchronous Data Services with Angular 2; applying the async pipe to the taskName interpolation in the template would be required. For the sake of simplicity, we will skip that refactoring here, but we encourage you to tweak the component to extend support for this scenario as well.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset