Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Justine Arreche
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Justine Arreche@saltinejustine )

Exploring $route Resolution In AngularJS

By Ben Nadel on

After my recent posts on AngularJS routing and the back-button and how route params are passed-around in redirectTo actions, I wanted to take a look at route resolution. This is a feature that I've never actually used before; but, in AngularJS, you can defer the triggering of the $routeChangeSuccess event until certain values have been resolved. These can be static values; or, they can be wrapped up in promises that get resolved (or rejected) eventually.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When it comes to routing, there are a number of moving parts. When you throw eventual resolution into the mix, it can become unclear as to which parts of the routing ecosystem are affected. The key to understanding this is to understand where route-resolution falls within the route-change workflow.

Looking at the source code, here's the general workflow for routing:

  • Step 1: Route is parsed - route is matched to current location. Pending params and pathParams are populated. $routeChangeStart is triggered.
  • Step 2: Parsed route is stored as "current" route (making params and pathParams available through the $route service).
  • Step 3: If redirectTo is present, location is changed (will cause new route change but, will not stop resolve functions from being invoked).
  • Step 4: Resolve functions are invoked and aggregated using $q.all().
  • Step 5-a: If values are resolved, they are copied into "$route.current.locals". The current params are copied into $routeParams. The $routeChangeSuccess event is triggered.
  • Step 5-b: If one or more values is rejected, the $routeChangeError event is triggered.

When you look at this workflow, there are a few important take-aways. First, the $route service is updated before any of the resolve-factories are invoked. And second, if the route-change fails (ie, by one rejected promise), nothing gets reverted. In fact, the only difference between a route resolution and a route rejection is which event gets triggered - success vs. error - and whether or not the $routeParams service is updated.

Beyond the general routing workflow, the other point of confusion that I had going into this is how often route resolution has to take place. Before I started reading about it, I had assumed that a route would only ever need to be resolved once - kind of like a service in the dependency-injection container. But, there's nothing in the routing workflow to suggest this. And, if you try my demo, you will see that one of the routes has to be resolved (using a $timeout) every time that the route is accessed.

Of course, you can implement logic that explicitly maintains a resolve-promise across route-changes. This would, for all intents and purposes, mean that the route only has to be resolved once. But, that is fodder for another blog post.

When the resolve-promises have all been resolved, they are stored in the "locals" object of the current route. This makes them available to any controller or service that has the $route service injected. However, if you are using ngView (which I am not), these locals are also used to augment the dependency-injection values during Controller instantiation.

