03 May 2013

comments

Managing state with AngularJS's ui-router (https://github.com/angular-ui/ui-router) is down right elegant. How is it different from a traditional router you might ask? Well I'll tell you, but first...

A little history about routers

Routers provide an abstraction between a url and a request that an application knows how to satisfy. With traditional web service applications, this is straight forward:

  • A url is routed to a request.
  • A request is used to look up some driver code that knows how to build a response.
When a service is rendering a page or retrieving data, this separation of responsibilities makes sense.

But what happens when the concept of a "page" becomes more ambiguous? What happens when only parts of a page need to be updated, or the end user is given the option to perform their own curation of a page? What happens when modularity is needed in a request?

If page composition is fluid, but we still need urls to discretely (for the most part) represent the various permutations of content, how do we effectively route requests? No matter how much we decouple our page components, we're still left with convoluted logic evaluating how to organize them into a specific page.

Sure something like a router is necessary to translate between a url and a request, but how can you include modularity in a request when you are declaratively mapping it to a url?

AngularJS State Manager

You Don't!

The most interesting thing about AngularJS's new router, isn't the router itself, but the state manager that comes with it. Instead of targeting a controller/view to render for a given url, you target a state. States are managed in a heirarchy providing inheritance of parent states and complex composition of page components, all the while remaining declarative in nature.

Without further ado, start fiddling: http://jsfiddle.net/benschwartz/LhydD/

or digging into angular's own docs: https://github.com/angular-ui/ui-router/wiki

... or stick around for an explanation.

 What can State do for me?

Lets start with a simple example:

index.html

1 <body ng-app="myApp">
2     <div ui-view></div>
3     <script src="app.js"></script>
4 </body>

app.js

 1 angular.module('myApp', ['ui.state'])
 2     .config(['$stateProvider', function ($stateProvider) {
 3 
 4         var home = {
 5             name: 'home',
 6             url: '/',
 7             template: 'Hello {{name}}',
 8             controller: ['$scope', function ($scope) {
 9                 $scope.name = "World";
10             }]
11         };
12 
13         $stateProvider.state(home);
14     }])

Not much is going on here, but basically this is how you'd implement a conventional route with the state manager.

Lets build a simplified version of the settings section from the  JSFiddle example.

Settings

settings-comp2

So how do we build it? Lets start by decomposing our pages (edit details and edit quotes) into states. To represent these two pages, we need three states: an abstract base state (settings) and two concrete child states (details and quotes.)

 1 var settings = {
 2     name: 'settings',
 3     abstract: true,
 4     url: '/settings'
 5 };
 6 
 7 var details = {
 8     name: 'settings.details',
 9     parent: settings,
10     url: ''
11 };
12 
13 var quotes = {
14     name: 'settings.quotes',
15     parent: settings,
16     url: '/quotes'
17 };

Though incomplete, this is the gist of how you define states with ui-router. Right off the bat, you can see that urls are built through state inheritance: to edit quotes, we'll navigate to "settings/quotes" since the quotes state declares settings as its parent and thus inherits its url among other things.

Next lets fill in the gaps and wire this up into an Angular module.

accountSettings Module

app.js

 1 angular.module('accountSettings', ['ui.state'])
 2     .config(['$stateProvider', function ($stateProvider) {
 3 
 4         var settings = {
 5             name: 'settings',
 6             url: '/settings',
 7             abstract: true,
 8             templateUrl: 'settings.html', 
 9             controller: 'SettingsController'
10         };
11 
12         var details = {
13             name: 'settings.details',
14             parent: settings,
15             url: '',
16             templateUrl: 'settings.details.html'
17         };
18 
19         var quotes = {
20             name: 'settings.quotes',
21             parent: settings,
22             url: '/quotes',
23             templateUrl: 'settings.quotes.html'
24         };
25 
26         $stateProvider
27             .state(settings)
28             .state(details)
29             .state(quotes);
30 
31     }])
32     .controller('SettingsController', ['$scope', function ($scope) {
33         $scope.user = {
34             name: "Bob Loblaw",
35             email: "bobloblaw@lawblog.com",
36             password: 'semi-secret',
37             quotes: "Lorem ipsum dolor sic amet"
38         };
39     }])

index.html

1 <body ng-app="accountSettings">
2     <div class="container" ui-view></div>
3     <script src="app.js"></script>
4 </body>

settings.html

 1 <div class="row">
 2   <div class="span3">
 3     <div class="pa-sidebar well well-small">
 4       <ul class="nav nav-list">
 5         <li class="nav-header">Settings</li>
 6         <li ng-class="{ active: $state.includes(\'settings.user.default\')}"><a href="#/settings" >User Details</a></li>
 7         <li ng-class="{ active: $state.includes(\'settings.quotes\')}"><a href="#/settings/quotes" >User Quotes</a></li>
 8       </ul>
 9       <hr>
10     </div>
11   </div>
12   <div class="span9" ui-view></div>
13 </div>

