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.
Defining child routers
As our applications scale, the idea of bundling all the route definitions in a centralized location (for example, the root component) does not seem like a good approach. The more routes we define there, the harder it will be to maintain the application, let alone the tight coupling we generate between our components (that are meant to be as much reusable and application-agnostic as possible) and the application itself.
This is why it is generally a good practice to split and wrap the route definitions that apply to a specific feature around a router configuration defined on a specific component per feature level, usually the root component that wraps that feature context. The Angular team had this idea in mind when the Router
library was designed and thus implemented support to extend a route with children routes while keeping the parent route fully agnostic of what routes are defined above its layer of functionality.
Let’s see all this through an actual example. We updated our timer recently to display the name of the task we wanted to work on after selecting it from the table. However, what if we want to keep providing a standalone timer not bound to any specific task? This way, we can leverage the countdown functionality for any impromptu task without having to create it beforehand.
So, we will want to give access to the timer in two flavors:
timer
: This will load the timer as it is without pointing to any specific tasktimer/task/{id}
: This will load the timer specifying a task name, where{id}
is the index of the task we want to load from the overall tasks array.
We will begin by updating the main root component, now turned into a router component, to turn the /timer/:id
path into a path pointing to a child router component. Open the component and replace the route definition pointing to the timer component using the following definition:
app/app.component.ts
{
path: 'timer/...',
name: 'TimerComponent',
component: TimerComponent
}
That’s it. The ellipsis right next to the route path informs the Angular Router that it should expect route definitions nested within that component. The problem here is that a component should not route to itself, since it cannot instantiate itself inside its own RouterOutlet
directive. This is why we need to proxy the timer component with a router component for this example. So, let’s create a routing component for our timer inside its own folder for simplicity sake. A routing component is, by definition, a component with no implementation other that to serve as a component dispatcher depending on routes. Their implementation, if any, is generally pretty limited, and it basically entails the RouteConfig
decorator containing the routes delivered by that feature context and the RouterOutlet
present in its template. Routing components are indeed a good way to decouple routing functionalities from the specific implementation of each component in the context of that feature, ensuring full reusability of its non-routing components of that feature.
Open the timer
feature folder and create a file for our timer routing component with the following implementation:
app/timer/timer.component.ts
import { Component } from '@angular/core'; import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated'; import TimerWidgetComponent from './timer-widget.component'; @Component({ selector: 'pomodoro-timer', directives: [ROUTER_DIRECTIVES], template: '<router-outlet></router-outlet>' }) @RouteConfig([ { path: '/', name: 'GenericTimer', component: TimerWidgetComponent, useAsDefault: true }, { path: '/task/:id', name: 'TaskTimer', component: TimerWidgetComponent } ]) export default class TimerComponent {}
As we can see, we have created a component class with no implementation other than dispatching routes to the TimerWidgetComponent
component itself. The RouteConfig
type allows us to create a proper decorator containing route definitions and ROUTER_DIRECTIVES
will allow us to bind the RouterOutlet
directive in the component template.
Please pay attention to the new useAsDefault
Boolean property in the first route definition. This informs the router that if no matching paths are found, the Router
class should load this route by default.
Note
At the time of this writing, the new Release Candidate Router still does not implement the useAsDefault
property, but it is planned to be implemented by its final version. Please refer to the official documentation for further details.
It is important to note that any component acting as a router component can have its own implementation living in parallel to the RouterOutlet
directive. Just like we did with AppComponent
, we can provide additional functionalities to our component other than the mere routing features.
All right, we have a component now that can redirect users to our timer component in two flavors, but how do we link to it?
Linking to child routes
There is no difference whatsoever in linking to a child router and linking to any given route managed by a top router, except for the fact that we will populate the route names array with the names of the child routes we want to link as well. Open the PomodoroTaskList
component and refactor the workOn()
method to look like this:
workOn(index: number): void { this.router.navigate(['TimerComponent', 'TaskTimer', { id: index }]); }
Here, we are telling Angular to link to the route named TimerComponent
(hence the importance of naming our routes after each target component’s name). Since this is a parent route (remember the ellipsis) configured at our top root router, we need to provide the name of the child route to load from within the routes configured at the child router level, TaskTimer
in this case. Obviously, we will compound up the route with the ID information required for loading the task requested. Click on any task and see how the timer is loaded, displaying the task name we wish to work on.
This implementation approach gives the component the chance to be displayed with or without Task ID information. This way, we can keep on browsing to the timer functionality, either from the top nav link or by clicking the Start button at the tasks table.
Just remember we configured the main route definition with the useAsDefault
property set as true, remember? This means that anything pointing to the timer route will degrade gracefully to this last route definition once we reach the child route domain.