Overview
We've been finding ourselves building more and more JS heavy apps here at &yet. Until recently, we've pretty much invented a custom app architecture for each one.
Not surprisingly, we're finding ourselves solving similar problems repeatedly.
On the server side, we use django to give us an MVC structure to follow. But there's no obvious structure to your client-side code. There are some larger libraries that give you this, but usually have a ton of widgets etc. I'm talking about solutions like Sproutcore, YUI, or Google Closure and there are toolkits like GWT and Cappuccino that let you compile other code to JS.
But for those of us who want to lovingly craft the UI exactly how we want them in the JavaScript we know and love, and yet crave quick lightweight solutions, those toolkits feel like overkill.
Recently something called Backbone.js hit my "three tweet threshold" I decided to take a look. Turns out it's a winner in my book and I'll explain why and how we used it.
The Problem
There are definitely some challenges that come with building complex, single-page apps, not the least of which is managing an ever increasing amount of code running in the same page. Also, since JavaScript has no formal classes there is no self-evident approach for structuring an entire application.
As a result of these problems new JS devs trying to build these apps typically goes through a series of realizations that goes something like this:
- Get all excited about jQuery and attempt to use the DOM to store data and application state.
- Realizing the first approach gets really tricky once you have more than a few things to keep track of and so instead you attempt to store that state in some type of JS model.
- Realizing that binding model changes to the UI can get messy if you call functions directly from your model setters/getters. You know you have to react to model changes in the UI somehow but not really knowing where to do those DOM manipulations and ending up something that's looking more and more like spaghetti code.
- Building some type of app structure/framework to solve these problems.
- And... finally realizing that someone's already solved many of these problems for you (and open sourced their code).
The Goals
So, how do we want our app to behave? Here are the ideals as I see them.
- All state/models for your app should live in one place.
- Any change in that model should be automatically reflected in the UI, whether that's in one place or many.
- Clean/maintainable code structure.
- Writing as little "glue code" as possible.
Enter Backbone.js
Backbone doesn't attempt to give you widgets or application objects or even really give you views. It basically gives you a few key objects to help you structure your code. Namely, Models, Collections and Views. Ultimately what it provides is some basic tools that you can use to build a clean MVC app in the client. We get some useful base objects for those and an event architecture for handling changes. Let's take a look at each of those.
The Model object
The model object just gives you a way to set and retrieve arbitrary attributes. So, all you really need to create a fully functioning and useful model is the following:
var Movie = Backbone.Model.extend({});
Now you can instantiate, and set and get attributes all you want:
matrix = new Movie();
matrix.set({
title: "The Matrix",
format: "dvd'
});
matrix.get('title');
You can also pass it attributes directly when you instantiate like so:
matrix = new Movie({
title: "The Matrix",
format: "dvd'
});
If you need to enforce that certain required attributes when you build it, you can do so by providing an initialize()
function to provide some initial checks. By convention the initialize function gets called with the arguments you pass the constructor.
var Movie = Backbone.Model.extend({
initialize: function (spec) {
if (!spec || !spec.title || !spec.format) {
throw "InvalidConstructArgs";
}
// we may also want to store something else as an attribute
// for example a unique ID we can use in the HTML to identify this
// item's element. We can use the models 'cid' or 'client id for this'.
this.set({
htmlId: 'movie_' + this.cid
})
}
});
You can also define a validate()
method. This will get called anytime you set attributes and you can use it to validate your attributes (surprise!). If the validate()
method returns something it won't set that attribute.
var Movie = Backbone.Model.extend({
validate: function (attrs) {
if (attrs.title) {
if (!_.isString(attrs.title) || attrs.title.length === 0 ) {
return "Title must be a string with a length";
}
}
}
});
Ok, so there's a quite a few more goodies you get for free from the models. But I'm trying to give an overview, not replace the documentation (which is quite good). Let's move on.
Collections
Backbone collections are just an ordered collection of models of a certain type. Rather than just storing your models in a JS Array, a collection gives you a lot of other nice functionality for free. Functionality such as conveniences for retrieving models and a way to always keep in sorted according to the rules you define in a comparator()
function.
Also, after you tell a collection which type of model it holds then adding a new item to the collection is a simple as:
// define our collection
var MovieLibrary = Backbone.Collection.extend({
model: Movie,
initialize: function () {
// somthing
}
});
var library = new MovieLibarary();
// you can add stuff by creating the model first
var dumbanddumber = new Movie({
title: "Dumb and Dumber",
format: "dvd"
});
library.add(dumbanddumber);
// or even by adding the raw attributes
library.add({
title: "The Big Lebowski",
format: "VHS"
});
Again, there's a lot more goodies in Collections, but their main thing is solving a lot of the common problems for maintaining an ordered collection of models.
Views
Here's where your DOM manipulation (read jQuery) takes place. In fact, I use that as a compliance check: The only files that should have jQuery as a dependency are Views.
A view is simply a convention for drawing changes to a model to the browser. This is where you directly manipulate the HTML. For the initial rendering (when you first add a new model) you really need some sort of useful client-side templating solution. My personal biased preference is to use ICanHaz.js and Mustache.js to store and retrieve them. (If you're interested there's more on ICanHaz.js on github.) But then, your view just listens and responds to changes in the model.
Here's a simple view for our Movie items:
var MovieView = Backbone.View.extend({
initialize: function (args) {
_.bindAll(this, 'changeTitle');
this.model.bind('change:title', this.changeTitle);
},
events: {
'click .title': 'handleTitleClick'
},
render: function () {
// "ich" is ICanHaz.js magic
this.el = ich.movie(this.model.toJSON());
return this;
},
changeTitle: function () {
this.$('.title').text(this.model.get('title'));
},
handleTitleClick: function () {
alert('you clicked the title: " + this.model.get('title"));
}
});
So this view handles two kinds of events. First, the events
attribute links user events to handlers. In this case, handling the click for anything with a class of title
in the template. Also, this just makes sure that any changes to the model will automatically update the html, therein lies a lot of the power of backbone.
Putting it all together
So far we've talked about the various pieces. Now let's talk about an approach for assembling an entire app.
The global controller object
Although you may be able to get away with just having your main app controller live inside the AppView object, I didn't like storing my model objects in the view. So I created a global controller object to store everything. For this I create a simple a singleton object named whatever my app is named. So, to continue our example I might look something like this.
var MovieAppController = {
init: function (spec) {
// default config
this.config = {
connect: true
};
// extend our default config with passed in object attributes
_.extend(this.config, spec);
this.model = new MovieAppModel({
nick: this.config.nick,
account: this.config.account,
jid: this.config.jid,
boshUrl: this.config.boshUrl
});
this.view = new MovieAppView({model: this.model});
// standalone modules that respond to document events
this.sm = new SoundMachine();
return this;
},
// any other functions here should be events handlers that respond to
// document level events. In my case I was using this to respond to incoming XMPP
// events. So the logic for knowing what those meant and creating or updating our
// models and collections lived here.
handlePubSubUpdate: function () {};
};
Here you can see that we're storing our application model that holds all our other models and collections and our application view.
Our app model would in this example would hold any collections we may have, as well as store any attributes that our application view may want to respond to:
var MovieAppModel = Backbone.Model.extend({
initialize: function () {
// init and store our MovieCollection in our app object
this.movies = new MovieCollection();
}
});
Our application view would look something like this:
var MovieAppView = Backbone.View.extend({
initialize: function () {
// this.model refers the the model we pass to the view when we
// first init our view. So here we listen for changes to the movie collection.
this.model.movies.bind('add', this.addMovie);
this.model.movies.bind('remove', this.removeMovie);
},
events: {
// any user events (clicks etc) we want to respond to
},
// grab and populate our main template
render: function () {
// once again this is using ICanHaz.js, but you can use whatever
this.el = ich.app(this.model.toJSON());
// store a reference to our movie list
this.movieList = this.$('#movieList');
return this;
},
addMovie: function (movie) {
var view = new MovieView({model: movie});
// here we use our stored reference to the movie list element and
// append our rendered movie view.
this.movieList.append(view.render().el);
},
removeMovie: function (movie) {
// here we can use the html ID we stored to easily find
// and remove the correct element/elements from the view if the
// collection tells us it's been removed.
this.$('#' + movie.get('htmlId')).remove();
}
});
Ok so now for a snapshot of the entire app. I've included all my dependencies, each in their own file (read notes on this below). We also include the ICanHaz.js templates. Then on $(document).ready()
I would simply call the init function and pass in whatever variables the server-side code may have written to my template. Then we draw our app view by calling its render()
method and appending that to our <body>
element like so:
All said and done, now if we add or remove any movies from our collection or change their titles in the model those changes will just be reflected in the HTML like magic. With all the proper behaviors you'd defined for them in your MovieView.
General Tips
- Store all your objects in their own files for development.
- Compress and minify all your JS into one file for production.
- Use JSLint
- Extend underscore.js for any of your global utility type functions instead of modifying native objects. More on that in this gist.
- jQuery should only be used in your views. That way you can potentially unit test your model and collections on the server side.
- Use generic jQuery events to signal other views in your app. That way you don't tightly couple them.
- Keep your models as simple as possible.
I know I've covered a lot of stuff here, and some of it duplicates what's available in the backbone.js documentation but when I started working on building an app with it, I would have wanted a complete, yet somewhat high-level walkthrough like this. So I figured I'd write one. Big props to DocumentCloud and Jeremy Ashkenas for creating and sharing backbone with us.
If you have any thoughts, comments or reactions I'm @HenrikJoreteg on twitter. Thanks and good luck.
If you're building a single page app, keep in mind that &yet offers consulting, training and development services. Hit us up (henrik@andyet.net) and tell us what we can do to help.