WebCake

If you’ve use UI-Router, there’s a really good chance you’re familiar with resolves. The folks on my team know I’m personally a huge fan, and I was glad to see that the new router in Angular 2 allow us to make use of them.

Unfortunately, at least at the time of this writing, there’s very little written about resolves in Angular 2, and it’s not explained in the developer guides for routing. But that shouldn’t stop us, right?

This post outlines how I was able to get resolves working using the RC4 release of Angular 2. It’s not intended to be an expert overview; rather, it’s one small success story, and the steps involved in replicating.

This post also assumes that you have an application where routes are already established. It does not walk you through how to create routes.

Understanding the Router

In order to understand the router, we’ll have to first take a step back and analyze the application bootstrap() method.

When we declare routes (as in my previous blog post), we do so via dependency injection in the bootstrapping of our core component:

import { bootstrap }    from '@angular/platform-browser-dynamic';

import { AppComponent } from './app.component';
import { appRouterProviders } from './app.routes';

bootstrap(AppComponent, [appRouterProviders]);

In this example, appRouterProviders is a list of routes that indicate to the <router-outlet> component within my AppComponent how to behave – that is, what components to load, based on route programming we’ve provided.

We can therefore assume that regardless of the components or routes to which we’re adding resolved data, our routing logic actually lives within the base component. That being the case, we’ll have to load any routing logic into the application bootstrap() method, which includes the routes that we plan to resolve, plus the classes that help us resolve them.

Here’s what that would look like:

import { bootstrap }    from '@angular/platform-browser-dynamic';

import { AppComponent } from './app.component';
import { appRouterProviders } from './app.routes';
// assuming that we only have one class to resolve data...
import { DataResolver } from './app.resolve';

bootstrap(AppComponent, [DataResolver, appRouterProviders]);

So far, this has been the only way I’ve gotten it to work regardless of which <router-outlet> loads the route with a resolve. For example, the sample project I used to build my previous post had a BaseComponent in its Division module that loads one of three child-routes in a <router-outlet>, and in a local branch I have one of those child-routes using a resolve. Despite that being the most immediate loading point for the routes in question, I still need to set the DataResolver as a provider on the bootstrap(); if I don’t, I get an exception once I load the route that has a resolve set, despite any attempts to set that DataResolver as a provider in the BaseComponent.

At this point I have not taken a tour of the source code (nor would I necessarily be able to find my way around if I did), so I can only guess as to why I would see this behavior. And in guessing, I think it might be because all declared routes are instantiated as part of the bootstrapping process, and aren’t considered local to any components. I’m curious to see how that changes once the Angular team introduces the @AppModule decorator in the future.

How to Construct a Resolve

By design, a resolve returns an observable BehaviorSubject, a la Rxjs. If you look at the docs on the Angular website for the Resolve interface, you can come to a few conclusions about how we’ll construct resolves, and what information they have access to. Here’s the interface details:

class TeamResolver implements Resolve {
  constructor(private backend: Backend) {}
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<any> {
    return this.backend.fetchTeam(this.route.params.id);
  }
}

The interface details listed in the docs require the route and state arguments to be set, thus giving us access to any data we would typically want to access in the currently active route. They also specify that you can either return an Observable<any>, or something of type any. Which essentially means you can return anything.

First implementation

In the spirit of returning anything, let’s start with a simple example that resolves a string. Somewhere accessible – for example, app/app.resolves.ts – create a file that will hold your classes that implement the Resolve interface. Once you have that, add the following:

import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

export class DataResolver implements Resolve<string> {
    constructor() {}

    resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):string {
        console.log('returning resolved data');
        return 'For science...you monster';
    }
}

When we implement this class on a route’s resolve property, we’ll get access to the string 'For science...you monster' as a result. Let’s go ahead and find out. In one of the files in which you declare your routes, import this DataResolver class, and then add it as the value of a route’s resolve property:


