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.
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 }]);
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.
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.
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.