Routing & controllers

For the vast majority of cases, we want users to navigate through the app, and for the user to be able to go back to the screen they were using the browser history (back/forward buttons). These screens are in reality states of the app: “new book form visible”, “viewing a list of books I own”, “browsing a book”, and so on, are examples of possible states that the UI is in.

As to what screen changes warrant a state, it depends on how granular you want the user experience to be. For instance, you may decide to show the new book form in place, perhaps right above my list of books, and not have a dedicated URL (or a page) for creating new books. Or maybe there’s a large volume of information to be filled in it and you’d like to avoid cluttering the screen, so it makes more sense for that form to live in a screen of its own.

The point is that while common sense (and known patterns) will provide great guidelines when it comes to deciding that, without taking UX into consideration, there are no absolutes. Backbone, like Ember or Angular, will give you a tool for creating screen states with URLs. Choosing what's appropriate is up to you and what you're building.

Hence if and where you choose to have state in your app, you’ll need to use the router for constructing the UI. The low-level logic for doing all that won't live in the router itself of course. Instead, it'll fetch data using models/collections, and it’ll instance and render views to display the data. Put simply: it's with the router that you'll “assemble” your application where state applies.

★ ★ ★

Backbone.Router fires a function when a certain URL pattern is visited. And since functions are objects which can be passed around as a variable, this means you can declare it elsewhere and assign it to a URL pattern through the routes object, avoiding declaring the actual URL logic in the router file.

This has the added bonus of making testing a lot easier, perhaps foregoing most of the need for testing the router itself in favour of simply testing these functions.

Assume you have the following router declared:

# Application router
# ==================

class Router extends Backbone.Router
  routes:
    'books':     'allBooksScreen'
    'books/:id': 'bookScreen'

  allBooksScreen: ->
    # Fetch all books and render a list with them.

  bookScreen: (id) ->
    # Fetch book with `id` and render it.
// Application router
// ==================

var Router = Backbone.Router.extend({
  routes: {
    'books':     'allBooksScreen',
    'books/:id': 'bookScreen'
  },

  allBooksScreen: function() {
    // Fetch all books and render a list with them.
  },

  bookScreen: function(id) {
    // Fetch book with `id` and render it.
  }
});

As each method that handles a route gets more complex, the router will end up very complex very quickly, and it alone will be the container of way too much application logic. The solution some adopt is using multiple routers, one for each topic part of the application, but while that addresses the problem of having a long router file, it fails to tackle the problem of separation of concerns: the router is doing it's job so long as it dispatches a function when a URL is visited. What that function does, however, can live outside of it.

In MVC frameworks such as Rails, the router matches HTTP requests to a function in a controller class, which is called to handle the request. And what the example above has done is in a way just that: the method bookScreen above is a controller, except that it’s declared directly in the router.

But we could and should be declaring them outside. Like so:

# View book screen controller
# ===========================
#
# Declared in a separate file, such as
# javascripts/controllers/view-book-controller.coffee.

viewBookController = (id) ->
  # Check a collection to see if book with id `id` exists,
  # and if not instance a new one and fetch it.

# Export viewBookController.
// View book screen controller
// ===========================
//
// Declared in a separate file, such as
// javascripts/controllers/view-book-controller.coffee.

var viewBookController = function(id) {
  // Check a collection to see if book with id `id` exists,
  // and if not instance a new one and fetch it.
}

// Export viewBookController.

And then in the router:

# Application router
# ==================

# Alternatively, made available in the `window` scope. With browserify,
# load it like so:
viewBookController = require 'view-book-controller'

class Router extends Backbone.Router
  routes:
    'books/:id': viewBookController

  # ...
// Application router
// ==================

// Alternatively, made available in the `window` scope. With browserify,
// load it like so:
var viewBookController = require('view-book-controller');

var Router = Backbone.Router.extend({
  routes: {
    'books/:id': viewBookController
  },

  // ...
});

