Wednesday, October 26, 2011

Abusing Singleton Views in Backbone.js

‹prev | My Chain | next›

Like any newbie, I am prone to fixate on a fascinating idea or concept. I have done that several times as I explore Backbone.js and the many recipes in Recipes with Backbone. One such instance is the Singleton View.

I continue to use the singleton view to manage the add/edit jQuery UI dialog forms in my calendar application:

There will only ever be a single form. Each time a user user edits an appointment, it re-uses that same form, populating the fields with different values. But it is always the same form. Hence the value of the singleton view.

Where it is not appropriate is the main calendar view, in this case, the month view:
        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;
          }
        }));
This is a singleton since I am assigning an instance of an anonymous View class to the CalendarMonth variable:
        var CalendarMonth = new (Backbone.View.extend({ /* ... */ });
The first sign of trouble is the element attribute, which is set to an existing DOM element:
        var CalendarMonth = new (Backbone.View.extend({
          el: $('#calendar'),
          // ...
        });
To be clear, there are some legitimate reasons to do something like that, but it is a bit of a code smell. Recipes with Backbone might view this as a slight violation of the the Precipitation Pattern—that lower order objects should have no knowledge of higher order concepts. Normally, the Precipitation Pattern applies to models, which should have no knowledge of collections, views and routes. But it can also apply to views (e.g. the day view should remain blissfully unaware of the month view).

I can also tell that I have gone sideways in that my render() method is doing more than simply rendering itself. It also causes a side-effect (modifying the page's existing <h1> tag) and creates a new <table> container:
var CalendarMonth = new (Backbone.View.extend({
          // ...
          render: function() {
            $('span.year-and-month', 'h1').html(' (' + this.date + ')');

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

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

            // Add a  CalendarMonthHeader to the table container
            // Add a CalendarMonthBody to the table container

            return this;
          }
        }));
All that this view should do is, starting with an unattached <table> tag, build up a table that a higher order object can insert a month view object appropriately.

If I remove all of that extra stuff, I am left with a much more focused view:
        var CalendarMonth = 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;
          }
        });
This has the side benefit of following last night's use of the precipitation pattern better as well (doing naught but appending the sub-view to the current view).

Since the calendar month view is no longer responsible for inserting itself in the DOM, something else has to do it. Following along with the precipitation pattern, I do this in my "Application" view, which is already responsible for establishing other views:
        var Application = Backbone.View.extend({
          // ...
          setDate: function(date) {
            this.collection.setDate(date);
            this.render();
          },
          render: function() {
            var date = this.collection.getDate();

            var month = new CalendarMonth({date: date});

            $(this.el).html(month.render().el);
          },
          // Other views here...
        });
And now, my highest order object in the Backbone stack, the router, needs to manipulate that application view:
      var Routes = Backbone.Router.extend({
        initialize: function(options) {
          this.application = options.application;
        },

        routes: {
          // ...
          "month/:date": "setMonth"
        },

        setMonth: function(date) {
          console.log("[setMonth] %s", date);
          this.application.setDate(date);
        }
      });
That feels better—a more cohesize top-down approach. I will call it a night here and pick back up with this refactoring tomorrow.


Day #186

No comments:

Post a Comment