Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Jason Seminara
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Jason Seminara@jseminara )

FAILURE: Exploring The ngRx Router In Angular 2 RC 4

By Ben Nadel on

For eight months, I've been digging into the pre-release of Angular 2. And, for the last 6 weeks or so, I've been trying to wrap my head around Routing. For me, Routing was the last piece of the puzzle - the final hurdle before I felt like I could start building an actual Angular 2 application. Unfortunately, after 6 weeks of up-and-down, trial-and-error, switching routers, and a major refactoring, I feel like I am no closer to the finish line. As such, I'm going to take a break from my routing exploration; but, I thought I would at least share what I was able to do with the tools that are currently available (at least in RC 4).


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Angular 2 RC 4 Caveat: Please keep in mind that I started my router exploration in Angular 2 RC 3. And, RC 5 is what is currently live; and I suspect that RC 6 will drop shortly. So, my exploration is already out-dated. And, everything is still pre-release; so, none of my ramblings necessarily represent the final state of the router. So, take the following with a possible grain of salt.

As you would expect, I started my routing exploration with the new Component Router provided by the Angular 2 team. When you're only point of reference is the Angular 1.x $location and $route services, the Component Router first appears to be both wildly complex and yet, at the same time, seemingly missing some basic functionality provided by Angular 1.x. For example, the inability to change just a single query-param became painfully obvious on day-one. And, attempting to access the value of a route-parameter associated with a parent-segment required a choreographed gymnastics routine of dependency-injection and relative Tree navigation. And, if you want to determine if an arbitrary route is activated - well, that's not actually a publicly-exposed feature (although I'm told that this last point has been fixed in recent releases).

Despite the complexity of the Component Router, I was able to make decent progress. For a week or two. Then, I ran into a brick wall. Due to the way the Component Router renders views, you cannot have a conditionally hidden <router-outlet> tag at the time of route resolution. This is fine when you're in a leaf view; but, when you have nested views that require asynchronous data-loading, this requirement becomes a show-stopper.

Consider a view that has to hide its embedded view while it loads its own segment's data:

  • <!-- BEGIN: Loading State. -->
  • <template [ngIf]="isLoading">
  •  
  • Loading...
  •  
  • </template>
  • <!-- END: Loading State. -->
  •  
  •  
  • <!-- BEGIN: Loaded State. -->
  • <template [ngIf]="! isLoading">
  •  
  • {{ dynamic_data_here }}
  • <router-outlet></router-outlet>
  • {{ dynamic_data_here }}
  •  
  • </template>
  • <!-- END: Loaded State. -->

To me, this is core to the way I think about rendering. But, if you try to do this, Angular 2 throws the following error:

Error: Cannot find primary outlet to load

After I hit this wall, I struggled for a week or two to find a feasible work-around. I looked at using the "resolve" route configuration to see if I could load the data before the view component was mounted. But, this greatly increased the complexity of the data load and moved the ownership of the data outside of the component that consumed it, which seemed wholly unnatural and required hoisting all of the relevant services up into the bootstrapping process.

Furthermore, this requirement means that the shape and behavior of a component is drastically different based arbitrarily on whether or not the view contains another view, which seems very much like a code-smell.

Furthermore, if the nested views each perform asynchronous data-loading, you can quickly find yourself in a situation where the entire rendering of the page blocks while all the cascading datasets load, leaving the user with a white-screen and no feedback - no spinner, no nothing.

As far as I'm concerned, the inability to have a <router-outlet> inside an *ngIf block is a non-starter. Until that is fixed, I don't see the Component Router as a viable routing solution for the type of applications that I currently build in my day-to-day development.

After two weeks of thrashing around with the Component Router and route resolution, I gave up. I couldn't find any approach that provided both a satisfactory Developer Experience (DX) as well as a good User Experience (UX). I decided to jump ship and try the ngRx Router.

And lo, on the 8th day, God created the ngRx Router, and it was good!

The ngRx Router is not exactly a "drop in" replacement for the Component Router, but it has many similar ideas. That said, the mental model for the ngRx Router is an order of magnitude smaller than that required by the Component Router. But really, the most amazing thing about the ngRx Router is that your <route-view> tags can be inside *ngIf blocks.

Oh sweet chickens!

Once I refactored my router demo to use the ngRx Router, I was finally able to make some serious progress. But, the ngRx Router is not perfect. For example, it doesn't support auxiliary routes. And, like the Component Router, it completely lacks the ability to change query-string parameters independently. And, when you do change query-string parameters with the .search() method, it - for some unknown reason - replaces the current history item, which breaks the intuitive nature of the Browser's back-button. Of course, I was able to get around these limitations with a small Route Utilities class that provided some basic $location-inspired stop-gap functionality:

  • // Import the core angular services.
  • import { Injectable } from "@angular/core";
  • import { parse } from "query-string";
  • import { Router } from "@ngrx/router";
  •  
  • @Injectable()
  • export class RouterUtils {
  •  
  • // I hold the root router.
  • private router: Router;
  •  
  •  
  • // I initialize the service.
  • constructor( router: Router ) {
  •  
  • this.router = router;
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I navigate to the current URL with the give name-value query-string pair. This
  • // method is designed to leave all the other query-string parameters in place.
  • public gotoQueryParam( name: string, value: any ) : void {
  •  
  • this.gotoQueryParams({
  • [name]: value
  • });
  •  
  • }
  •  
  •  
  • // I navigate to the current URL with the given name-value pairs in the query-string.
  • // This method is designed to leave all other query-string parameters in place.
  • public gotoQueryParams( delta: { [key: string]: any } ) : void {
  •  
  • var parts = this.router.path().split( "?" );
  • var pathString = parts.shift();
  • var queryString = parts.shift();
  • var updatedQueryParams = parse( queryString );
  •  
  • for ( var key in delta ) {
  •  
  • if ( delta[ key ] === null ) {
  •  
  • delete( updatedQueryParams[ key ] );
  •  
  • } else {
  •  
  • updatedQueryParams[ key ] = delta[ key ];
  •  
  • }
  •  
  • }
  •  
  •  
  • this.router.go( pathString, updatedQueryParams );
  •  
  • }
  •  
  • }

Unfortunately, none of this progress means much of anything as I soon found out that the ngRx Router was officially deprecated:


 
 
 

 
ngRx Router deprecated tweet. 
 
 
 

Well, donkeys!

So where does that leave me with my router exploration? I'm not sure. I know that I'm a few RC releases behind at this point. So, I can try to upgrade and switch back over to the Component Router; but again, if I can't conditionally show a <router-outlet> tag, it is quite literally a deal-breaker. So, with the final release of Angular 2 looming, I am not entirely sure if there is officially-endorsed routing solution that works for the type of applications that I actually build.

Regardless of the state of routing, I still feel like my exploration was tremendously valuable. It really made me think about how I'd like to organize a large route-based application. The folder structure that I settled upon was one that completely mirrored the actual UI (user interface) hierarchy in the application. In my final approach, I don't differentiate between "layouts" and "views" - to me, they're all just "views". And, some views happen to be nested inside other views.


 
 
 

 
 Router folder hierarchy in Angular 2. 
 
 
 

Of course, I'm still very much learning and trying to figure things out - your mileage may vary. And, with the latest NgModule updates in Angular 2 RC 5, I am not yet sure how that would affect this structure, if at all. Actually, a good follow-up to this would be to take this demo and refactor it to use the latest RC with NgModules.

In the end though, I am not quite sure where routing is going. But, I have faith in the Angular 2 team that the final release will provide a positive developer experience while, at the same time, empowering us to provide enjoyable user experiences in our applications.




Reader Comments

For what it's worth, Brandon Roberts, whose been working on the Angular Component Router, tells me that they do have plans to allow the <router-outlet> to be conditionally rendered; but, it will not be done pre-release. So, there may be a bright future, but it seems like the initial release of Angular 2 will be limited to only certain types of applications.

Reply to this Comment

Ideally you don't need to place router outlet inside *giF structural directive.
You can have a long list of routes in route config and choose the component to load in outlet.

Reply to this Comment

@Manoj,

The router-outerlet is inside an *ngIf because that is what enables the cascading load in the user-experience. I am not sure how you would create the same UX simply by using different routes?

Reply to this Comment

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.