Sunday, October 30, 2011

Let the Custom Events Fly in Backbone.js

‹prev | My Chain | next›

Yesterday, I was able to use custom events in my Backbone.js calendar application to further decouple my design. Today, I hope to use them to add an actual feature. Specifically, I would like to add a filter view that can be used to highlight matching appointments on the calendar.

If I filter on the word "beta", for instance, then the last appointment in October should be highlighted:


I start with a simple filter view that contains just a text field for the text snippet being used to filter and a "Filter" button:
    var CalendarFilter = Backbone.View.extend({
      template: template(
        '<input type="text" name="filter">' +
        '<input type="button" class="filter" value="Filter">'
      ),
      render: function() {
        $(this.el).html(this.template());
        return this;
      },
      events: {
        'click .filter':  'filter'
      },
      filter: function() {
        var filter = $('input[type=text]', this.el).val();
        $('.appointment').trigger('calendar:filter', filter);
      }
    });
In there, I attach an event listener that fires the filter() method when the "Filter" button (with class="filter") is clicked. The filter() method triggers events on all of the appointments in the calendar.

I kinda want to trigger a calendar:filter:foo event when the user supplies "foo" as the filtering text. Somehow that just feels cleaner. Unfortunately, there is no way to listen for wildcard events (e.g. "calendar:filter:*)—jQuery events simply do not work with those. Anyhow, triggering an "calendar:filter" event with "foo" as the event data is not that much different.

It also feels a little wrong to trigger events on items with an "appointment" class. Explicitly targeting the class assigned by my Appointment view class is coupling the two classes. Unfortunately, I see no way around this. I do not know any way to broadcast events other than actually grabbing every element in the current DOM. That, of course, would introduce another set of problems (too many events, events bubbling over each other, etc).

Anyhow, to use that view, I add it to the list of things to be created by the application view:
    var Application = Backbone.View.extend({
      initialize: function(options) {
        // ...
        this.initialize_filter();
      },
      // ...
      initialize_filter: function() {
        $(this.el).after('<div id="calendar-filter">');

        var filter = new CalendarFilter({
          el: $('#calendar-filter')
        });
        filter.render();
      },
      // ...
    });
In there, I do what's becoming a mini pattern for me: I create the holder <div> tag, create the CalendarFilter object and render() it. With that, I have my filtering UI:


To actually get the appointments to filter themselves, I need the Appointment class to respond to the "calendar:filter" event, which I tie to a filter() method:
    var Appointment = Backbone.View.extend({
      // ...
      events: {
        'click': 'handleClick',
        'calendar:filter': 'filter'
      },
      // ...
      filter: function(evt, str) {
        var regexp = new RegExp(str, "i");
        if (this.model.get("title").toString().match(regexp)) {
          $(this.el).addClass("highlight");
        }
        else {
          $(this.el).removeClass("highlight");
        }
      },
      // ...
    });
In that filter() method, I create a regular expression that will perform a case-insensitive match on the filter term. If the current appointment matches the regular expression, then I add the "highlight" class to it. Otherwise, I make sure that the term is not highlighted.

With that, I have my highlighting working:


Cool beans. Custom events are certainly a useful tool. Somebody should write recipe about them. Preferably by tomorrow.


Day #190

2 comments:

  1. The calendar filter should fire an event on itself. Then the collection view should be bound to the calendar filter's event and call "highlight" on all matching subviews. Trigger should be considered a private method.

    See the pagination chapter I just wrote.

    ReplyDelete
  2. Heh. I figured some of that out while writing the custom events chapter. That's the kind of review that writing a book yields (I love it so!). It's especially nice having someone critique it so quickly :)

    It makes complete sense that trigger should be considered private, thanks. I don't think I had taken it that far yet.

    ReplyDelete