Friday, October 7, 2011

Filtering Backbone.js Collections

‹prev | My Chain | next›

One of the things lacking in my Backbone.js calendar application is the ability to switch to different months. Currently it simply shows the current month (and does not even display the month name):


So I do a little plain-old Javascript hacking to give me a global function named draw_calendar(), which takes an ISO 8601 string that can draw the basic calendar (without the Backbone application adding appointments):
  function draw_calendar(year_and_month) {
    $('.year-and-month', 'h1').html(' (' + year_and_month + ') ');
    reset_calendar();
    add_dates_to_calendar(year_and_month);
  };
The first line in there now displays the calendar date:

And, if I set the date to September 2011 in Chrome's Javascript console:
draw_calendar('2011-09');
Then I am treated to a blank September calendar:
Now I just need to teach my Backbone application to filter its Appointments collection. But, first I need the CouchDB backend to be able to support filtering appointments by month.

In the futon admin interface, I define a temporary view that lists documents by the year and month of the appointment's startDate:

This gives me the desired results—keys of the format YYYY-MM and values of the appointment documents:

So I save the view so that I can re-use it in my application:

Since CouchDB is of the web, I can query my new view with curl to verify that I can find all startDates in the month of 2011-10:
➜  calendar git:(pagination) curl http://localhost:5984/calendar/_design/appointments/_view/by_month\?key\='"2011-10"'
{"total_rows":18,"offset":12,"rows":[
  {"id":"4a5600f5b2e36fc99d24fe9b8700037d",
   "key":"2011-10",
   "value":{
     "_id":"4a5600f5b2e36fc99d24fe9b8700037d",
     "_rev":"5-181418fffeacdc08b5fdda91525978b6",
     "title":"Update dialog errors",
     "description":"asdf1",
     "startDate":"2011-10-05"}},
  {"id":"8b5c80c0211068428272af4784000451",
   "key":"2011-10",
   "value":{
     "_id":"8b5c80c0211068428272af4784000451",
     "_rev":"1-87f5be84687eada2cd178c8dab7aa34c",
     "title":"Finish Beta",
     "description":"Book important",
     "startDate":"2011-10-31"}},
  {"id":"8b5c80c0211068428272af478400f7bb",
   "key":"2011-10",
   "value":{
     "_id":"8b5c80c0211068428272af478400f7bb",
     "_rev":"2-b1ce8c6c815a11f7b78b7db412988451",
     "title":"Go to bed early",
     "description":"asdf",
     "startDate":"2011-10-22"}},
  {"id":"8b5c80c0211068428272af478400fc93",
   "key":"2011-10",
   "value":{
     "_id":"8b5c80c0211068428272af478400fc93",
     "_rev":"1-b19359520433df557ad2fa0d56165f24",
     "title":"Validations",
     "description":"description should be required",
     "startDate":"2011-10-03"}},
   {"id":"956a5c19fd866a6a024bbb4c39002e3b",
    "key":"2011-10",
    "value":{
      "_id":"956a5c19fd866a6a024bbb4c39002e3b",
        "_rev":"2-0bbb8660f7f116cc813e0fd9093cec6a",
        "title":"Has Description",
        "description":"asdf",
        "startDate":"2011-10-13"}},
   {"id":"956a5c19fd866a6a024bbb4c390031a2",
    "key":"2011-10",
    "value":{
      "_id":"956a5c19fd866a6a024bbb4c390031a2",
      "_rev":"2-0568aded9df520a0c1c8bb2ee8961156",
      "title":"In-dialog errors","description":"asdf",
      "startDate":"2011-10-04"}}
]}
Yay!

I update my backend server, which is effectively middleware between the browser and CouchDB, to request this design document resource rather than the _all_doc resource it had been using.

Teaching the Apppointments Backbone collection how to interact with this new resource requires almost no changes at all. Only the parse() method needs to be updated to return appointments from the value attribute of my query:
      var Collections = (function() {
        var Appointments = Backbone.Collection.extend({
          model: Models.Appointment,
          url: '/appointments',
          parse: function(response) {
            return _(response.rows).map(function(row) { return row.value ;});
          }
        });

        return {Appointments: Appointments};
      })();
To initialize the collection, I now need to pass the date query parameter to the Appointments URL. This is done by passing a data option to the collection's fetch() method (just like with jQuery's ajax() call):
// ...
      // Initialize the app
      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}});
With that working, all that remains is the ability to switch months.

For tonight, I will just try to get this working manually in Chrome's Javascript console. In there I set the variable september to last month's date, then draw_calendar(september) (to get the calendar itself drawn correctly). Lastly, I tell the collection to load in September's appointments via a calendar.appointments.fetch({data: {date: september}}):
Based on the length of the Calendar appointments collections, it seems that 9 appointments hath September and, indeed, they actually show up on the calendar:

Best of all, I can do all of the common operations in my Backbone application like editing:
That is pretty darn cool.

I call it a night here. I will pick back up tomorrow trying to do all of this without resorting to Javascript console hacking.


Day #167

3 comments:

  1. There is an alternative to this that makes the collection more resilient: url as a function.

    Because you're setting the date via the fetch, any other call to fetch would override what you set. Alternatively:


    var appointments = new Collections.Appointments({date: year_and_month});

    Then in the constructor:

    this.year_and_month = options.year_and_month

    Then define:

    url: function() {
    return '/appointments/'+year_and_month
    }
    setDate: function(y_m) {
    this.year_and_month = y_m;
    this.fetch;
    }

    Then at any time you can do fetch() and it will refresh the current month, or you can do setDate('2011-11') to change the month.

    This way you don't have to litter your app with fetch statements, and also when you create a new event you already know the month :-)

    ReplyDelete
  2. I generally don't emit the complete doc as the value in a CouchDB view map function. Instead of 'emit(doc.startDate.subStr(0, 7), doc);' I'd use 'emit(doc.startDate.subStr(0, 7), null);' and query the view using ?include_docs=true.

    This is a performance versus storage trade-off. Views will be much smaller if you don't emit the doc as the value, but view queries will be a bit faster if you have the doc stored in the view index.

    ReplyDelete
  3. @nick Good stuff -- will give that a try. It didn't occur to me that URL could be a function.

    @breun Thanks for the tip! It probably doesn't make much of a difference with my tiny database (18 documents total), but I will keep that in mind when I get around to scaling.

    ReplyDelete