Sunday, November 6, 2011

Model "Has-A" Collection in Backbone.js

‹prev | My Chain | next›

Yesterday, I was able to implement a mostly decent "has many" relationship in my Backbone.js calendar application. In there, an appointment can have many people invited to it:
{
   "_id": "6bb06bde80925d1a058448ac4d004fb9",
   "_rev": "2-7fb2e6109fa93284c19696dc89753102",
   "title": "Test #7",
   "description": "asdf",
   "startDate": "2011-11-17",
   "invitees": [
       "6bb06bde80925d1a058448ac4d0062d6",
       "6bb06bde80925d1a058448ac4d006758",
       "6bb06bde80925d1a058448ac4d006f6e"
   ]
}
Mapping those IDs to people in my backend, I can display their last names:


A couple of things worry me about yesterday's solution. First it took a fair bit of effort to get it working properly. Second, the responsibility for building that collection falls on the appointment view:
    var AppointmentEdit = new (Backbone.View.extend({
      // ...
      showInvitees: function() {
        $('.invitees', this.el).remove();
        $('#edit-dialog').append('<div class="invitees"></div>');

        var invitees = this.model.get("invitees");

        if (!invitees) return this;
        if (invitees.length == 0) return this;

        var collection = new Collections.Invitees({invitees: invitees});
        var view = new Invitees({collection: collection});

        $('.invitees').append(view.render().el);

        collection.fetch();

        return this;
      }
    }));
It is hard for me to say if this is a terrible practice. The Appointment view should be responsible for creating the invitee view and attaching it (per the precipitation pattern in Recipes with Backbone). So maybe this is a perfectly OK approach.

Still, the server-side MVC coder in me would prefer that the view did not do quite so much here. Maybe it would be nicer if I could get the collection as this.model.invitees(). Then the above could be re-written as:
    var AppointmentEdit = new (Backbone.View.extend({
      // ...
      showInvitees: function() {
        $('.invitees', this.el).remove();
        $('#edit-dialog').append('<div class="invitees"></div>');

        if (this.model.invitees.length == 0) return this;

        var view = new Invitees({collection: this.model.invitees});

        $('.invitees').append(view.render().el);

        return this;
      }
    }));
I rather like that. If I can make it work, I do away with the fetch()-in-a-view, needing to check the presence of the "invitees" attribute on the appointment model and the need to instantiate the collection. It is not so much that I am saving three lines of code. Rather, it is that I am decreasing the responsibility of this "show" invitees method that makes me excited.

So, back in the appointment model, I add a loadInvitees() method as part of the initialize() method. The loadInvitees() method of the appointment is now responsible for creating the invitees collection:
    var Appointment = Backbone.Model.extend({
      urlRoot : '/appointments',
      initialize: function(attributes) {
        // ...
        this.loadInvitees();
      },
      // ...
      loadInvitees: function() {
        var ids = this.get("invitees") || [];

        this.invitees = new Collections.Invitees({invitees: ids});
        this.invitees.fetch();
      }
    });
And that works.

I rather like that. The Appointment view is now solely responsible for attaching the sub-view. The appointment view still needs to know about the existence of the collection , but there is really no way to get around that: an appointment has many people invited to it. This is a plain fact of which the appointment view needs to be aware.

One interesting note here is that this approach seemingly violates the Precipitation Pattern from Recipes with Backbone. This pattern suggests that information in Backbone applications should flow from Router, to View, to Collection, and finally to Model. In this case, a view instantiates a model, which instantiates a collection. Although a violation of the letter of the law, this is clearly not a violation of the spirit: the collection is clearly subordinate to the model in this case.


Day #197

1 comment:

  1. For anyone reading this post, check out these links
    http://backbonerelational.org/
    http://stackoverflow.com/questions/10871369/how-to-handle-relations-in-backbone-js

    ReplyDelete