Thursday, December 8, 2011

Require.js Backbone Views

‹prev | My Chain | next›

Tonight, I hope to convert the views in my Backbone.js application to use require.js. Until now, I have been collecting my views inside a closure so that only those views needed by the outside world are exposed. Also, I have a lot of views:
var Views = (function() {
    var Appointment = Backbone.View.extend({ /* ... */ });

    var CalendarMonth = Backbone.View.extend({ /* ... */ });
    var CalendarMonthHeader = Backbone.View.extend({ /* ... */ });
    var CalendarMonthBody = Backbone.View.extend({ /* ... */ });
    var CalendarMonthWeek = Backbone.View.extend({ /* ... */ });
    var CalendarMonthDay = Backbone.View.extend({ /* ... */ });

    var AppointmentEdit = new (Backbone.View.extend({ /* ... */ }));
    var AppointmentAdd = new (Backbone.View.extend({ /* ... */ }));

    var CalendarNavigation = Backbone.View.extend({ /* ... */ });

    function template(str) { /* ... */ }

    var TitleView = Backbone.View.extend({ /* ... */ });

    var Application = Backbone.View.extend({ /* ... */ });

    return {
      Application: Application
    };
  })();
Yesterday, I found that working my way up the precipitation chain—that is from lower order Backbone objects (e.g. models) to higher order objects (collections and views)—worked fairly well for this kind of refactoring.

For the most part, this goes fairly smoothly. I adopt the file naming convention of using the same capitalization that is used in the actual code. Thus, CalendarMonthDay becomes public/javascripts/calendar/views/CalendarMonthDay.js on the filesystem. Aside from the require.js define() library wrapper, it looks very much the same as a plain-old Backbone view:
// javascripts/calendar/views/CalendarMonthDay.js
define(['backbone',
        'underscore',
        'calendar/helpers/to_iso8601'],
function(Backbone, _, to_iso8601) {
  return Backbone.View.extend({
    tagName: 'td',
    initialize: function(options) {
      this.date = options.date;
    },
    render: function() { /* ... */ },
    events : { /* ... */ },
    // ...
  });
});
Working all the way on up to the top of the Calendar view stack, the CalendarMonth, which is responsible for drawing the actual calendar, is similarly familiar:
define(['backbone',
        'jquery',
        'calendar/views/CalendarMonthHeader',
        'calendar/views/CalendarMonthBody'],
function(Backbone, $, CalendarMonthHeader, CalendarMonthBody) {
  return Backbone.View.extend({
    tagName: 'table',
    initialize: function(options) {
      this.date = options.date;
    },
    render: function() {
      var header = new CalendarMonthHeader();
      header.render();
      $(this.el).append(header.el);

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

      return this;
    }
  });
});
The only trouble that I encounter in this exercise is how to best split up helper functions. In my old, monolithic version of the application, I had an extensive list of mostly date-related helper functions:
var Helpers = (function() {
    function pad(n) {return n<10 ? '0'+n : n}
    function to_iso8601(date) { /* ... */ }
    function from_iso8601(date) { /* ... */ }
    function firstOfTheMonth(date) { /* ... */ }
    function previousMonth(month) { /* ... */ }
    function nextMonth(month) { /* ... */ }
    function dayAfter(date) { /* ... */ }
    function weekAfter(date) { /* ... */ }

    return {
      to_iso8601: to_iso8601,
      previousMonth: previousMonth,
      nextMonth: nextMonth,
      dayAfter: dayAfter,
      weekAfter: weekAfter,
      firstOfTheMonth: firstOfTheMonth
    };
  })();
During my initial require.js refactoring, I simply break each out into separate require.js files / libraries. That makes for some messy library definitions. For example, the CalendarMonthBody view relies on three helpers:
define(['backbone',
        'underscore',
        'calendar/views/CalendarMonthWeek',
        'calendar/helpers/firstOfTheMonth',
        'calendar/helpers/to_iso8601',
        'calendar/helpers/weekAfter'],
function(Backbone, _, CalendarMonthWeek, firstOfTheMonth, to_iso8601, weekAfter) {
  return Backbone.View.extend({
    initialize: function(options) { /* ... */ },
    render: function() { /* ... */ }
    }
  });
});
From a long-term maintainability standpoint, I do not care for that. A function with an arity of 6—even if it is a library wrapper—just feels wrong. I will live with that during refactoring, but my hope is to pull at least some of the helpers out of libraries and directly into the CalendarMonthBody definition. That is, if the firstOfTheMonth helper is only used by CalendarMonthBody, then it need not be a helper. In that case, it can live as an inline function.

Once all of the Calendar views have been extracted into require.js libraries, I call it a night. Up tomorrow, I have a couple of anonymous views still lurking. I am not quite sure how those are going to work in this brave new require.js world. That could prove a challenge.


Day #229

2 comments:

  1. Hi Chris,

    There is a sugared form of define() that is supported, mostly for wrapping existing CommonJS modules, but it has the nice property of vertically aligning dependencies:

    define(function (require) {
    var a = require('a'),
    b = require('b')
    });

    More info here:

    http://requirejs.org/docs/whyamd.html#sugar

    ReplyDelete
  2. Oh, man! I was already tempted to start pulling long requirements out of `arguments` :P

    I rather like the non-sugar way. Too many dependencies feel like a code smell (esp. in a BB application) and high arity makes it obvious. But this? It's too pretty to _not_ use!

    I'm definitely going to have a go at that tonight. Thanks!

    ReplyDelete