Quantcast
Channel: Articles by thoughtram
Viewing all articles
Browse latest Browse all 68

Routing in Angular 2 revisited

$
0
0

A long time ago we’ve written about routing in Angular 2 and you’ve probably noticed that this article is deprecated due to many changes and rewrites that happened in the router module of Angular 2. 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 2.

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!

Before we start

Just to make sure we don’t run into weird errors and debug tons of hours just to realise we had the wrong version of the router installed: We need @angular/router version >= 3.0.0-alpha.8. In other words, if we play with the code on our local machine, the package.json dependency should look like this:

"dependencies":{..."@angular/router":"3.0.0-alpha.8"}

Defining Routes

Let’s say we want to build a contacts application (in fact, this is what we do in our Angular 2 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>
`})exportclassContactsListComponent{...}

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>
`})exportclassContactsDetailComponent{...}

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:

interfaceRoute{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';exportconstContactsAppRoutes=[{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. Angular takes advantage of its dependency injection system to make this work. The easiest way to make our routes available via DI is to import a function called provideRouter(routes: RouteConfig), which creates providers for us.

import{bootstrap}from'@angular/core';import{provideRouter}from'@angular/router';import{ContactsAppComponent}from'./contacts.component';import{ContactsAppRoutes}from'./contacts.routes';bootstrap(ContactsAppComponent,[provideRouter(ContactsAppRoutes)]);

If you’ve read our articles on Dependency Injection in Angular 2 you know that bootstrap() takes a list of providers as second argument. That’s all we do here.

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>ContactsApp</h1>
<!--here'swherewewanttoloadthedetailandthelistview-->`})exportclassContactsAppComponent{...}

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. There are different ways to get hold of it, but the easiest is probably to import the ROUTER_DIRECTIVES, which is simply a predefined list of directives we can add to a component’s template like this:

import{ROUTER_DIRECTIVES}from'@angular/router';@Component({...directives:[ROUTER_DIRECTIVES]})exportclassContactsAppComponent{...}

We can now use all the directives that are exposed in that collection. That includes the RouterOutlet directive. Let’s add a <router-outlet> tag to our component’s template so that loaded components are displayed accordingly.

@Component({...template:`<h1>ContactsApp</h1>
<router-outlet></router-outlet>
`directives:[ROUTER_DIRECTIVES]})exportclassContactsAppComponent{...}

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:

<arouterLink="/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 added ROUTER_DIRECTIVES to our ContactsAppComponent, 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 {{contact.id}} 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>
`,directives:[ROUTER_DIRECTIVES]})exportclassContactsListComponent{...}

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 2’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 the dynamic value which ends up as route parameter
  • In order to use RouterLink in the template, we added ROUTER_DIRECTIVES to the component

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',...})exportclassContactsDetailComponent{constructor(privateroute: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',...})exportclassContactsDetailComponent{contact:Contact;constructor(privateroute:ActivatedRoute,privatecontactsService:ContactsService){}ngOnInit(){this.route.params.map(params=>params['id']).subscribe((id)=>{this.contactsService.getContact(id).subscribe(contact=>this.contact=contact);});}}

Using Router Snapshots

However, 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!

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!


Viewing all articles
Browse latest Browse all 68

Trending Articles