Tuesday, October 25, 2011

Precipitating Backbone.js Views

‹prev | My Chain | next›

I really need to cut down on my posting to focus on my Recipes with Backbone writing. So far, my co-author Nick Gauthier is doing most of the writing at that's not cool. Still, I must appease the gods of my chain...

I undertook a significant refactoring of some hideous Backbone.js code last night. For the most part, I liked my smaller, focused Backbone views. It certainly beats a huge template full of evaluate-able code.

One thing that bothers me is that my sub-views need to know about the parent view. Per the Precipitation Pattern in Recipes with Backbone, it is OK for higher order objects to reference lower order objects, but not the other way around.

Consider, for example, my calendar:

The lowest level thing in the calendar is the day view. But my day view knows about week view that called it:

        var CalendarMonthDay = Backbone.View.extend({
          tagName: 'td',
          initialize: function(options) {
            this.parent = options.parent;
            this.date = options.date;
          },
          render: function() {
            this.parent.append(this.el);

            this.el.id = Helpers.to_iso8601(this.date);
            var html = '<span class="day-of-month">' + this.date.getDate() + '</span>';
            $(this.el).html(html);

            return this;
          }
        });
Really, my first clue should have been the reference to parent. You should never see a reference to a parent object in Backbone code.

The problem is that both sides of the relationship need to know about each other, compounding the likelihood that something is going to go wrong in one place or the other. If nothing else, it makes for more code than is necessary.

The month/week view in my app needs to know how to render itself into the parent view in addition to knowing that the day sub-views needs a reference to the week view:
        var CalendarMonthWeek = Backbone.View.extend({
           // ...           
           render: function() {
            for (var i=0; i<7; i++) {
              var day = new CalendarMonthDay({
                parent: $(this.el),
                date: date
              });
              day.render();
              $(this.el).append(day.el);

              date = Helpers.dayAfter(date);
            }
          });
That's just silly.

If I refactor my lowest order object, the day-view, so that it is completely unaware of the higher order week view, then it looks like:
        var CalendarMonthDay = Backbone.View.extend({
          tagName: 'td',
          initialize: function(options) {
            this.date = options.date;
          },
          render: function() {
            this.el.id = Helpers.to_iso8601(this.date);
            var html = '<span class="day-of-month">' + this.date.getDate() + '</span>';
            $(this.el).html(html);

            return this;
          }
        });
That is 3 fewer lines of code and one less responsibility (inserting into the parent object's HTML node).

In the week view, I no longer pass the week view's DOM el to the day view and I no longer have to insert the week view's DOM el into the month view's parent el. All I have to do is insert the day view's el (a table td cell) into the week view's tr table row:
        var CalendarMonthWeek = Backbone.View.extend({
          tagName: 'tr',
          initialize: function(options) {
            this.date = options.date;
          },
          render: function() {
            var date = this.date;
            for (var i=0; i<7; i++) {
              var day = new CalendarMonthDay({date: date});
              day.render();
              $(this.el).append(day.el);

              date = Helpers.dayAfter(date);
            }
          }
        });
Single responsibility with an assist from the Precipitation Pattern nets me 4 fewer lines of code. Nice.

I work through the remaining views (in all, there are CalendarMonth, CalendarMonthHeader, CalendarMonthBody, CalendarMonthWeek, and CalendarMonthDay). The only one that gives me any trouble is the CalendarMonthBody because it has no DOM el. Its purpose is to reside a the same abstraction layer as the CalendarMonthHeader and to proxy the week rows back to the CalendarMonth.

The solution there turns out to create a fake el that holds an array of week DOM elements:
        var CalendarMonthBody = Backbone.View.extend({
          initialize: function(options) {
            this.date = options.date;
            this.el = [];
          },
          render: function() {
            // ...
            while (date == firstSunday || date.getMonth() <= month) {
              var week = new CalendarMonthWeek({date: date});
              week.render();
              this.el.push(week.el);

              date = Helpers.weekAfter(date);
            }
          }
        });
I can get away with this because, at each higher order order layer, I am using jQuery's append() to insert the lower layer's DOM element. Conveniently, append accepts an array of elements:
        var CalendarMonth = new (Backbone.View.extend({
          el: $('#calendar'),
          setDate: function(date) {
            this.date = date;
            this.render();
          },
          render: function() {
            $('span.year-and-month', 'h1').html(' (' + this.date + ')');

            $(this.el).html('<table></table>');

            var table = $('table', this.el);

            var header = new CalendarMonthHeader()
            header.render();
            $(table).append(header.el);

            var body = new CalendarMonthBody({date: this.date});
            body.render();
            $(table).append(body.el);

            return this;
          }
        }));
The month-header and month-body insertions look exactly the same: $(table).append(lower_layer_view.el) even though one is a single element and the other is an array of week rows. Some might think of that as a cheat. I choose to think of it as a pattern.

Anyhow, my quick refactoring has netted me a total of 12 new lines of code and 29 deleted lines of code, for a net loss of 17 lines of code that also serves to decrease responsibility through an entire vertical slice of my Backbone app. That is a big win.


Day #185

No comments:

Post a Comment