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.Modelor 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.Eventsor 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
- 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.
- 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
- Found a bug that makes
renderrun 2n times? That's probably because you bind to a model in
renderin 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
- User clicks on an image
- An instance of
ImageVieweats the click (nom), fires an
image:selectevent, passing the image model along with it.
- An instance of
image:selectevent on the view, calls
session.select(image)passing in the image it got from the
sessionwhich is a model object representing the state of the workspace, fires a
selection:changedevent with the image as an argument.
- Our original instance of
ImageViewwhich was listening to
selection:changedevent, 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
.selectedclass 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:
- 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.
- Initializing model objects we need with that data.
- 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
//pageData contains our model info app = new MyApp(pageData); app.initialize();
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
= requireto declare dependencies rather than just package up your assets. That means if you use a function from
underscore.jsin file, then require underscore at the top of that file.
- 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.
It's a bit dated but Martin Fowlers article on GUI architectures shows that they figured most of this stuff out in the nineties.