Saturday, October 8, 2011

Overriding url and fetch in Backbone.js Collections

‹prev | My Chain | next›

After yesterday's Backbone.js session, my Recipes with Backbone co-author, Nick Gauthier, suggested a few improvements. I had gotten filtering working on my Backbone calendar so that only appointments from the currently displayed month would be fetched from the server. The setup code for this ran something like:
      var appointments = new Collections.Appointments();

      new Views.Application({collection: appointments});

      var today = new Date(),
        year = today.getFullYear(),
        month = today.getMonth() + 1,
        year_and_month = year + '-' + pad(month);

      appointments.fetch({data: {date: year_and_month}});
That code creates an instance of my appointments collections, hitches it to the application view, and then fetches the data from server. By invoking fetch() with the {data: {date: year_and_month}} argument, I am explicitly sending a query string of "date=2011-10" to the /appointments URL backing the collection.

This can be problematic if any other part of the application forces an appointments.fetch(). The collection instance no longer remembers the month, so the wrong month's appointments would be returned or worse—all of the appointments stored on the server could be returned.

Storing the date in the model is a simple matter of adding an initialize method to the collection:
        var Appointments = Backbone.Collection.extend({
          model: Models.Appointment,
          url: '/appointments',
          initialize: function(options) {
            options || (options = {});
            this.date = options.date;
          },
          // ...
         });
Of course, simply storing the date in the collection has no benefit unless something else in the class can act upon it. Since I know that I need to fetch() the collections, I try overriding fetch(). This follows the familiar pattern of supporting the same argument as the real Backbone.Collection.prototype.fetch(). With that, I can call the method on the Backbone.Collection's prototype:
        var Appointments = Backbone.Collection.extend({
          model: Models.Appointment,
          url: '/appointments',
          initialize: function(options) { /* ... */ },
          fetch: function(options) {
            options || (options = {});
            var data = (options.data || {});
            options.data = {date: this.date};

            return Backbone.Collection.prototype.fetch.call(this, options);
          },
          // ...
         });
To make switching months slightly more convenient, I follow Nick's suggestion of adding a setDate() method on the collection that sets the new date and then calls fetch():
      var Collections = (function() {
        var Appointments = Backbone.Collection.extend({
          // ...
          setDate: function(date) {
            this.date = date;
            this.fetch();
          },
          // ...
        });
With that, I change the setup code to set the month when I instantiate the appointments collection instead of when I fetch it:
      var year_and_month = Helpers.iso8601(new Date()).substr(0,7),
          appointments = new Collections.Appointments({date: year_and_month});

      new Views.Application({collection: appointments});

      appointments.fetch();
Reloading the page, I find that everything is still working:
Now, if I want to check out September's appointments, I only need redraw the calendar (with the non-Backbone draw_calendar()) and make use of the new setDate() method on the collection:
var september = "2011-09";
draw_calendar(september);
calendar.appointments.setDate('2011-09');
Best of all, if I need to redraw for some reason, I can now fetch() the collection without having to remember the date:
That works, but the overridden fetch() method is a bit dense. Perhaps if I had followed my co-author's advice in the first place—by overriding url—I might be in a cleaner place now. Of course nothing is preventing me from trying that out now...

I remove the overridden fetch(). I also remove the url attribute, which had been set to '/appointments'. I replace the url attribute with a url() method (turns out Backbone collections can use either a url attribute or method):
        var Appointments = Backbone.Collection.extend({
          // ...
          url: function () {
            return '/appointments/' + this.date;
          },
          // ...
        });
The collection in its entirety is now:
        var Appointments = Backbone.Collection.extend({
          model: Models.Appointment,
          initialize: function(options) {
            options || (options = {});
            this.date = options.date;
          },
          url: function () {
            return '/appointments/' + this.date;
          },
          setDate: function(date) {
            this.date = date;
            this.fetch();
          },
          parse: function(response) {
            return _(response.rows).map(function(row) { return row.value ;});
          }
        });
That url() method is much cleaner than the fetch() that I had earlier. There is a cost, however. The server now needs to respond to a URL like '/appointments/2011-10' as if this were requesting a list of records rather than a single record with ID of '2011-10'. In other words, I am no longer RESTful.

In this case, I am not using the GET single record resource, so I go ahead and make the change in my express.js server:
app.get('/appointments/:date', function(req, res){
  var options = {
    host: 'localhost',
    port: 5984,
    path: '/calendar/_design/appointments/_view/by_month?key="'+ req.params.date +'"'
  };

  http.get(options, function(couch_response) { /* ... */ });
});
It might be better to create a new collection resource like /month_appointments to support this. And I still might do that in the future. But, for now I live with my non-RESTful '/appointments/2011-10' route.

Trying this out in the Javascript console, I see that it still works:
And the correct appointments—all from September—are now displayed in my Backbone app:
So, in the end, overriding url() makes for some clean code. It does potentially violate REST, but there ought to be workarounds (i.e. with collection resources). I can still see some benefit to overriding fetch() to accomplish the same thing. Although denser, it does offer better ability to work with existing resources—something that can be especially handy when making backend changes is non-trivial.

Up tomorrow: switching months without the Javascript console (this time I mean it).


Day #168

5 comments:

  1. There is a bug:

    options || (options = {});
    var data = (options.data || {});
    options.data = {date: this.date};

    Should be:

    options || (options = {});
    var data = (options.data || {});
    options.data.date = this.date;

    Or
    options.data['date'] = this.date;

    Overwise you overwrite all of options.data with {date: ...} after going through the work to try to preserve it!

    ReplyDelete
    Replies
    1. Forgot to say thanks for the post!

      Delete
    2. And of course my reply has a bug. It should be:

      options || (options = {})
      options.data || (options.data = {})
      options.data.date = this.date

      That accomplishes what I think the original code meant to do.

      Delete
  2. First, thanks for writing this up, it's great following it all as I go through some of the same thought processes with my app.

    I don't think that replacing the URL makes it less RESTful, but this is why I override the default collection URL (by using urlRoot) in Backbone, and also use predicate mapping filters. In theory, you should have an appointment model with a url that *should* be something like :

    function(){return urlRoot+this.id}

    This just differentiates the collection URL has having a different REST structure even though the predicates are identical. When you look at the url of a Backbone collection it should describe the state of the models contained within; in your case, all the models fall within 2011-10, so it's a valid descriptor of a discrete set.
    Think about it: if another client interacted with the '/appointments/2011-10' url, the would get back the correct data regardless of whether it's pulling from a table with only appointments from 2011-10, or a view, or whatever. The point is if your server is setup to only allow GET/PUT/POST/DELETE to resources identified with the correct year/month modifier, you are doing RESTful operations on a discrete sub-set of your larger set, as long as you are using unique primary keys to identify items within the sub-set.

    ReplyDelete
  3. Thanks!
    This helped me get further with my project.
    Wanted to pull images down from an album, and this worked perfectly!

    ReplyDelete