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});
        view.render();
      }
    });
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});
        view.render();
      });
    });
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) {
      console.log('add!!!!');
      var view = new AppointmentView({model: appointment});
      view.render();
    });
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() {
        Backbone.Model.prototype.save.call(this, {
          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")};
        Backbone.Model.prototype.save.call(this, 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

3 comments:

  1. This line:

    Backbone.Model.prototype.save.call(this, attributes, options});

    Has an extra curly brace at the end

    ReplyDelete
  2. The last code sample forget to return which breaks jQuery deferred thus making impossible to do model.save().done().
    Last line should be: return Backbone.Model.prototype.save.call(this, attributes, options);

    ReplyDelete