14 Nov 2014

comments

I got great reception from my original post about managing state with AngularJS’s ui-router (ui-router on github,) but some feedback as well. Primarily, there was an ask for a full example (rather than the JSFiddle which seems to break a couple of times a week.) Since the library has matured and if I’m going to create a repo version of this then I’m going to start with yeoman, I decided to write a new post with current versions and an application base generated by yeoman.

If you just can’t wait for the example repo, here it is.

State Management vs. Routers

What I wrote before is largely still applicable, so take a look at my original take for a full description.

The gist of the argument, is that routes make sense for discrete endpoints, and states make sense for application composition. If every page was essentially unique, then routes would be sufficient, but in a modern single page web app there are fewer concrete pages and more updates to the application’s screen composition.

Managing your application with states facilitates this way of thinking.

A Demo is worth a thousand words

The example application I’m going to walk through is hosted on github.io (this blog) and the source is likewise available on github.

Run it at home

You can get it up and running by cloning a copy and serving it up with grunt:

git clone https://github.com/benschw/ui-router-demo.git
cd ui-router-demo
bower install
npm install
grunt serve

If you don’t have any of the dependencies (node, npm, bower, grunt) there are install instructions in the repo README.

grunt serve will pop up a node webserver running on localhost:9000, so you can head there now to check out the application (actually, grunt probably opened a browser for you.) If you aren’t following along at home, you can just take a look at the hosted version and compare against the source code.

What am I looking at?

Essentially this is a fake webapp with a home page and a settings section. The home page is pretty straight forward and would have made sense to be implemented with routes, but the settings section illustrates the composition problem I outlined above.

setting details

This isn’t a “details page.” It’s a web app with a top nav and footer, containing a settings section with “details” selected as the primary focus.

With ui-router I can model this composition as states and if I want to switch to “quotes,” I only need to specify how to re-render that small portion of the page.

How do I Do That?

The root application is setting up the header, footer, and a container. It does this by registering a root “app” state which supplies a template for the header and footer, and leaves the main container empty.

The home module is just a single state that fills the container with the contents of a container.

Settings Section

This is the meat of our demo, so I’ll dig a little deeper.

The settings module sets up an abstract settings state which adds the /settings namespace to the url, and defines a SettingsController which will set user data on the current $scope. This state also sets up the settings section nav and defines where the primary-focus content will go (its default view.)

app.settings

'use strict';

angular
  .module('app.settings', [
    'ngResource',
    'ngRoute',
    'ui.router'
  ])
  .config(['$stateProvider', function($stateProvider) {

      var settings = {
          name: 'app.settings',
          abstract: true,
          url: '/settings',
          views: {
            '@': {
              templateUrl: 'scripts/settings/settings.html',
            }
          }
      };

      $stateProvider.state(settings);

  }])
  .controller('SettingsController', ['$scope', '$resource', function ($scope, $resource) {
    var User = $resource('stub-user.json');
    var user = User.get();
    $scope.user = user;
  }]);

settings.html

<div class="row">
  <div class="col-sm-3">
    <div class="pa-sidebar well well-small">
      <ul class="nav nav-list">
        <li class="nav-header">Settings</li>
        <li ng-class="{ active: $state.includes('app.settings.details')}"><a ui-sref="app.settings.details" >User Details</a></li>
        <li ng-class="{ active: $state.includes('app.settings.quotes')}"><a ui-sref="app.settings.quotes" >User Quotes</a></li>
      </ul>
      <hr>
    </div>
  </div>
  <div class="col-sm-9" ui-view></div>
</div>

There are two submodules here too: details and quotes. For such a trivial example, these could have been part of the settings module (but still constitute unique states), but I wanted to abstract them out to show how you could organize a more complex settings section.

These states piggyback on the app.settings state to supply content for the abstract settings’ default ui-view. They also leverage the SettingsController defined in the settings module (since both states show a different view of the same user data resource.)

app.settings.details

'use strict';

angular
  .module('app.settings.details', [
    'ui.router',
    "app.settings"
  ])
  .config(['$stateProvider', function($stateProvider) {

      var details = {
          name: 'app.settings.details',
          url: '/',
          views: {
            '': {
              templateUrl: 'scripts/settings/details/details.html',
              controller: 'SettingsController'
            }
          }
      };

      $stateProvider.state(details);

  }]);

details.html

<h3>{{user.name}}'s Details</h3>
<hr>
<div class="form-group"><label>Name</label><input class="form-control" type="text" ng-model="user.name" /></div>
<div class="form-group"><label>Email</label><input class="form-control" type="text" ng-model="user.email" /></div>

<button class="btn btn-default" ng-click="done()">Save</button>

app.settings.quotes

'use strict';

angular
  .module('app.settings.quotes', [
    'ngResource',
    'ngRoute',
    'ui.router',
    "app.settings"
  ])
  .config(['$stateProvider', function($stateProvider) {

      var quotes = {
          name: 'app.settings.quotes',
          url: '/quotes',
          views: {
            '': {
              templateUrl: 'scripts/settings/quotes/quotes.html',
              controller: 'SettingsController'
            }
          }
      };

      $stateProvider.state(quotes);

  }]);

quotes.html

<h3>{{user.name}}'s Quotes</h3>
<hr>

<div class="form-group">
  <label>Quotes</label>
  <textarea class="form-control" type="text" ng-model="user.quotes"></textarea>
</div>
listlist
<button class="btn btn-default" ng-click="done()">Save</button>

Conclusion

And there you have it!

ui-router allows you to implement your app more naturally: as a composition of views. No longer do you need to recast how your app is logically structured into a hierarchy or set of unique pages with shared content.



comments powered by Disqus