Home Articles

Go faster than navigation with Angular resolvers

Jul 1, 2020 ∘ Yoko Ishioka
Cowboy wrangling tumbleweed with key

I remember the first time I read about Angular resolvers, they sounded so cool. Let the router prefetch data? Yes, please! But when I tried to follow along the tutorial on Angular's website, I got a little lost.

While the writer did a really amazing job of breaking everything down, I kept running into null injector errors because it's a little different when you go from lazy-loaded library to library rather than straight from a module to your app. And you really need to understand the difference between the router, the activated route, and the router state to truly appreciate how cool Angular resolvers can be.

What is an Angular resolver anyway?

Think of an Angular pipe but one you use before the page even loads! That means you can transform or fetch data without having to subscribe to the Activated Route! In other words, no more waiting!

Resolvers can greatly enhance your app or website experience because they start the moment the user decides to navigate somewhere. In fact, the page won't even load until your resolver is finished. So, you don't have to scramble with race conditions and can be prepared for when they arrive.

Keep in mind we're talking about milliseconds, so don't go too crazy. Here are some use cases that I started working on:

  1. Generate and update meta tags
  2. Fetch blog articles
  3. Grab dynamic parameters to conduct search queries
  4. Create navigation menus based on your routing module

How do they work?

Imagine having a totally separate service that exists outside of your module act as though it was actually inside of your module. It uses the ActivatedRouteSnapshot and RouterStateSnapshot as arguments to its resolve method, so that it can use the same exact routing data you get with ActivatedRoute and RouterState to send you data.

Therefore, instead of subscribing to routing events, it has data prepared already for you! All you have to do is add extra parameters to your routing module, like you do with path, component, and data. So at the same level, just add a resolve parameter that states what key to use and which resolver should provide the data. Then, get the activated route's snapshot to retrieve the data.

Can you give me an end-to-end example?

Sure! A while back, I wrote an article about creating a self-updating navigation menu. While I really liked the aspect of updating the route and meta information in one place, I didn't like having to create multiple menus even if it was super simple and quick to do so.

So, now to create a navigation menu, I can add the following to my routing module:

    resolve: {
      displayTitle: MetaTagsResolverRouteDataService,
      menu: ResolveMenuService
     },
     

Note: 'displayTitle' and 'menu' are arbitrary values that are only declared here. They will be used as keys when it's time to find your route's data.

You have the option of using the resolver at the module, route, or component level. Remember, if you provide it to a parent route, all of its children can readily access it. However, you might want to keep in mind that it will fire every time a user navigates to that route.

demos-routing.module.ts

Next, I added data to the routing module to specify what values to use.

  {
    path: 'select-menus',
    component: DemoSelectMenusComponent,
    data: {
      title: 'Select Menus',
      description: "Cloud engineering demo of customizable select menus",
      menu: {
        icon: 'dropdown',
        label: 'Select Menus',
        show: true,
        activeSelfOnly: true,
      },
    },
   },

resolve-menu.service.ts

My resolver is super simple. It just parses out the data that's needed to build the navigation menu and returns it.

One thing to note is that there can be parent/child hierarchies depending on how you set up your routes. Almost always, my menus will be at the parent level because I want them to be on every page that belongs to their library. You can create a library-specific template with consistent layouts by using Angular's router outlet.

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from   '@angular/router';
import { ResolveMenu } from './menus';
import { Observable } from 'rxjs';


@Injectable({
  providedIn: 'root'
})

export class ResolveMenuService implements Resolve<ResolveMenu[]> {  

  constructor() { 
  }

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): ResolveMenu[] | Observable<ResolveMenu[]> {

  let menu: ResolveMenu[] = [];
  route.routeConfig.children.map(child => {
     if (child.data.menu)
        menu.push({
        path: child.path,
        show: child.data.menu.show,
        activeSelfOnly: child.data.menu.activeSelfOnly,
        title: child.data.title,
        label: child.data.menu.label,
        icon: child.data.menu.icon
      })
    })
  return menu;
  }
}

menus.demo.component.ts

Finally, to see the resolved data, I can call it like so:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ButtonRoute } from 'projects/buttons/src/public-api';

@Component({
  selector: 'ces-demos-menu',
  templateUrl: './demos-menu.component.html',
  styleUrls: ['./demos-menu.component.scss']
})

export class DemosMenuComponent implements OnInit {

  buttons: ButtonRoute[] = [];

  constructor(
    private route: ActivatedRoute,
  ) {}

  ngOnInit(): void {
    this.buttons = this.route.snapshot.data.menu;
  }
}

The resolved data is stored in the ActivatedRouteSnapshot and RouterStateSnapshot. Just look for whatever key you added when specifying the route, and it will be available in JSON format.

demos-menu.component.html

To render the menu, I just pass this.buttons value to the button route component I created to handle routes:

<nav>
  <ces-menu-toggle class="nav-buttons align-left" menuTitle="navigation" toggleIcon="ellipsis-vertical">
  <ces-button-route *ngFor="let button of buttons" [link]="button.path" [title]="button.title" [label]="button.label" [icon]="button.icon" [activeSelfOnly]="button.activeSelfOnly" class="nav-button">
  </ces-button-route>
</ces-menu-toggle>
</nav>

And voila, here is the navigation menu in action. I would normally hide it since it looks silly to have a navigation menu for one item, but will keep it up for now in case you want to take a peek. This way, in the future, anytime I add another demo to this section, the menu will automatically get created.

Here are the Github Gists if you want to see all of the files together.

Things to watch out for

Circular references

You cannot build a resolver inside of the same library/module that will use it. Likewise, you cannot use a service that's inside of the library/module nor put your resolver in the same library that the target library uses. This will create a circular reference and Angular is smart enough to let you know.

For example, I created a data library for my blog service to fetch articles from. I was not able to put the resolver in either of those libraries nor use the blog service to fetch the articles. So, I put my resolver in a routing library that uses the data service to fetch the articles.

It's best practice anyway to keep everything in one direction so as not to cause unwanted dependencies.

No provider errors

Remember, the resolver is like a floating route. So, if you want to use the resolver other than when the user is navigating to a certain route, pass in the ActivatedRoute and RouterState, not the ActivatedRouteSnapshot and RouterStateSnapshot.

Hopefully, you enjoyed this article. Thanks for reading it!