The Backbone.js Todo List Sample, Refactored - Part 1
- Thursday, August 11, 2011
This is a long post with lots of code. I want to be complete, but I also don't want to bore you to tears. If you want to see the refactor right now - here it is. I'm fairly certain I don't know what I'm doing - and that I probably lack the experience to even be writing this post. All I can tell you is that I have a feeling… a not so good feeling… when reading the current Todo List tutorial up on Github. So I figured I'd refactor it and submit a pull request. Is this correct? You tell me…
What’s The Problem?
Backbone is all about views reacting to events on models and collections. Your data publishes various events - like “change” on the model and “add”, “remove” and “fetch” on the collection. You Views (and other things) react to these events and the user sees the result.
So, when reading over the Todo list app I was a little puzzled with some of the code there. I mean literally puzzled - not puzzled in a snarky way. For instance - look at this template code:
If statements and instrumentation in the template doesn’t really “flow” with the Backbone idea.
In addition - the app constructs are loaded on document.ready():
Which doesn’t make sense - the methods that use the DOM shouldn’t be loaded - sure - but the constructs should. This is sort of minor - and as I mention: puzzling.
And finally, on line 131 is a handler for ending the inline edit experience of a given task:
“updateOnEnter” as you see here is bound to the “keypress” event - so the View is listening to each keypress and if you hit the enter key - this calls “close()”.
The Close method saves the model then updates the UI with the correct class. This doesn’t make all that much sense from an eventing point of view. The only event we have here is “keypress” and that magically leads to a class change and the saving of a model.
Once again - and so as not to offend anyone - when I first saw this I couldn’t tell you precisely what was wrong with it. All I knew is that it confused me and so I thought that maybe I would take a crack at writing it myself to see how I would do it differently.
I managed to trim out about 40 or so lines of code - from 250ish down to 208ish. Keep in mind that both files are heavily commented.
My first approach was to split the UI out into different Views, the idea being that each one has a specific responsibility:
The first View is the FormView - it handles the input of the various Tasks. The purple box after that is the ListView - it shows the list of items, allows you to edit them and set their status etc, and the yellow box on the bottom is the StatsView - showing you what tasks you have as well as allowing you to remove the completed bits.
The ListView has a child view as well - each line that you see there is a little sub-view called ListItemView. This might seem like a lot of views - but they’re needed to keep the code craziness in check.
Let’s see how these all work.
If you’ve been diddling with Backbone at all - you’re used to starting out with the model. So I’ll do the same here:
You “extend” the Backbone.Model bits into what you can think of as a “class”. Here, I’ve set defaults for the model’s data - specifically a status which I’ll set as “incomplete”.
Normally you wouldn’t override the “save()” method as I’ve done here - but I’m using in-memory storage rather than a remote REST call - so I’ve overridden it.
Finally I created a method which keeps the status change on a task internal so the Views aren’t trying to set properties and so on - the last line of this method is the important one: this.collection.trigger(“status-changed”). This basically fires an event - one that I just made up - telling the collection one of its models has changed its status.
What’s a Collection? Good question…
The collection is an intelligent wrapper for a bunch of Models - in this case, Tasks. All you have to do is tell it which Model it’s wrapping - which we do in the first line - and you’re off to the races.
Typically a collection comes with the ability to do a JSON call off to a remote server - so in the second line you should specify a URL from which is can pull some JSON - in our case this would probably be “/tasks”. Since I’m using in-memory here, I don’t need that.
The final two methods simple query the collection using Underscore’s “select” method - returning the completed and incomplete tasks.
Finally - I instantiate a global collection here for use throughout the app.
OK - so we have our data ready to go - let’s kick up the Views…
This is where I veered away from the original sample. I’m not a fan of trapping the enter key - I’d rather let the DOM do the work for me - so I decided to use jQuery Templates to create the HTML and wire up the submit function. This is where things get a bit hairy… hang on…
In line one I tell the View which template to use - simple enough. Because the template is just a template and not part of the DOM when the page renders - I can’t use $(function) to wire events - I need Backbone to do it for me.
That’s the next thing here - “events”. This hash basically says “here’s an event, here’s the jQuery selector criteria” and I want you to call “this method” when the event fires. In essence, Backbone is giving you a specific place to write events for your template - in the same way you would do with document.ready normally.
The “render” method is the bread and butter of any view - here I’m telling jQuery templates to render the template for me and then I’m shoving that rendered stuff into a special construct called “el” - more on that in a moment. Finally - I’m returning the View so things can be chained on later.
Remember I mentioned that Views handle eventing for you? But how do they do that? It’s not magical - Backbone simple creates a DOM element for you and attaches those events to it. All the DOM bits that come out of the template get popped into “el” - and BOOM you have eventing on your templates.
This is confusing. If you’re freaked out and your head hurts, I don’t blame you. It is rather simple, however, if you play around for a bit.
If you look at the “events” hash you’ll see that I’m wiring the submit event of my form to a method called “save” - and that’s the last function in the form. I’m pulling out the value explicitly here - but as the comment says there are better ways to bind your model to your form - so go read up on what Dereck Bailey is doing.
Anyway - this sets the model’s “name” key to the value in the text box and then saves it. If you look up the page here a bit - you’ll see the save method on the model simply adds it to the “tasks” collection in memory - that’s not always what happens.
Usually it gets PUT to your web server (or POSTed if it’s new). But that’s more than I want to cover here.
OK goody - so how does this thing actually get popped onto the page?
We have a Model, a Collection, and a View - they don’t just magically play happily together. You need to instrument their interactions in the same way a Controller rides shotgun with a View and Model in server-side MVC.
For this, we use the Backbone Router (which used to actually be called a Controller). It’s job is simple: look at the URL and decide what to do. Here’s our Router (the original sample didn’t include one):
The first thing the Router does is instantiate the Views (you’ll notice there are 2 more views here then what we’ve talked about - more on that in a bit). It passes the information to those views that they need to exist - in this case a Collection and the DOM element that I want to use as the “el”.
The “collection” is simply our global tasks variable (see the Collection stuff up top). “el” is being set here by me explicitly - but that’s not required. I happen to like creating views that can render themselves - this might be wrong - but I think it’s much cleaner doing it this way.
The next part is “routes”. In here we only have a single route: the default empty route. This gets fired when a user comes to the page - and I’m telling the Router “when you see nothing, fire the index action” - which its doing.
In the index method - the form View is rendered. This pulls the jQuery template, renders the HTML within it, and shoves it into the “#todo-form” DIV tag.
But what about the other views? How do they get on there?
I’ll cover that in Part 2. For now - I need to eat lunch and rest my cramped fingers.