Saturday, August 27, 2011

Converting to Backbone.js

‹prev | My Chain | next›

Last night I got my super slick, Funky Calendar populated with events from a CouchDB backend via AJAX. As I consider the harsh realities of what faces me when trying to create, update, delete those events in the UI, I think, maybe, I could use some help. So let's see if Backbone.js might do the trick.

First up, I install backbone.js locally and source it in my Jade layout:
!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(src='/javascripts/jquery.min.js')
    script(src='/javascripts/backbone.js')
  body!= body
Now, I need a Backbone model to encapsulate my events:
script
  $(function() {
    window.Event = Backbone.Model.extend({});
    });
  });
I do not think that I need any default values, specialized construction methods, or any other helper methods. Before moving on, I do a quick sanity check to make sure that that very simple thing is working. In fact, it is not. In my Javascript console, I see:
Uncaught TypeError: Cannot call method 'extend' of undefined  backbone.js:150
Checking out line 150 of backbone.js, I find:
  // Attach all inheritable methods to the Model prototype.
  _.extend(Backbone.Model.prototype, Backbone.Events, {
    // ...
Say, what's that funny underscore at the beginning of that line? Ohhh....

What I meant to say earlier was that I install backbone.js and underscore.js locally. I then source both in my site layout file:
!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(src='/javascripts/jquery.min.js')
    script(src='/javascripts/underscore.js')
    script(src='/javascripts/backbone.js')

  body!= body
(and it seems that underscore.js needs to come before backbone.js)

Now when I load the page, I get no errors. But, of course, I hope to do a little more than no errors, so now it is time to create a collection of events:
    window.EventList = Backbone.Collection.extend({
      model: Event
    });
Now I need to populate the EventList collection. The Backbone.js documentation seems to suggest bootstrapping the data inside the template as a preferred solution. I am not really a fan of this as it seems an opportunity for blocking the request as I lookup the data. Besides this is not how I did it with the AJAX version of Funky Calendar—I sent the static page, then loaded the events via an AJAX call.

To do the same in Backbone, I think I need to specify the URL for the collection and then add a parse() method to massage the CouchDB _all_docs resource into something that can be consumed by my Event model's initializer. CouchDB's response, proxied thru my app's /events resource, looks like:
{"total_rows":2,"offset":0,"rows":[
  {"id":"fdbed27594feb433c74e82eb910015e0",
   "key":"fdbed27594feb433c74e82eb910015e0",
   "value":{"rev":"2-b7c22d428e648a6cdd2978c213f79ec0"},
   "doc":{"_id":"fdbed27594feb433c74e82eb910015e0",
          "_rev":"2-b7c22d428e648a6cdd2978c213f79ec0",
          "startDate":"2011-08-25",
          "title":"create blog post",
          "description":"talk about node and CouchDB"}},
  {"id":"fdbed27594feb433c74e82eb91001f45",
   "key":"fdbed27594feb433c74e82eb91001f45",
   "value":{"rev":"1-2b18432cf6e63b82c6507ff28af9724c"},
   "doc":{"_id":"fdbed27594feb433c74e82eb91001f45",
          "_rev":"1-2b18432cf6e63b82c6507ff28af9724c",
          "startDate":"2011-08-26",
          "title":"blog again",
          "description":"add backbone into the node + couch mix"}}
]}
The following url and parse attributes on my collection ought to translate into an array of attributes that Backbone will send directly into the Event model's initializer:
    window.EventList = Backbone.Collection.extend({
      model: Event,
      url: '/events',
      parse: function(response) {
        return _(response.rows).map(function(row) { return row.doc ;});
      }
    });
I am using underscore.js to massage the rows in the CouchDB response into something with a map() function. I then map the rows to return just the doc attribute. This ought to produce the following array:
[{"_id":"fdbed27594feb433c74e82eb910015e0",
  "_rev":"2-b7c22d428e648a6cdd2978c213f79ec0",
  "startDate":"2011-08-25",
  "title":"create blog post",
  "description":"talk about node and CouchDB"},
 {"_id":"fdbed27594feb433c74e82eb91001f45",
  "_rev":"1-2b18432cf6e63b82c6507ff28af9724c",
  "startDate":"2011-08-26",
  "title":"blog again",
  "description":"add backbone into the node + couch mix"}]
To test this out, I instantiate an Events collection object and tell it to fetch() results from the server:
    window.Events = new EventList;
    Events.fetch();
Since I don't have this hooked up to anything in my UI, I drop down to the console to try this out:
Nice! Just like that, I have direct access to objects with the appropriate calendar event attributes.

Last up is to actually get the event data to display in my currently empty calendar:
For that, I will need a Backbone.js view object, which I cleverly name AppView:
    window.AppView = Backbone.View.extend({
    });
In there, I need to initialize the object to render whenever my Events collections loads all of its data. I also need to ensure that all of the data is fetched from the /events resource:
    window.AppView = Backbone.View.extend({
      initialize: function() {
        Events.bind('all', this.render, this);
        Events.fetch();
      }
    });
If that does what I expect it to, then all I need is a render method for this AppView class:
    window.AppView = Backbone.View.extend({
      initialize: function() {
        Events.bind('all', this.render, this);
        Events.fetch();
      },
      render: function() {
        Events.each(function(event) {
          var start_date = event.get("startDate"),
              title = event.get("title"),
              description = event.get("description"),
              el = '#' + start_date;

          $(el).html(
            '<span title="' + description + '">' +
              title +
            '</span>'
          );
        });
      }
    });
I am not getting into Backbone's templating in that render function. Rather I am just using jQuery for now—just like I did last night in my pure AJAX solution. I get the start data of the event, knowing that my HTML calendar cells have ISO 8601 date IDs just like my start dates (e.g. '#2011-08-27'). If the event start date is on my calendar, then I replace the matching element's html with a <span> containing the event title.

Once I have this class in place, all that is left is to instantiate it:
    window.AppView = Backbone.View.extend({
      initialize: function() { /* ... */},
      render: function() { /* ... */}
    });

    window.App = new AppView;
And now, I have my calendar events populated on my calendar via Backbone.js:
In the end, that turns out to be a heck of a lot more code than my pure AJAX solution (34 LOC vs 16). The idea behind Backbone.js is not to simplify simple applications. Rather it provides structure when building complex client-side applications. So I will pick back up tomorrow adding some complexity like creating, removing, and moving calendar events.

Day #126

No comments:

Post a Comment