Mapping AngularJS Routes Onto URL Parameters And Client-Side Events

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 AngularJSas 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:
123456
$route.when(
"/friends/:friendID",
{
templateUrl: "views/friend-detail.htm"
}
);
view rawmisc_angular_1.js hosted with ❤ by GitHub
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:
123456
$route.when(
"/friends/:friendID",
{
event: "friends.view"
}
);
view rawmisc_angular_2.js hosted with ❤ by GitHub
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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
<!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>
view rawindex.htm hosted with ❤ by GitHub
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:
1234567
<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>
view rawmisc_angular_3.htm hosted with ❤ by GitHub
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.

1 comment: