Routing in Angular revisited
A long time ago we’ve written about routing in Angular and you’ve probably noticed that this article is deprecated due to many changes and rewrites that happened in the router module of Angular. Just recently, the Angular team announced yet another version of the new router, in which they considered all the gathered feedback from the community to make it finally sophisticated enough, so it’ll fulfill our needs when we build applications with Angular.
In this article we want to take a first look at the new and better APIs, touching on the most common scenarios when it comes to routing. We’re going to explore how to define routes, linking to other routes, as well as accessing route parameters. Let’s jump right into it!
Defining Routes
Let’s say we want to build a contacts application (in fact, this is what we do in our Angular Master Class). Our contacts application shows a list of contacts, which is our ContactsListComponent
and when we click on a contact, we navigate to the ContactsDetailComponent
, which gives us a detailed view of the selected contact.
A simplified version of ContactsListComponent
could look something like this:
@Component({
selector: 'contacts-list',
template: `
<h2>Contacts</h2>
<ul>
<li *ngFor="let contact of contacts | async">
{{contact.name}}
</li>
</ul>
`
})
export class ContactsListComponent {
...
}
Let’s not worry about how ContactsListComponent
gets hold of the contact data. We just assume it’s there and we generate a list using ngFor
in the template.
ContactsDetailComponent
displays a single contact. Again, we don’t want to worry too much about how this component is implemented yet, but a simplified version could look something like this:
@Component({
selector: 'contacts-detail',
template: `
<h2>{{contact.name}}</h2>
<address>
<span>{{contact.street}}</span>
<span>{{contact.zip}}</span>
<span>{{contact.city}}</span>
<span>{{contact.country}}</span>
</address>
`
})
export class ContactsDetailComponent {
...
}
Especially in ContactsDetailComponent
there are a couple more things we need to consider when it comes to routing (e.g. how to link to that component, how to get access to URL parameters), but for now, the first thing we want to do is defining routes for our application.
Defining routes is easy. All we have to do is to create a collection of Route
which simply follows an object structure that looks like this:
interface Route {
path?: string;
component?: Type|string;
...
}
As we can see, there are actually a couple more properties than just the three we show here. We’ll get to them later but this is all we need for now.
Routes are best defined in a separate module to keep our application easy to test and also to make them easier to reuse. Let’s define routes for our components in a new module (maybe contacts.routes.ts
?) so we can add them to our application in the next step:
import { ContactsListComponent } from './contacts-list';
import { ContactsDetailComponent } from './contacts-detail';
export const ContactsAppRoutes = [
{ path: '', component: ContactsListComponent },
{ path: 'contacts/:id', component: ContactsDetailComponent }
];
Pretty straight forward right? You might notice that the path
property on our first Route
definition is empty. This simply tells the router that this component should be loaded into the view by default (this is especially useful when dealing with child routes). The second route has a placeholder in its path called id
. This allows us to have some dynamic value in our path which can later be accessed in the component we route to. Think of a contact id in our case, so we can fetch the contact object we want to display the details for.
The next thing we need to do is to make these routes available to our application. In fact, we first need to make sure there’s a router in our application at all. We do that by importing Angular RouterModule
into our application module. But not only that, we also configure it with our routes using RouterModule.forRoot()
.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { ContactsListComponent } from './contacts-list';
import { ContactsDetailComponent } from './contacts-detail';
import { ContactsAppComponent } from './app.component';
import { ContactsAppRoutes } from './app.routes';
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(ContactsAppRoutes)
],
declarations: [
ContactsAppComponent,
ContactsListComponent,
ContactsDetailComponent
],
bootstrap: [ContactsAppComponent]
})
You might wonder where ContactsAppComponent
comes from. Well, this is just the root component we use to bootstrap our application. In fact, it doesn’t really know anything about our ContactsListComponent
and ContactsDetailComponent
. We’re going to take a look at ContactsAppComponent
in the next step though.
Displaying loaded components
Okay cool, our application now knows about these routes. The next thing we want to do is to make sure that the component we route to, is also displayed in our application. We still need to tell Angular “Hey, here’s where we want to display the thing that is loaded!“.
For that, we take a look at ContactsAppComponent
:
@Component({
selector: 'contacts-app',
template: `
<h1>Contacts App</h1>
<!-- here's where we want to load
the detail and the list view -->
`
})
export class ContactsAppComponent {
...
}
Nothing special going on there. However, we need to change that. In order to tell Angular where to load the component we route to, we need to use a directive called RouterOutlet
. Since we’ve imported RouterModule
into our application module, this directive is automatically available to us. Let’s add a <router-outlet>
tag to our component’s template so that loaded components are displayed accordingly.
@Component({
...
template: `
<h1>Contacts App</h1>
<router-outlet></router-outlet>
`
})
export class ContactsAppComponent {
...
}
Bootstrapping that app now displays a list of contacts! Awesome! The next thing we want to do is to link to ContactsDetailComponent
when someone clicks on a contact.
Linking to other routes
With the new router, there are different ways to route to other components and routes. The most straight forward way is to simply use strings, that represent the path we want to route to. We can use a directive called RouterLink
for that. For instance, if we want to route to ContactsDetailComponent
and pass the contact id 3
, we can do that by simply writing:
<a routerLink="/contacts/3">Details</a>
This works perfectly fine. RouterLink
takes care of generating an href
attribute for us that the browser needs to make linking to other sites work. And since we’ve already imported RouterModule
, we can simply go ahead and use that directive without further things to do.
While this is great we realise very quickly that this isn’t the optimal way to handle links, especially if we have dynamic values that we can only represent as expressions in our template. Taking a look at our ContactsListComponent
template, we see that we’re iterating over a list of contacts:
<ul>
<li *ngFor="let contact of contacts | async">
{{contact.name}}
</li>
</ul>
We need a way to evaluate something like {% raw %}{{contact.id}}{% endraw %}
to generate a link in our template. Luckily, RouterLink
supports not only strings, but also expressions! As soon as we want to use expressions to generate our links, we have to use an array literal syntax in RouterLink
.
Here’s how we could extend ContactsListComponent
to link to ContactsDetailComponent
:
@Component({
selector: 'contacts-list',
template: `
<h2>Contacts</h2>
<ul>
<li *ngFor="let contact of contacts | async">
<a [routerLink]="['/contacts', contact.id]">
{{contact.name}}
</a>
</li>
</ul>
`
})
export class ContactsListComponent {
...
}
There are a couple of things to note here:
- We use the bracket-syntax for
RouterLink
to make expressions work (if this doesn’t make sense to you, you might want to read out article on Angular’s Template Syntax Demystified - The expression takes an array where the first field is the segment that describes the path we want to route to and the second a dynamic value which ends up as route parameter
Cool! We can now link to ContactsDetailComponent
. However, this is only half of the story. We still need to teach ContactsDetailComponent
how to access the route parameters so it can use them to load a contact object.
Access Route Parameters
A component that we route to has access to something that Angular calls the ActivatedRoute
. An ActivatedRoute
is an object that contains information about route parameters, query parameters and URL fragments. ContactsDetailComponent
needs exactly that to get the id of a contact. We can inject the ActivatedRoute
into ContactsDetailComponent
, by using Angular’s DI like this:
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'contacts-detail',
...
})
export class ContactsDetailComponent {
constructor(private route: ActivatedRoute) {
}
}
ActivatedRoute
comes with a params
property which is an Observable
. To access the contact id, all we have to do is to subscribe to the parameters Observable
changes. Let’s say we have a ContactsService
that takes a number and returns an observable that emits a contact object. Here’s what that could look like:
import { ActivatedRoute } from '@angular/router';
import { ContactsService } from '../contacts.service';
@Component({
selector: 'contacts-detail',
...
})
export class ContactsDetailComponent {
contact: Contact;
constructor(
private route: ActivatedRoute,
private contactsService: ContactsService
) {
}
ngOnInit() {
this.route.params
.map(params => params['id'])
.subscribe((id) => {
this.contactsService
.getContact(id)
.subscribe(contact => this.contact = contact);
});
}
}
Oh, look what we have here! Notice that we’re nesting two subscribe()
calls? This is usually an indicator that we can refactor our code using flatMap()
, or even better switchMap()
, as we’re calling an asynchronous API and want to deal with out-of-order responses.
Let’s refactor our code:
ngOnInit() {
this.route.params
.map(params => params['id'])
.switchMap(id => this.contactsService.getContact(id))
.subscribe(contact => this.contact = contact);
}
Much better! But are observables really required to get hold of our id
parameter?
Using Router Snapshots
Sometimes we’re not interested in future changes of a route parameter. All we need is this contact id and once we have it, we can provide the data we want to provide. In this case, an Observable can bit a bit of an overkill, which is why the router supports snapshots. A snapshot is simply a snapshot representation of the activated route. We can access the id
parameter of the route using snapshots like this:
...
ngOnInit() {
this.contactsService
.getContacts(this.route.snapshot.params['id'])
.subscribe(contact => this.contact = contact);
}
...
Check out the running example right here!
ParamMap since Angular version 4.x
Because router parameters can either have single or multiple values, the params type should actually be { [key: string]: string | string[] }
. We as developers usually know if we expect a single or multiple values. That’s why since Angular version 4.x, there’s a new ParamMap
interface that we can use to decide whether we we’re interested in a single value (ParamMap.get()
) or multiple values (ParamMap.getAll()
).
The above code can therefore be rewritten as:
...
ngOnInit() {
this.contactsService
.getContacts(this.route.snapshot.paramMap.get('id'))
.subscribe(contact => this.contact = contact);
}
...
This feature has been introduce in this commit, go ahead and take a look for more information.
Of course, there’s way more to cover when it comes to routing. We haven’t talked about secondary routes or guards yet, but we’ll do that in our upcoming articles. Hopefully this one gives you an idea of what to expect from the new router. For a more in-depth article on the underlying architecture, you might want to read Victor’s awesome blog!