To see this in action, I've put together a demo with three routes - A, B, and C. Route A is resolved immediately with a static value. Route B is resolved eventually with a timeout. And, Route C is resolved immediately with a rejected promise.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Exploring $route Resolution In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Exploring $route Resolution In AngularJS
  • </h1>
  •  
  • <p>
  • <a href="#/a/1">Section A</a>
  • &mdash;
  • <a href="#/b/20">Section B</a>
  • &mdash;
  • <a href="#/c/300">Section C (will fail)</a>
  • </p>
  •  
  • <p>
  • <strong>Route Action</strong>: {{ routeAction }}
  • </p>
  •  
  • <!--
  • BEGIN: Route Activity Notification. This indicator will show up while the
  • route is being resolved. This will give the user some indication that "work"
  • is being done.
  • -->
  • <div
  • ng-controller="RouteActivityController"
  • ng-show="isResolvingRoute"
  • class="route-activity">
  •  
  • <em>Loading Route...</em>
  •  
  • </div>
  • <!-- END: Route Activity Notification. -->
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-route-1.3.8.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [ "ngRoute" ] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I setup the routes for the application.
  • app.config(
  • function( $routeProvider ) {
  •  
  • // Define routes for the application.
  • $routeProvider
  • .when(
  • "/a/:id",
  • {
  • action: "section-a",
  • resolve: {
  • thingOne: function( $q ) {
  •  
  • // Immediately resolved with static value.
  • return( "thingOneValue" );
  •  
  • }
  • }
  • }
  • )
  • .when(
  • "/b/:id",
  • {
  • action: "section-b",
  • resolve: {
  • thingTwo: function( $timeout ) {
  •  
  • // Resolved with timeout.
  • // --
  • // NOTE: Since $timeout() won't resolve with a
  • // "value", then "thingTwo" will be undefined in
  • // "locals" within the $route.current object.
  • return( $timeout( angular.noop, 1000, false ) );
  •  
  • },
  • thingThree: function( $q, $timeout ) {
  •  
  • // Immediately resolved with promise.
  • return( $q.when( "thingThreeValue" ) );
  •  
  • }
  • }
  • }
  • )
  • .when(
  • "/c/:id",
  • {
  • action: "section-c",
  • resolve: {
  • thingFour: function( $q ) {
  •  
  • // This will cause the route to fail and trigger the
  • // $routeChangeError event.
  • return( $q.reject( "meh" ) );
  •  
  • }
  • }
  • }
  • )
  •  
  • // And, if nothing else matches, just redirect to section A.
  • .otherwise({
  • redirectTo: "/a/9"
  • })
  • ;
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope, $route, $routeParams, $location ) {
  •  
  • $scope.routeAction = null;
  •  
  • // I listen for route change events.
  • $scope.$on(
  • "$routeChangeSuccess",
  • function handleRouteChangeSuccessEvent( event ) {
  •  
  • // When the route is finally resolved, the results of the
  • // resolution will be available in the ".locals" object on the
  • // current route. This object always exists (upon success), but
  • // will be empty if the route didn't have to resolve anything.
  • // --
  • // CAUTION: "locals" only exists when route is resolved - it will
  • // not exist if the route was rejected.
  • console.log( "Route loaded with locals:", $route.current.locals );
  •  
  • // Store our current action for output.
  • $scope.routeAction = $route.current.action;
  •  
  • }
  • );
  •  
  • // I listen for route failure events.
  • $scope.$on(
  • "$routeChangeError",
  • function handleRouteChangeErrorEvent( event ) {
  •  
  • console.warn( "Route failed!" );
  • console.log( "Route:", $route.current );
  •  
  • // Warning: On error, the $routeParams are NOT updated; however,
  • // the $route.current.params and $route.current.pathParams ARE
  • // updated to reflect even the failed route.
  • console.log( "Current Params:", $route.current.params );
  • console.log( "Route Params:", $routeParams );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the route-activity indicator.
  • app.controller(
  • "RouteActivityController",
  • function( $scope ) {
  •  
  • // I determine whether or not there is pending activity on the route
  • // change. If the route change doesn't have to resolve anything, the
  • // route transition will [likely] start and end within the bounds of
  • // a single digest phase. However, if there is data to resolve, the
  • // route transition will be spread across multiple digests and the
  • // "loading" message will be displayed.
  • $scope.isResolvingRoute = false;
  •  
  • // When the route starts changing, mark the activity.
  • $scope.$on( "$routeChangeStart", handleRouteChangeStartEvent );
  •  
  • // Whether or not the route finishes successfully, or in error, it is
  • // done. As such, we need to mark is as done resolving.
  • $scope.$on( "$routeChangeSuccess", handleRouteChangeFinallyEvent );
  • $scope.$on( "$routeChangeError", handleRouteChangeFinallyEvent );
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I handle either the Success or Error route change events.
  • function handleRouteChangeFinallyEvent( event ) {
  •  
  • $scope.isResolvingRoute = false;
  •  
  • }
  •  
  •  
  • // I handle the the Start route change event.
  • function handleRouteChangeStartEvent( event ) {
  •  
  • $scope.isResolvingRoute = true;
  •  
  • }
  •  
  • }
  •  
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

If I run this page and click across the three routes, I get the following console output:


 
 
 

 
 Exploring route resolution in AngularJS and how the $route service is updated upon failure. 
 
 
 

As you can see, the resolved values from route A and route B are made available in both the current route - $route.current.locals - as well as in the $routeParams service. When route C fails to resolve, however, nothing is reverted. The URL still contains the "c" route. And, when we inspect the current route - $route.current.params - we see that they contain the params extracted from route C's location.

This post was intended to explore the mechanics of route-resolution. At this time, I don't really have a good use-case for it; but, now that I understand a bit more about how it works, I can start to noodle on what you would use it for. Personally, I suspect that route resolution will be a lot more exciting in AngularJS 2.0 when we can [easily] lazily-load sub-modules of an application.

Tweet This Titillating read by @BenNadel - Exploring $route Resolution In AngularJS Thanks my man — you rock the party that rocks the body!



Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.