Field Notes on Client-Side Javascript

By Najaf Ali

A random bunch of ideas for developing complex javascript UIs on the client, using libraries like Backbone.

The Golden Rule:

1. All State Shall Live in the Model

  • This includes anything that you might think at first belongs in a view. Some examples:
    • If you're looking at one page out of a book, the 'currentPage' belongs in the model.
    • If your UI allows you to make a selection, that belongs in the model.
    • If your UI has different sections that a user moves between, the 'currentSection' belongs on the user, or the 'session' or somewhere in the model.
  • A 'model' doesn't need to be a subclass of Backbone.Model or anything like that to be deemed a part of the model. It does however need to be able to trigger events that views can listen to, and Backbone.Events or similar helps here.
  • Add persistence to the server last, it's entirely orthogonal to the client-side UI.
  • N.B. You'd be surprised at what state you end up persisting when you start pulling state out of your views and into your model.

Why? Because by putting it in the model, all of this state is something that can be listened to by one or more views.

This rule was taught to me by a certain Mark Evans at New Bamboo. There is deep wisdom in this rule. Abide by it, and your UI code will magically begin to organize itself. Think fractal patterns, cherry blossoms and haiku. Let state creep in elsewhere, and watch as it turns into an unmaintainable mess.

2. Your views have precious few responsibilities

They are:

  • Render themselves in the DOM
  • Listen for changes on model objects and respond as appropriate
  • Emit events on user interactions (button click, drag, etc) with relevant models passed along as arguments.

And that's it.

They might:

  • Instead of emiting events, call methods on passed in models (but preferably not)
  • Initialize and contain any 'sub-views'

Other random bits of useful:

  • Views don't necessarily have to render elements into the DOM. If multiple views render in a mostly similar way, you could keep things DRY and compose a renderer object that does the heavy-lifting instead.
  • Use mustache templates, even if you think the markup is small enough not to warrant it. I don't believe in a deity, but if I did I'm quite sure he wouldn't look kindly on stitching together DOM elements with jQuery or Backbone.Views builtin make method.
  • Found a bug that makes render run 2n times? That's probably because you bind to a model in render in your view without unbinding on whatever you were listening to before. Unbinding symmetrically will probably clean that right up.

3. 'Handlers' listen to views or models and do stuff

  • A handler (or observer, take your pick) listens to both views and models and makes updates on models.
  • These updates usually cause models to emit events.
  • Since the views are listening on events, views update themselves in line with the new model state.

4. An Example Interaction

  1. User clicks on an image
  2. An instance of ImageView eats the click (nom), fires an image:select event, passing the image model along with it.
  3. An instance of SelectionHandler catches the image:select event on the view, calls passing in the image it got from the image:select event trigger.
  4. session which is a model object representing the state of the workspace, fires a selection:changed event with the image as an argument.
  5. Our original instance of ImageView which was listening to session gets the selection:changed event, figures out that it's displaying the same image as the one that just got selected, and responds by running a method that adds a .selected class to it's element.

5. Stitch all of these components together in 'Apps'

You normally write a bunch of initializiation code for your single page app. This code almost always ends up:

  1. Getting state from somewhere. This could be via an AJAX call to an endpoint or (I know, it's gross, but we all do it) loading it out of JSON rendered into the page and stuffed in a global.
  2. Initializing model objects we need with that data.
  3. Initializing your view, and injecting models into it.

What I tend to do is create an App object, who's job it is to do step 2 onwards. That way, your top-level javascript ends up being something along the lines of

<%= code 'top-level-js.js' %>

Apps could in theory compose and delegate to each other, stitched together in any way you see fit.

The first step is to have a place for all this messy initialization code, then you can start cutting it up into logically separate pieces.

Other random tidbits

  • If you haven't already, give CoffeeScript a try, I've found it's quite nice. It's cool if you don't like it though, I know a few rock-solid js devs who loathe it.
  • poirot is great little gem for making life with your mustache smooth.
  • Are you using the rails asset pipeline? If you are then save yourself some serious headache and use = require to declare dependencies rather than just package up your assets. That means if you use a function from underscore.js in file, then require underscore at the top of that file.

    If you do this religiously, sprockets will figure out the correct order it needs to package your javascript in and you won't get strange errors about objects/functions not existing where you expect them to.

  • No really, declare your dependencies from the beginning, it's a real pain to do this after the fact.
  • Some alternatives to Backbone? Spine, jsmodel for your models and the up and coming egg.js.

Further Reading

It's a bit dated but Martin Fowlers article on GUI architectures shows that they figured most of this stuff out in the nineties.