Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with:

Mapping AngularJS Routes Onto URL Parameters And Client-Side Events

By Ben Nadel on

Earlier this week, I talked about mapping RESTful resource URIs onto URL parameters and server-side events. When developing a thick-client application, much of the same URL routing functionality is required on the client. Only, with the ever-growing complexity of rich user interfaces, routing and partial page rendering on the client becomes much more difficult. Lately, I've been looking at Google's AngularJS as a declarative framework for client-side applications. And, as a follow-up to yesterday's post, I thought I would look at mapping AngularJS routes onto client-side URL parameters and rendering events.


 
 
 

 
  
 
 
 

AngularJS comes with a fairly robust routing and history management mechanism. Out of the box, it supports both hash-based routing as well as HTML5 push-state (I've only played with hash-based routing). The route provider is primarily concerned with mapping routes onto template paths and is configured with a series of when() method calls:

  • $route.when(
  • "/friends/:friendID",
  • {
  • templateUrl: "views/friend-detail.htm"
  • }
  • );

In the above case, the given route causes the given template to be rendered in an "ng-view" AngularJS directive. This approach is great for smaller websites and shallow web applications; but, when you have nested navigation and complex interfaces, the route-to-partial paradigm can quickly become quite limiting.

Fortunately, you don't have to use templates with routes. Instead, you can use routing to resolve client-side render events. In the same way that I resolved resource URIs onto server-side events, we can resolve client-side routes onto a hierarchy of values that defines the state of the rendered page.

The trick to this is understanding that the hash used to define the AngularJS route can contain any arbitrary data (so long as it doesn't conflict with some reserved values). So, for example, rather than passing in a templateUrl, we can pass in event data:

  • $route.when(
  • "/friends/:friendID",
  • {
  • event: "friends.view"
  • }
  • );

In this case, we are mapping the route onto the render event "friends.view". This event parameter will then be exposed as part of the $route state. The ":friendID" value, in the route, will also be exposed as part of the $routeParams state.

To see how this can be used to render a page, I've created a light-weight demo with a single Controller that conditionally renders content based on the route-provided render-event.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>AngularJS Routing</title>
  •  
  • <style type="text/css">
  •  
  • a {
  • color: #333333 ;
  • }
  •  
  • a.on {
  • color: #CC0000 ;
  • font-weight: bold ;
  • text-decoration: none ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • AngularJS Routing
  • </h1>
  •  
  • <p>
  • Current Render Action:
  •  
  • <!--
  • We're going to bind the content of the Strong element to
  • the scope-level model, renderAction. Then, when this gets
  • set in the Controller, it will be updated here.
  • -->
  • <strong ng-bind="renderAction">Unknown</strong>
  • </p>
  •  
  • <!--
  • For the navigation, we'll be conditionally adding the "on"
  • class based on the state of the current scope.
  • -->
  • <p>
  • <a href="#/home" ng-class="{ on: isHome }">Home</a> -
  • <a href="#/friends" ng-class="{ on: isFriends }">Friends</a> -
  • <a href="#/contact/ben" ng-class="{ on: isContact }">Contact</a>
  • </p>
  •  
  • <!--
  • When the route changes, we're going to be setting up the
  • renderPath - an array of values that help define how the
  • page is going to be rendered. We can use these values to
  • conditionally show / load parts of the page.
  • -->
  • <div ng-switch on="renderPath[ 0 ]">
  •  
  • <!-- Home Content. -->
  • <div ng-switch-when="home">
  •  
  • <p>
  • This is the homepage content.
  • </p>
  •  
  • <p>
  • Sub-path: <em>{{ renderPath[ 1 ] }}</em>.
  • </p>
  •  
  • </div>
  •  
  • <!-- Friends Content. -->
  • <div ng-switch-when="friends">
  •  
  • <p>
  • Here are my friends!
  • </p>
  •  
  • <p>
  • Sub-path: <em>{{ renderPath[ 1 ] }}</em>.
  • </p>
  •  
  • </div>
  •  
  • <!-- Contact Content. -->
  • <div ng-switch-when="contact">
  •  
  • <p>
  • Feel free to contact me.
  • </p>
  •  
  • <p>
  • Sub-path: <em>{{ renderPath[ 1 ] }}</em>.
  • </p>
  •  
  • <p>
  • Username: <em>{{ username }}</em>
  • </p>
  •  
  • </div>
  •  
  • </div>
  •  
  •  
  • <!-- Load AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var Demo = angular.module( "Demo", [] );
  •  
  • // Configure the routing. The $routeProvider will be
  • // automatically injected into the configurator.
  • Demo.config(
  • function( $routeProvider ){
  •  
  • // Typically, when defining routes, you will map the
  • // route to a Template to be rendered; however, this
  • // only makes sense for simple web sites. When you
  • // are building more complex applications, with
  • // nested navigation, you probably need something more
  • // complex. In this case, we are mapping routes to
  • // render "Actions" rather than a template.
  • $routeProvider
  • .when(
  • "/home",
  • {
  • action: "home.default"
  • }
  • )
  • .when(
  • "/friends",
  • {
  • action: "friends.list"
  • }
  • )
  • .when(
  • "/contact/:username",
  • {
  • action: "contact.form"
  • }
  • )
  • .otherwise(
  • {
  • redirectTo: "/dashboard"
  • }
  • )
  • ;
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define our root-level controller for the application.
  • Demo.controller(
  • "AppController",
  • function( $scope, $route, $routeParams ){
  •  
  • // Update the rendering of the page.
  • render = function(){
  •  
  • // Pull the "action" value out of the
  • // currently selected route.
  • var renderAction = $route.current.action;
  •  
  • // Also, let's update the render path so that
  • // we can start conditionally rendering parts
  • // of the page.
  • var renderPath = renderAction.split( "." );
  •  
  • // Grab the username out of the params.
  • //
  • // NOTE: This will be undefined for every route
  • // except for the "contact" route; for the sake
  • // of simplicity, I am not exerting any finer
  • // logic around it.
  • var username = ($routeParams.username || "");
  •  
  • // Reset the booleans used to set the class
  • // for the navigation.
  • var isHome = (renderPath[ 0 ] == "home");
  • var isFriends = (renderPath[ 0 ] == "friends");
  • var isContact = (renderPath[ 0 ] == "contact");
  •  
  • // Store the values in the model.
  • $scope.renderAction = renderAction;
  • $scope.renderPath = renderPath;
  • $scope.username = username;
  • $scope.isHome = isHome;
  • $scope.isFriends = isFriends;
  • $scope.isContact = isContact;
  •  
  • };
  •  
  • // Listen for changes to the Route. When the route
  • // changes, let's set the renderAction model value so
  • // that it can render in the Strong element.
  • $scope.$on(
  • "$routeChangeSuccess",
  • function( $currentRoute, $previousRoute ){
  •  
  • // Update the rendering.
  • render();
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

In this demo, our application controller is listening for changes to the route. When a route is changed (also fires when page loads for the first time), the AppController extracts the route-mapped "action" value and injects it into the scope of the page. This value (and its derivative, renderPath) is then used to conditionally render the page using "ng-switch" directives.

Right now, this page has only one level of nesting. But, ng-switch directives can easily be nested in order to allow for deep-linking of your application. Furthermore, the "ng-switch-when" directive can be combined with the AngularJS "ng-include" directive to lazy-load page partials:

  • <div ng-switch on=" renderPath[ 0 ]">
  •  
  • <div ng-switch-when="home" ng-include=" 'home.htm' "></div>
  • <div ng-switch-when="friends" ng-include=" 'friends.htm' "></div>
  • <div ng-switch-when="contact" ng-include=" 'contact.htm' "></div>
  •  
  • </div>

On the server-side, events tend to map to resources and the access / mutation of those resources. In a rich, client-side application, however, events are a different beast. Rather than mapping to resources, they map to page state. And, depending on the complexity of your user interface, state may be defined as a complex hierarchy of sections. Fortunately, AngularJS routes make it possible to map routes onto render events which can then be used to drill down through a rendered page.




Reader Comments

Is this really more manageable than using templateUrl: in the routeProvider?

You are still having to define

  • $routeProvider.when(...)

for each route and then you just put the partial view in series of ng-include / switch-case statements and a boolean is{Action.name} for each action url...

So... why is this more manageable than just using templateUrl for your view? In this example, it just seems like you've created more code to do the same thing that the $routeProvider service does already...

Reply to this Comment

@DjebbZ,

I am not sure if it's documented :) But, I am building an app that relies on this concept.

@Kelt,

Very excellent question. I would say that if you are building an app that is using a single ngView directive, then my approach adds no advantage above and beyond the route functionality already provided out of the box.

In my particular context (which I didn't really outline in the post), I have a complex UI that has a good number of nested views. As such, we're using ngSwitch and ngInclude to build the completed UI based on the kind of "action" variables that are rendered.

So, all to say, you are correct, if the ngView works for you, rock it. If not, it's nice that the routing is extensible. One of these days, I hope to outline what I'm doing a bit more; but, right now, my deadline is dominating my life :(

Reply to this Comment

When $routeProvider.otherwise({redirectTo:'/dashboard'});

TypeError: Cannot call method 'split' of undefined
at AppController.render

How to resolve this problem?

Reply to this Comment

Hi Ben, you solved my purpose which i was looking desperately. as my application as different views and partial views have there own left navigation. i could successfully apply this method .but could not apply css using ng-select as my tabs are generated dynamically and selection of tab is not working in partial.

ng class could not resolve binding in main app controller

  •  
  • <mytag class="ng-binding" hr="#/Education/0" ng-1353154597234="61" ng-class="{selected: isEducation}">

ideally ng-class binding should evaluate in main app controller. i.e. ng-class="{selected}" as $scope.isEducation is true

Any help is highly appreciated as am blocked on my delivery.

Reply to this Comment

@praveen,

You should probably use {'selected': isEducation} ...

selected is not a variable, it's a string so put it in quotes.

Reply to this Comment

@Ben,

Thanks for the article and screencast, it's an interesting way to use Angular's routing.

However, wouldn't it also be possible to achieve by using `resolve` parameter when defining a route?

For example:

  • $routeProvider
  • .when(
  • "/contact/:username",
  • {
  • controller: 'AppController',
  • resolve: { action: function () { return "contact.form"; }
  • }
  • )
  • // Other routes

According to Angular docs for $routeProvider, then in your controller you'll be able to inject and use the value returned by `action`:

  • Demo.controller("AppController", function($scope, $route, $routeParams, action) {
  • var renderPath = action.split(".");
  • // Everything like before
  • });

This way is a bit verbose than yours, but feels more in AngularJS style. I wonder if I'm missing something, though.

Reply to this Comment

Admittedly off-topic but since when has

  • Demo.controller("AppController", function(){...});

replaced a global function AppController() as the way to write controllers in Angular?

Reply to this Comment

On topic now. The code doesn't run. Fields is correct about the error: $route.current.action is undefined (and undocumented). Could you post an update?

Reply to this Comment

I don't know if this is a good idea. What about if we need different controller for different view?

Thanks,

Reply to this Comment

Thanks a lot, Ben!!
I was looking for a way to pass some data to the controllers via the routing mechanism.

Also whatever anton says also makes sense.

Reply to this Comment

@Fields, @Marc,

When you have a redirect, there is no action. It's a edge case that this particular demo doesn't deal with. Hopefully by Friday, I'll have a much more robust demo to post that uses this approach. But basically, when the $routeChangeSuccess event fires, you need to check to see if the action is defined (which is won't be on a redirect).

For this reason, I don't actually use the $routeChangeSuccess in my app (as the primary means to update views). Rather I broadcast a "requestContextChanged" event, which has additional data along with it. But, like I sad, hopefully on Friday I'll have finished putting my demo together.

Reply to this Comment

@Anton,

I looked into the resolve stuff a bit. There's a pretty good blog post on it, including how to delay view change until the view data is resolved (or something along those lines). For me though, I wanted to leave data gathering up to the Controller itself. The route provide simply worries about mapping routes onto actions / parameters - the Controllers take care of the rest.

Reply to this Comment

@Mingcai,

I think using different Controllers for a single view is a rather rare edge case. And, from what I have seen, even when it seems like a good idea at first, updating the app later necessitates breaking things apart.

That said, there's nothing about this approach that actually requieres you to use one Controller per View. This simply prepares an "action" variable. How you use that - how your Controllers work and which views get included can still be completely dynamic.

I'll be able to expand on this in my upcoming demo.

Reply to this Comment

Thanks Ben...I found this very useful. I'm using this concept to coordinate routing with multiple ng-includes. Seems to work great at keeping multi-level navigation, URL, and view content synchronized in an elegant manner. Thanks again..

Reply to this Comment

Hi there, Django & Python lover here.

I'm wondering if you've discovered a Don't Repeat Yourself approach to resolving url routes in templates?

In Django we are able to give our server side routes names and then we can generate the urls in templates and in views by referring to the name instead of the full path.

It's kind of awesome.

I'm hoping Angular has something similar?

Reply to this Comment

Hey, this is a great blog, thanks. I wondered if you can achieve these goals without needing to add the switch statement into the HTML markup?

That is, can the state of the UI be pushed entirely from the events and the code?

The way this works is demonstrated in frameworks like Caliburn.Micro (Silverlight), or the newer Durandal.js (JavaScript):

http://durandaljs.com/documentation/Using-Composition/

That describes the "Visual Composition" approach there, where a value is matched to its visual view and then injected into the DOM, and the data-binding will recompose the view as the underlying model changes.

Thanks,
Josh

Reply to this Comment

@Zenobius,

Hmmm. Ultimately, you render the root page of the app; so, if that is a Python-generated page (sorry, not too familiar with how Python renders HTML templates), you can definitely generate the JavaScript in the root page to define the routes. In my demo, the JavaScript is static; but there's no reason it can't be generated dynamically as part of the rendering.

Is that what you mean?

Reply to this Comment

@Josh,

That's an excellent question. The way I see it is that having the Switch in the Template allows the controller to be separate from the logic that determines which View to actually render. So, the Controller manages the data and the Template manages the rendering.

To further explain, I recently had a template called something like this:

use-case.htm

... then, over time, the rendering got more feature-rich and I needed to break it up into some swappable sub-templates. So, I created a directory, "use-case" to house the use-case and it's associated views:

./use-case/index.htm
./use-case/sub-view-A.htm
./use-case/sub-view-B.htm

The beauty of keeping this all in the Template was that the Controller didn't have to change at all. Since the controller only managed thew $scope (or "View Model"), it didn't care where the templates were stores or how they were organized. It made the refactoring much more localized.

Reply to this Comment

@Ben
Oh I never mix my angularjs assets into my Django templates. That wouldn't scale under high load.

I only mention urlconfs in Django as an example. Every Django app and project can (and should) define a set of URL routing rules. Essentially regex patterns that point at views. I.e.

  • urlpatterns = patterns('',
  • url(r'^user/(?P,<user_pk>\d+)/things/$', views.ThingListView.as_view(), name='user-thing-list'),
  • )

So in my templates I can do something like:

  • <link href="{% url 'user-thing-list' request.user.id %}">{{ request.user.first_name }}</link>

Which would end up looking like

  • <link href="/user/1/things/">Zeno</link>

So at any time I decide that I want to change the URL routing scheme to /people/1/stuff-list/, I'm not looking at a tedious task of editing a billion templates...

Now, all my angular logic and its templates are static assets served up by nginx instead of going through the request cycle of Django.

If I have some angular routes, I was hoping to give them handy names so I could 'reverse' them in my angular templates.

Reply to this Comment

I couldn't find examples of passing multiple arguments using the when() routing statement so figured out through trial and error that you can pass multiple arguments using the following format:

.when('/friends/:friendID/:friendLocation', { template: " ", controller: routes.showFriends}).

In my controller it comes as part of the $routeParams object:

show_showFriends: function($scope, $routeParams, $http){

alert($routeParams.friendID + ' lives in ' + $routeParams.friendLocation);

}

Reply to this Comment

Hi,

I have same king of structure for my App.Problem is "/contact/:username" , in that case when is use this scenario and refresh the page it remove user name and redirect me back to some other page.

What i want is when i use "/contact/kashif" as URL and refresh, i want the same URL. In my case it remove "kashif"

Reply to this Comment

Excellent post. This concept is key to getting history (back) button working with SPA's (single page applications).

Reply to this Comment

Hello,

Interesting approach! I recently encountered a similar whereby I needed more control over paths than angular-ui/router gave me.

In my case, I created a Path service which listened on $location for changes to the path and fired it to anything that was interested (as well as allowed new paths to be set). This gave individual controllers exact control over how to react to changes in the path, including exposing bits of it on $scope and updating views with an hg-include.

Knowing now that $router supports custom properties and not just templateUrl mappings, I will probably investigate further, as it may well help break the path information down a little better before handing it out, given $routeParams etc.

All in all, I love that Angular lets me work how I want; It was one of the things I found frustrating when I tried Ember.

James

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.