settings.details.html

1 <h3>{{user.name}}\'s Details</h3>
2 <hr>
3 <div><label>Name</label><input type="text" ng-model="user.name" /></div>
4 <div><label>Email</label><input type="text" ng-model="user.email" /></div>
5 
6 <button class="btn" ng-click="done()">Save</button>

settings.quotes.html

1 <h3>{{user.name}}\'s Quotes</h3>
2 <hr>
3 <div><label>Quotes</label><textarea type="text" ng-model="user.quotes"></textarea></div>
4 
5 <button class="btn" ng-click="done()">Save</button>

We're Done!

Wait, what happened?

Ok ok, we can take a look at the code before we call it done.

The first thing you might notice, is that settings has a url but details doesn't. Since it doesn't make sense to go to the settings state (if for no other reason then that it's abstract) we have assigned the concrete state "details" to have a url of '' so that traffic to '/settings' will route to it. We could have done this the other way around, but as we add new pages to our accountSettings module (and children to our settings state) it makes sense for the parent state to declare the url namespace.

The other interesting thing (and this is where we really start realizing the power of states) is that there is only one controller and it's only wired up against the settings state! How are the 'details' and 'quotes' states getting populated with data only accessible by 'settings' state you ask? Controller inheritance! Sorry if I'm getting excited here, but think about what we just did: without any convoluted registry/cache scheme, we were able to remove the one-to-one relationship that is typical between server requests and building a page. All of a sudden, we can start thinking about what data we need for a feature and how to present that data to the user as not intrinsically linked!

Scope Creep!

No, not $scope creep. Scope creep like what Marketing throws at you after you're 90% done with their initial request.

Now we're being asked to create a status bar in the settings section that will always display a description of what the user is supposed to be doing. (Don't ask me why marketing would want this... but it does help to illustrate another feature of ui-router)

settings-comp3

Multiple (named) Views

We've already seen multiple views per page in the form of hierarchical states, each with their own view, but what about a single state that needs to display multiple views? Our new requirement, displaying a description of what the user is supposed to be doing, relies on just that.

So maybe you've guessed by now that ui-router supports declaring multiple named views per state. If so, you're right; and here's an update to our hello world example illustrating how:

index.html

1 <body ng-app="myApp">
2     <div ui-view></div>
3     static nonsense
4     <div ui-view="foo"></div>
5     <script src="app.js"></script>
6 </body>

app.js

 1 angular.module('myApp', ['ui.state'])
 2     .config(['$stateProvider', function ($stateProvider) {
 3 
 4         var home = {
 5             name: 'home',
 6             url: '/',
 7             views: {
 8                 '': {
 9                     template: 'Hello {{name}}',
10                     controller: ['$scope', function ($scope) {
11                         $scope.name = "World";
12                     }]
13                 },
14                 'foo': {
15                     template: 'bar'
16                 }
17         };
18 
19         $stateProvider.state(home);
20     }])

There you have it. One state updating two views. The default view (represented by '') and the 'foo' view.

accountSettings Module Updates

Since this is such an easy feature to add on, we'll let marketing slide it in (even though we were already done.) All we need to do is add a named view to the settings.html partial and convert our 'details' and 'quotes' states to update both views.

settings.html

 1 <div class="alert" ui-view="hint"></div>
 2 <div class="row">
 3   <div class="span3">
 4     <div class="pa-sidebar well well-small">
 5       <ul class="nav nav-list">
 6         <li class="nav-header">Settings</li>
 7         <li ng-class="{ active: $state.includes(\'settings.user.default\')}"><a href="#/settings" >User Details</a></li>
 8         <li ng-class="{ active: $state.includes(\'settings.quotes\')}"><a href="#/settings/quotes" >User Quotes</a></li>
 9       </ul>
10       <hr>
11     </div>
12   </div>
13   <div class="span9" ui-view></div>
14 </div>

state definitions from app.js

 1 var details = {
 2             name: 'settings.details',
 3             parent: settings,
 4             url: '',
 5             views: {
 6                 '': {
 7                     templateUrl: 'settings.details.html'
 8                 },
 9                 'hint': {
10                     template: 'edit your details!'
11                 }
12         };
13 
14         var quotes = {
15             name: 'settings.quotes',
16             parent: settings,
17             url: '/quotes',
18             views: {
19                 '': {
20                     templateUrl: 'settings.quotes.html'
21                 },
22                 'hint': {
23                     template: 'edit your quotes!'
24                 }
25         };

Conclusion

In a perfect world, the GOF would come along and explain in simple terms the "right" way to route urls to requests in single page Javascript applications, and this way would soon become ubiquitous.

In lieu of this, we'll just have to keep trying things out and seeing what works. Maybe one day we'll be back to that comfort zone which Routing provided for web service application architecture.

Since I only scratched the surface, go check out ui-router on GitHub. There's a more complete example there as well as some documentation on the wiki.



comments powered by Disqus