Single Page Apps with Node and Angular

Single-page JavaScript web apps seem to be all the rage right now. I don’t think I’ve gone more than a day or two without seeing another blog debating the merits of Backbone, Knockout, Ember, Angular, and the myriad other client-side frameworks.

For me, it started with jQuery Mobile. This was my first taste of paging within a single DOM, requesting only data from the server. Unfortunately, jQuery Mobile isn’t something that is meant to be used on all devices from mobile to desktop.

This is where responsive design with media queries comes into play. In some ways responsive design is like coming up with several different fixed-width layouts, but it’s more sophisticated than that. Media queries target width-based “break-points” and add css which overrides the default styles. Clever use of these techniques can produce a website which looks decent on everything from mobile sized screens to large desktops.

So where have I ended up? Here’s a top-down look at the way my single page app works.

Server Side

The server, based off the angular-express-seed, is Node and Express, and has only two essential functions. The first is to serve static files. A directory public contains all css, images, and javascripts. Another views directory contains Jade templates. The angular-express-seed project is not designed specifically with single page apps in mind, so by default the views folder contains a subfolder for partials that contain any widgets or small pages you might need, but I really only rely on the index file containing everything.

So beyond static files, the only other thing the server does is provide a json api for all the core data operations of the app. The main file for the app lists all possible routes. I list out all my api URIs first, and then lastly default to serving the index html.

app.js

var express = require('express'),
    routes = require('./routes'),
    api = require('./routes/api');

var app = module.exports = express.createServer();

app.configure(function() {
    app.set('views', __dirname + '/views');
    app.set('view engine', 'jade');
    app.set('view options', {
        layout: false,
        pretty: true
    });
    app.use(express.bodyParser());
    app.use(express.methodOverride());
    app.use(express.static(__dirname + '/public'));
    app.use(app.router);
});

//RESTful Routes
app.get('/api/posts', api.posts);
app.get('/api/post/:post_id', api.post);
app.post('/api/posts', api.postAdd);
app.put('/api/post/:post_id', api.postEdit);
app.delete('/api/post/:post_id', api.postDelete);

app.get('*', routes.index);

app.listen(3000, function() {
    console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});

Client Side

The client is all about Angular.js. It starts with an app module definition and some predefined routes. This took a minute to wrap my brain around, since we’re basically routing on the client and the server. But it really makes a lot of sense. The client and api routes follow similar patterns, but they must be different. This is why the api routes all start with the prefix “/api” (I will explain this a little bit more later).

public/js/app.js

var MyApp = angular.module('MyApp', []).
  config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
    $routeProvider.
      when('/', {
        templateUrl: 'postList.html', 
        controller: "PostListCtrl"
      }).
      when('/post/:id', {
        templateUrl: 'postShow.html', 
        controller: "PostShowCtrl"
      }).
      when('/posts/add', {
        templateUrl: 'postAdd.html', 
        controller: "PostAddCtrl"
      }).
      when('/post/:id/edit', {
        templateUrl: 'postEdit.html', 
        controller: "PostEditCtrl"
      });
    $locationProvider.html5Mode(true);
  }]);

Now, if you look at the templateUrls above, you might wonder how this can truly be a single page app. The answer lies in the fact that these do not refer to actual html files, but rather a special script template that Angular uses. This is the real magic.

views/index.jade (written as html for everyone’s benefit)

<header><!-- banner, nav bar, etc --></header>

<section ng-view=""></section>

<script id="postList.html" type="text/ng-template">
  <h2>Post List Page</h2>
  <div class="post-preview" ng-repeat="post in posts">
    <a class="post-title" href="/post/{{post.post_id}}">
      {{post.post_title}}
    </a>
    <span class="post-details">
      Authored by {{post.author}} on {{post.date}}
    </span>
  </div>
</script>

<script type="text/ng-template" id="postShow.html">
  <h2>{{post.post_title}}</h2>
  <div ng-bind-html-unsafe="post.post_body"></div>
</script>

<script id="postAdd.html" type="text/ng-template">
  <h2>Add New Post</h2>
  <form>
    <label>Title</label>
    <input type="text" name="post_title" ng-model="post_title"/>
    <label>Body</label>
    <textarea type="text" name="post_body" ng-model="post_body"></textarea>
    <button ng-click="submitPost()">Add Post</button>
  </form>
</script>
<script id="postEdit.html" type="text/ng-template">
  <!-- etc, etc -->
</script>

This technique is very much like <div data-role="page"> from jQuery Mobile. The last thing left to play with are the Angular controllers.

public/js/controllers.js

MyApp.controller('PostListCtrl', function PostListCtrl($scope, $http) {
  $http.get('/api/posts').
    success(function(data, status, headers, config){
      if(data.success){
        $scope.posts = data.posts;
      }
    });
});

MyApp.controller('PostShowCtrl', function PostShowCtrl($scope, $http, $routeParams, $location) {
  $http.get('/api/post/'+$routeParams.id).
    success(function(data, status, headers, config) {
      if(data.success){
        $scope.post = data.post;
      }else {
        //handle error/redirect to list
        $location.path('/');
      }
    });
});

MyApp.controller('PostAddCtrl', function PostAddCtrl($scope, $http, $location) {
    $scope.submitPost = function() {
      $http.post('/api/posts', {
          post_title: $scope.post_title,
          post_body: $scope.post_body
        }).success(function(data, status, headers, config) {
          if(data.success){
            $location.path('/');
          }else {
            //do something about the error
          }
        });
    };
  });

//all other controllers omitted for brevity

 App Flow Summary

Let’s walk through this from the user perspective. You click a link to myapp.com. The request matches the catch-all route ‘*’ and so the index page is returned. Like normal, the browser will follow up with any css/image/js resource requests which match the express.static() directive. But then the angular app fires up. It looks at the location.path(), matches it with ‘/’ and loads the PostList.html template into the ng-view, and then calls the controller. The PostListCtrl immediately makes an XHR for a list of posts, which it assigns to the $scope.posts variable. Thanks to Angular’s two-way binding, the page automatically generates all the DIVs to match each post.

Something interesting to note that should hopefully make all of this gel is what happens when you click the link for /post/6. If you’re already on the index/list page, no request is made to the server — the app just switches pages, triggers the new controller and the ajax request gets made and the page gets filled out. Now, what if you were started with this url? For example, if it was bookmarked or someone sent you a link directly to the post.

When you start by requesting something like /post/6, the request does go to the server, but it won’t match any routes because, as I said earlier, we set up our api to not overlap with the client routes. Instead, that wonderful catch-all route at the bottom serves back the index file. When Angular takes over, it matches location.path() against the client route for PostShow.html and shows that page first instead of the List page… and then goes and gets you a beer, because that’s just how awesome Angular is.

 Finally, Getting Responsive

I think one of the easiest ways to get started with responsive design is to use either Twitter Bootstrap or Zurb Foundation. Each of these provide a bunch of boilerplate css to get started, and can be extended with basic JavaScript widgets. The most essential feature, however, is a responsive grid system for defining the layout of your page. Media queries are already in place to shuffle the design around at the various breakpoints, which saves a lot of trouble.

References

For more information, the best starting point for something like this is the angular-express-seed and the angular-express-blog (based on seed). Also worth checking out is the recently released Yeoman project, which helps setup highly robust web app stacks of this vein. Yeoman has code generators to build skeleton apps based off of different client-side frameworks, such as Angular, Backbone, or Ember. Brian Ford just did a nice write-up of using Yeoman for Angular apps.

This entry was posted in Javascript and tagged , . Bookmark the permalink.

Comments are closed.