This pattern has the huge benefit of making it possible to test what happens when a user goes to a certain URL completely outside of browser testing. All you need to do is load the controller module, and call it passing whatever parameters it expects (in the example above, you'd pass the id of a mocked instance of Book, for example).

Another benefit is as a controller grows complex, it can have its own dependencies and module-specific methods which the router doesn't need to know about. So you're scaling controllers separately, as you should.

★ ★ ★

On scaling and refactoring controllers, earlier in the Models, collections & data chapter I described a repetitive data loading pattern. Mind the refactoring steps:

# View books screen controller
# ============================
#
# This uses a global `authors` collection which the controller
# either needs to require or is available in `window`.

# Container where the view will be rendered.
$container = $ '.some-container'

viewBooksController = (authorId) ->
  author = authors.get authorId
  if author?
    # Author exists in the collection. Keep moving.
    view = new BookList collection: author.books
    $container.empty().append view.render().el
    author.books.fetch()
  else
    # Author isn't in the collection. Fetch it
    # before rendering a collection.
    author = new Author id: authorId
    author.fetch success: =>
      # Add the author to the global collection.
      authors.add author
      view = new BookList collection: author.books
      $container.empty().append view.render().el
      author.books.fetch()
// View books screen controller
// ============================
//
// This uses a global `authors` collection which the controller
// either needs to require or is available in `window`.

// Container where the view will be rendered.
var $container = $('.some-container');

var viewBooksController = function(authorId) {
  var author = authors.get(authorId);
  if (author) {
    // Author exists in the collection. Keep moving.
    var view = new BookList({collection: author.books});
    $container.empty().append(view.render().el);
    author.books.fetch()
  } else {
    // Author isn't in the collection. Fetch it
    // before rendering a collection.
    var author = new Author({id: authorId});
    author.fetch({
      success: function() {
        // Add the author to the global collection.
        authors.add(author);
        var view = new BookList({collection: author.books});
        $container.empty().append(view.render().el);
        author.books.fetch();
      }
    });
  }
}

Note how we're rendering the BookList view before even fetching . The idea is as soon as author.books.fetch() is called, the collection will trigger a request event which the instance of BookList can listenTo and display a loading indicator. When fetch completes, it'll fire sync, and we can then re-render this view with actual books in it.

But this is still awfully repetitive:

# View books screen controller
# ============================
#
# This uses a global `authors` collection which the controller
# either needs to require or is available in `window`.

# Container where the view will be rendered.
$container = $ '.some-container'

# Renders the appropriate view using a books collection,
# and then calls fetch() on it.
renderAndFetchBooks = (collection, $container) ->
  view = new BookList collection: collection
  $container.empty().append view.render().el
  author.books.fetch()

# The controller itself. This is what gets exported.
viewBooksController = (authorId) ->
  author = authors.get authorId
  if author?
    renderAndFetchBooks author.books, $container
  else
    # Author isn't in the collection. Fetch it
    # before rendering a collection.
    author = new Author id: authorId
    author.fetch success: =>
      # Add the author to the global collection.
      authors.add author
      renderAndFetchBooks author.books, $container
// View books screen controller
// ============================
//
// This uses a global `authors` collection which the controller
// either needs to require or is available in `window`.

// Container where the view will be rendered.
var $container = $('.some-container');

// Renders the appropriate view using a books collection,
// and then calls fetch() on it.
var renderAndFetchBooks = function(collection, $container) {
  var view = new BookList({collection: collection});
  $container.empty().append(view.render().el);
  author.books.fetch();
}

// The controller itself. This is what gets exported.
var viewBooksController = function(authorId) {
  var author = authors.get(authorId);
  if (author) {
    renderAndFetchBooks(author.books, $container);
  } else {
    // Author isn't in the collection. Fetch it
    // before rendering a collection.
    author = new Author({id: authorId});
    author.fetch({
      success: function() {
        // Add the author to the global collection.
        authors.add(author);
        renderAndFetchBooks(author.books, $container);
      }
    });
  }
}

The latter is much better. The controller code itself is now reduced to pretty much the high level logic. But we'll need to handle our app getting a 404 back when requesting an author. Adding this last bit should be trivial:

# …

renderNotFound = (author) ->
  view = new AuthorNotFound model: author
  $container.empty().append view.render().el

# And where we call author.fetch()…
author.fetch
  success: =>
    authors.add author
    renderAndFetchBooks author.books, $container
  error: (model, xhr) =>
    if xhr.status is 404
      renderNotFound author
// …

var renderNotFound = function(author) {
  var view = new AuthorNotFound({model: author});
  $container.empty().append(view.render().el);
}

// And where we call author.fetch()…
author.fetch({
  success: function() {
    authors.add(author);
    renderAndFetchBooks(author.books, $container);
  },
  error: function(model, xhr) {
    if (xhr.status === 404) {
      renderNotFound(author);
    }
  }
});

Ideally no single module of the codebase should grow too large or too complex, but the gist here is doing this stuff away from the router is a huge win. Follow this pattern and you'll no longer be looking at overly complex routers.

★ ★ ★

You may have a mix of public and private content in your application. Which means you'll need to “protect” certain screens from users who aren't logged in. I don't mean to imply your security model should rely on the app not rendering a form that lets users access data they otherwise shouldn't be able to see. The server should enforce consistency and your API should respond with an appropriate status if I request private information without authentication.

But we need to handle that gracefully, and possibly present the user with a login screen or at least some notice saying the screen/data is unavailable, and the right place to put that kind of logic is in the router.

A simple user authentication pattern works by declaring a method named requireLogin. We then nest in a callback argument anything that's meant to happen on a successful login:

class Router extends Backbone.Router
  routes:
    'books/:id': viewBookController

  requireLogin: (callback) ->
    if user.isLoggedIn()
      callback()
    else
      alert "Login required! Please sign in first."
      @navigate '/login', trigger: yes
var Router = Backbone.Router.extend({
  routes: {
    'books/:id': viewBookController
  },

  requireLogin: function(callback) {
    if (user.isLoggedIn()) {
      callback();
    } else {
      alert("Login required! Please sign in first.");
      this.navigate('/login', { trigger: yes });
    }
  }
});

And in viewBookController:

viewBookController = (id) ->
  @requireLogin ->
    # Build the UI here, which will happen only
    # if `user.isLoggedIn()` is true.
var viewBookController = function(id) {
  this.requireLogin(function() {
    // Build the UI here, which will happen only
    // if `user.isLoggedIn()` is true.
  });
}

Because viewBookController is invoked from the context of the Router instance, any methods declared in the router will be available via the @ (or this) notation.

The user object you're seeing in the router would be a user model the router has access to either via a global or a module it required, representing the current user browsing the app.

A note: whenever it seems easier to bring a model into the router by creating concepts like currentUser, or currentBook for the book you're browsing, think twice. This means you're making that type of model a first class entity in your application. And while we could debate until we're blue in the face why a user deserves the spot and not a book when the app is a bookstore app, the fact is in the vast majority of software you write (including this example), you'll always have a user operating the application, regardless of said user being logged in or not.

★ ★ ★

Earlier in the Models, collections & data chapter I briefly explained a strategy for data fetching where reusing existing collection data when a route is visited is a good idea.

And as I explained earlier in this chapter, part of constructing the UI (the router's role) consists of fetching data which is then passed to a view for displaying. Creating a fast app has a lot to do with how smartly it does this.

Staying within the example thus far, and to keep things simple let's consider a personal catalogue of books. At any given time I'll be dealing with one collection of books: the ones I own. I could instance a collection of books elsewhere in the app to handle that, possibly within the user object, and keep this data around for as long as the session goes on.

When logging in I get a list of books in my catalogue:

# View my books controller
# ========================

# Presume a user object instanced elsewhere is passed
# to this controller.
user = require 'current-user'

viewMyBooksController = ->

  view = new ViewMyBooks collection: user.books
  # Render the view. For a millisecond it won't
  # have any books to render, which is fine.

  # If there are no books my catalogue, try the
  # server.
  if user.books.isEmpty()

    # Calling fetch here will trigger a request event,
    # which ViewMyBooks should watch for and display
    # feedback along the lines of saying
    # “Loading your books…”.
    user.books.fetch()

  else

    # Probably visited this screen before and books
    # were loaded, so trigger an event which the view
    # is watching to tell it to just render the books.
    user.books.trigger 'reset'
// View my books controller
// ========================

// Presume a user object instanced elsewhere is passed
// to this controller.
var user = require('current-user');

var viewMyBooksController = function() {
  var view = new ViewMyBooks({collection: user.books});
  // Render the view. For a millisecond it won't
  // have any books to render, which is fine.

  // If there are no books my catalogue, try the
  // server.
  if (user.books.isEmpty()) {
    // Calling fetch here will trigger a request event,
    // which ViewMyBooks should watch for and display
    // feedback along the lines of saying
    // “Loading your books…”.
    user.books.fetch()
  } else {
    // Probably visited this screen before and books
    // were loaded, so trigger an event which the view
    // is watching to tell it to just render the books.
    user.books.trigger 'reset'
  }
}

With that, when navigating away from this screen and back to it, it'll seem as if my catalogue of books just rendered instantly.

But for instance, if this screen loads only the 20 newest books introduced to my catalogue, and we arrive to a book not among these 20 maybe through a link elsewhere in the screen, then we load it from the server and add it to the collection. Here's the overly commented out flow:

# View single book controller
# ===========================

user = require 'current-user'

viewSingleBookController = (bookId) ->

  if model = user.books.get bookId

    # We fetched this model before, so give it to the
    # view and display it.
    view = new ViewSingleBook model: model
    # Render the view…

  else

    # The model doesn't exist in the collection. Instance
    # a new object to receive it when we fetch it, and hand
    # it “hollow” to the view, which will know what to display
    # when a model has no attributes yet.
    model = new Book id: bookId
    view = new ViewSingleBook model: model
    # Render the view…

    # Now fetch the book. This will trigger a “request”
    # event, which we can use to give the user feedback on
    # the fact it's being fetched.
    model.fetch success: =>

      # Fetched successfully, add to the collection.
      user.books.add model

      # No further action required here as when the model
      # was fetched, it fired a “sync” event, which forced
      # the view to re-render, thus displaying the model
      # with all it's attributes.
// View single book controller
// ===========================

var user = require('current-user');

var viewSingleBookController = function(bookId) {
  var model = user.books.get(bookId);
  if (model) {
    // We fetched this model before, so give it to the
    // view and display it.
    var view = new ViewSingleBook model: model
    // Render the view…
  } else {
    // The model doesn't exist in the collection. Instance
    // a new object to receive it when we fetch it, and hand
    // it “hollow” to the view, which will know what to display
    // when a model has no attributes yet.
    var model = new Book({id: bookId});
    var view = new ViewSingleBook({model: model});
    // Render the view…

    // Now fetch the book. This will trigger a “request”
    // event, which we can use to give the user feedback on
    // the fact it's being fetched.
    model.fetch({
      success: function() {
        // Fetched successfully, add to the collection.
        user.books.add(model);

        // No further action required here as when the model
        // was fetched, it fired a “sync” event, which forced
        // the view to re-render, thus displaying the model
        // with all it's attributes.
      }
    });
  }
}

In essence, what this controller does is if it finds that a book is already in the user's catalogue collection, it hands it to the appropriate view and gets it to render it, otherwise it fetches it first, adds to the catalogue, and then renders it. So now if I navigate away from this book's page and back to it, it'll also render it instantly.

There are a few caveats to this approach: there's a clear buildup of data happening on the client, but before you think the app will explode with a mere thousand entries even, things aren't as grim as that. Still, you may want to expire and remove data from the collection from time to time, and/or under certain circumstances such as the server sending a specific header saying the collection has changed. This should be handled by the collection object itself though, and not by the router. Be extra mindful about this if the app is designed for mobiles, since unlike most desktop apps where people will close a tab and come back to it later, they'll probaly stay running in the background, so the data buildup won't go away.

Last, keeping things in memory is a form of caching. You may be displaying stale data for a book, so the process will have to be smarter than merely checking whether a book exists in memory. One very simple yet effective approach is to do the following:

# Skipping to the part where we grab the book …
if model = user.books.get bookId

  # We fetched this model before, so give it to the
  # view and display it.
  view = new ViewSingleBook model: model
  # Render the view…

  # Call fetch to ensure we have the latest data. When
  # fetch completes, the UI will be re-rendered since
  # we're watching for “sync” events in it.
  model.fetch()
// Skipping to the part where we grab the book …
var model = user.books.get(bookId);
if (model) {
  // We fetched this model before, so give it to the
  // view and display it.
  var view = new ViewSingleBook model: model
  // Render the view…

  // Call fetch to ensure we have the latest data. When
  // fetch completes, the UI will be re-rendered since
  // we're watching for “sync” events in it.
  model.fetch();
}

This re-rendering of the view should then display the updated data, and of course, for the sake of good UX a near-instant update is better than a slow one, but you can mitigate that by displaying a progress indicator somewhere so the user knows it's looking for data in the server. In the meantime, the user is given something to work with, which in the likelihood of it not being stale data, it'll be all that they need.

C J