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

Resolving route data in Angular 2

$
0
0

Not long ago, we wrote about Navigation Guards and how they let us control the navigation flow of our application’s users. Guards like CanActivate, CanDeactivate and CanLoad are great when it comes to taking the decision if a user is allowed to activate a certain route, leaving a certain route, or even asynchronously loading a route.

However, one thing that these guards don’t allow us to do, is to ensure that certain data is loaded before a route is actually activated. For example, in a contacts application where we’re able to click on a contact to view a contact’s details, the contact data should’ve been loaded before the component we’re routing to is instantiated, otherwise we might end up with a UI that already renders its view and a few moments later, the actual data arrives (of course, there are many ways to get around this). Route resolvers allow us to do exactly that and in this article we’re going to explore how they work!

Want to see things in action first?

code View Demos

Understanding the problem

Let’s just stick with the scenario of a contacts application. We have a route for a contacts list, and a route for contacts details. Here’s what the route configuration might look like:

import{Routes}from'@angular/router';import{ContactsListComponent}from'./contacts-list';import{ContactsDetailComponent}from'./contacts-detail';exportconstAppRoutes:Routes=[{path:'',component:ContactsListComponent},{path:'contact/:id',component:ContactsDetailComponent}];

And of course, we use that configuration to configure the router for our application:

import{NgModule}from'@angular/core';import{BrowserModule}from'@angular/platform-browser';import{RouterModule}from'@angular/router';import{AppRoutes}from'./app.routes';@NgModule({imports:[BrowserModule,RouterModule.forRoot(AppRoutes)],...})exportclassAppModule{}

Nothing special going on here. However, if this is all new to you, you might want to read our article on routing.

Let’s take a look at the ContactsDetailComponent. This component is responsible of displaying contact data, so it somehow has to get access to a contact object, that matches the id provided in the route URL (hence the :id parameter in the route configuration). In our article on routing in Angular 2, we’ve learned that we can easily access route parameters using the ActivatedRoute like this:

import{Component,OnInit}from'@angular/core';import{ActivatedRoute}from'@angular/router';import{ContactsService}from'../contacts.service';import{Contact}from'../interfaces/contact';@Component({selector:'contacts-detail',template:'...'})exportclassContactsDetailComponentimplementsOnInit{contact:Contact;constructor(privatecontactsService:ContactsService,privateroute:ActivatedRoute){}ngOnInit(){letid=this.route.snapshot.params['id'];this.contactsService.getContact(id).subscribe(contact=>this.contact=contact);}}

Okay, cool. So the only thing ContactsDetailComponent does, is to fetch a contact object by the given id and assign that object to its local contact property, which then allows us to interpolate expressions like {{contact.name}} in the template of the component.

Let’s take a look at the component’s template:

<h2>{{contact?.name}}</h2><dl><dt>Phone</dt><dd>{{contact?.phone}}</dd><dt>Website</dt><dd>{{contact?.website}}</dd></dl>

Notice that we’ve attached Angular’s Safe Navigation Operator to all of our expressions that rely on contact. The reason for that is, that contact will be undefined at the time this component is initialized, since we’re fetching the data asynchronously. The Safe Navigation Operator ensures that Angular won’t throw any errors when we’re trying to read from an object that is null or undefined.

In order to demonstrate this issue, let’s assume ContactsService#getContact() takes 3 seconds until it emits a contact object. In fact, we can easily fake that delay right away like this:

import{Injectable}from'@angular/core';@Injectable()exportclassContactsService{getContact(id){returnObservable.of({id:id,name:'Pascal Precht',website:'http://thoughtram.io',}).delay(3000);}}

Take a look at the demo and notice how the UI flickers until the data arrives.

Depending on our template, adding Safe Navigation Operators everywhere can be quite tiring as well. In addition to that, some constructs don’t support that operator, like NgModel and RouterLink directives. Let’s take a look at how we can solve this using route resolvers.

Defining resolvers

As mentioned ealier, route resolvers allow us to provide the needed data for a route, before the route is activated. There are different ways to create a resolver and we’ll start with the easiest: a function. A resolver is a function that returns either Observable<any>, Promise<any> or just data. This is great, because our ContactsService#getContact() method returns an Observable<Contact>.

Resolvers need to be registered via providers. Our article on Dependency Injection in Angular 2 explains nicely how to make functions available via DI.

Here’s a resolver function that resolves with a static contact object:

@NgModule({...providers:[ContactsService,{provide:'contact',useValue:()=>{return{id:1,name:'Some Contact',website:'http://some.website.com'};}]})exportclassAppModule{}

Let’s ignore for a second that we don’t always want to return he same contact object when this resolver is used. The point here is that we can register a simple resolver function using Angular’s dependency injection. Now, how do we attach this resolver to a route configuration? That’s pretty straight forward. All we have to do is add a resolve property to a route configuration, which is an object where each key points to a resolver.

Here’s how we add our resolver function to our route configuration:

exportconstAppRoutes:Routes=[...{path:'contact/:id',component:ContactsDetailComponent,resolve:{contact:'contact'}}];

That’s it? Yes! 'contact' is the provider token we refer to when attaching resolvers to route configurations. Of course, this can also be an OpaqueToken, or a class (as discussed later).

Now, the next thing we need to do is to change the way ContactsDetailComponent gets hold of the contact object. Everything that is resolved via route resolvers is exposed on an ActivatedRoute’s data property. In other words, for now we can get rid of the ContactsService dependency like this:

@Component()exportclassContactsDetailComponentimplementsOnInit{contact;constructor(privateroute:ActivatedRoute){}ngOnInit(){this.contact=this.route.snapshot.data['contact'];}}

Here’s the code in action:

In fact, when defining a resolver as a function, we get access to the ActivatedRouteSnapshot, as well as the RouterStateSnapshot like this:

@NgModule({...providers:[ContactsService,{provide:'contact',useValue:(route:ActivatedRouteSnapshot,state:RouterStateSnapshot)=>{...}]})exportclassAppModule{}

This is useful in many scenarios where we need access to things like router parameters, which we actually do. However, we also need a ContactsService instance, which we don’t get injected here. So how do we create resolver that need dependency injection?

Resolvers with dependencies

As we know, dependency injection works on class constructors, so what we need is a class. We can create resolvers as classes as well! The only thing we need to do, is to implement the Resolve interface, which ensures that our resolver class has a resolve() method. This resolve() method is pretty much the same function we have currently registered via DI.

Here’s what our contact resolver could look like as a class implementation:

import{Injectable}from'@angular/core';import{Resolve,ActivatedRouteSnapshot}from'@angular/router';import{ContactsService}from'./contacts.service';@Injectable()exportclassContactResolveimplementsResolve<Contact>{constructor(privatecontactsService:ContactsService){}resolve(route:ActivatedRouteSnapshot){returnthis.contactsService.getContact(route.params['id']);}}

As soon as our resolver is a class, our provider configuration becomes simpler as well, because the class can be used as provider token!

@NgModule({...providers:[ContactsService,ContactResolve]})exportclassAppModule{}

And of course, we use the same token to configure the resolver on our routes:

exportconstAppRoutes:Routes=[...{path:'contact/:id',component:ContactsDetailComponent,resolve:{contact:ContactResolve}}];

Angular is smart enough to detect if a resolver is a function, or a class and if it’s a class, it’ll call resolve() on it. Check out the demo below to see this code in action and note how Angular delays the component instantiation until the data has arrived.

Hopefully this gave you a better idea of how route resolvers in Angular 2 work!


Viewing all articles
Browse latest Browse all 68

Trending Articles