Saturday, September 17, 2011

Don't Be Stupid When Overriding Backbone.js Methods

‹prev | My Chain | next›

Yesterday, I was able to get an add-appointment view into my Backbone.js calendar application. The add View is a thin wrapper around a jQuery UI dialog. It seems to work well (I did the same thing with my edit View).

The problem is that, after I create my View, I have to manually add a new appointment View:
    window.AppointmentAddView = Backbone.View.extend({
      create: function() {
        var appointment = Appointments.create({ /* options here */ });

        // TODO: convert to event listener....
        var view = new AppointmentView({model: appointment});
What I would like to have occur is that the collection notices a new appointment and is responsible for creating a new View object. The collection is already responsible for doing this when existing appointments are fetched after page load:
    window.Appointments = new AppointmentList;

    Appointments.bind('reset', function(appointments) {
      appointments.each(function(appointment) {
        var view = new AppointmentView({model: appointment});
So, when my appointment-add View calls Appointments.create(), this ought to trigger an "add" event on the collection. So, can I extract the appointment View creation call out of the appointment-add View's create() method and dump it into an event handler:
    Appointments.bind('add', function(appointment) {
      var view = new AppointmentView({model: appointment});
With that, I see some definite similarities between the Collection reset and add event handlers. I will worry about DRYing things up once I have it working. And I don't... my jasmine spec covering add-appointments is now failing:
It takes me quite a while to track this down. The Firebug debugger does not work for me right now and the specs do not work in Chrome, so I reduced to console.log() debugging. Eventually, I realize that my model is not passing success events back to the Collection. It sounds easy enough to find in retrospect, but I had 30+ console.log() statements strewn about my code and the Backbone.js library.

Ultimately, I trace the issue down to my Model class. Since I am using a CouchDB data store, I need to pass document revisions along with the document. I accomplish this via an If-Match header. It's all very slick:
    window.Appointment = Backbone.Model.extend({
      urlRoot : '/appointments',
      save: function() {, {
          headers: {'If-Match': this.get("rev")}
      // ...
Except, I am missing something crucial to getting this to work. My save() method takes no arguments whereas the Backbone method that I am overriding takes two arguments. The second argument contains options, which include on-success and on-error handlers. Since I completely ignore them here, my Model's save() method has no way of calling them. Duh.

The fix is easy enough:
      save: function(attributes, options) {
        attributes || (attributes = {});
        attributes['headers'] = {'If-Match': this.get("rev")};, attributes, options);
Instead of ignoring the options being passed in, I am now sure to pass them along to the save() call on the prototype's save() method. For good measure, I do the same for attributes while I am at it.

With that, I have my tests passing again:
I am happy to stop here for the day. I consider myself lucky to have escaped that mess unscathed. The lesson learned from today: if you're going to override a method, be sure to accept the same arguments (and pass them along the original). Sadly, I should have already known that particular lesson. The other lesson: be grateful for solid tests. I shudder to think how long that would have taken me to track down without my Jasmine suite.

Day #136


  1. This line:, attributes, options});

    Has an extra curly brace at the end

  2. The last code sample forget to return which breaks jQuery deferred thus making impossible to do
    Last line should be: return, attributes, options);