import { RouterConfig } from '@angular/router'; import { YourComponent } from './path/to/your.component'; // I'm importing my resolver here: import { DataResolver } from './path/to/app.resolve'; export const divisionRoutes: RouterConfig = [ // feel free to include other paths; i'm just showing one for simplicity {path: 'testing/resolves', component: YourComponent, resolve: {testing: DataResolver}} ];

Be sure to include your DataResolver class in the bootstrap() method of your main.ts, or wherever it’s called. You can use the example at the top of this post as a reference.

The last thing we need to do is to modify our component so that it demonstrates the resolution of data. Two things to note, as we’re doing this:

  • Resolved data is attached to the data property of the ActivatedRoute class, accessible through @angular/router
  • The data property of the ActivatedRoute class is a BehaviorSubject, which means we’ll need to subscribe or map/subscribe to it in order to access its values

Here’s how we can do that in a component:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
    selector: 'your-component',
    template: `<h2>Check the console logs.</h2>`
})
export class YourComponent {
    constructor(public route:ActivatedRoute) {
        console.log('component loaded');
        // subscribe to data as you would any other observable
        // notice data can have many properties
        this.route.data.subscribe(val => console.log(val.testing));
        // depending on your configuration, the ts compiler might
        // throw an error, and you may have to write your console
        // log using square brackets: console.log(val['testing']);
    }
}

Now, if you load up your app, and navigate to your route that includes resolved data, you’ll see the string we hard-coded logged in the console, after the other two messages indicating that data is being resolved, and your component has been loaded. Here’s what I see when I navigate to http://localhost:3000/testing/resolves:

Synchronous console logs

Resolving asynchronous data

Let’s get a little nuts, shall we? In real life, you’re more likely to resolve data from a backend service. So you’re not really going to return a synchronous value; instead you’ll likely return a promise or an observable as a property in your resolve data. So let’s revise our DataResolver class to return async data, this time through a promise that resolves after a setTimeout:

export class DataResolver implements Resolve<Promise> {
    constructor() {}

    resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Promise {
        console.log('returning resolved data');
        return new Promise(resolve => {
            setTimeout(() => {
                resolve('For science...you monster');
            }, 5000);
        });
    }
}

We’re not changing much. Instead of returning a string, we’re returning a promise that will be resolved after five seconds. That being the case, we have to update our component class to handle a promise instead of an immediately returned string:

export class YourComponent {
    constructor(public route:ActivatedRoute) {
        console.log('component loaded');
        this.route.data.subscribe(val => {
            // logging this for experimentation purposes
            // I'll explain in a minute...
            console.log('resolve value changed');
            // since we're resolving a promise, we have to
            // handle it using an async callback
            val.testing.then(data => console.log(data))
        });
    }
}

Now if you refresh your view, you should see the following after five seconds:

Async console logs

A Few Key Takeaways

One of the first things I wanted to tinker with – and you might have noticed I did in the code block above – was the timing in which my route data was considered ‘resolved’. If you’re not familiar with UI-Router this might seem obvious, but I wanted to see if my data was considered ‘resolved’ when the promise was returned, or when the promise itself was resolved. If you watch the behavior of the component, you’ll notice two things:

  • the component loads well before the setTimeout expires, meaning route states are not blocked by unresolved data
  • the string ‘resolve value changed’ shows up in the logs immediately, meaning the route’s resolve property simply returns the logic to fetch the resolved value; it’s up to the component to act appropriately based on whether or not that value is available.

I’m also going to play around some more with observable resolved data. A few things I’m interested in are the actual behavior of a full HTTP request in a resolve, as well as finding a good pattern for mapping multiple observable properties on the data object to the active route. It seems like without a clean pattern for doing that, there would be a lot of repetitive code, which no one likes.

2 responses to “Exploring Resolved Data in the Angular 2 Router”

  1. Hello. Great article!
    Have you finally worked with observable resolved data? I have not been able to make it work by now.

    Thanks ahead.

    • Colin says:

      Nothin doin yet, but I’m taking the next few days to refamiliarize with the latest changes and the new NgModel structure. So I’m hoping to update some demos that I have out there with things like observable resolves soon.

Leave a Reply