Friday, December 9, 2011

Pretty Backbone.js Views with Require.js

‹prev | My Chain | next›

I am continuing my efforts to extract individual Backbone.js classes out into require.js libraries.

First up tonight, I extract my router out into a library:
// public/javascripts/calendar/router.js
define(function(require) {
  var Backbone = require('backbone')
    , to_iso8601 = require('calendar/helpers/to_iso8601');

  return Backbone.Router.extend({ /* ... */ });
});
Here, I am using a bit of require.js syntactic sugar by requiring my libraries inside the define()'s anonymous function. Previously, I had been declaring the libraries as the first argument to define() and assigning them in the anonymous function:
define(['backbone', 'to_iso8601'],
function(Backbone, to_iso8601) {
  return Backbone.Router.extend({ /* ... */ });
});
The old way is a bit more compact, but I much prefer the syntactic sugar of the new way. It devotes a single line to a module / library declaration. Individual library dependencies are then specified one per line almost like a real language.

Where this really shines, however, is when there are a large numbers of libraries. For instance, my application class used to look like:
define(['jquery',
        'underscore',
        'backbone',
        'calendar/collections/appointments',
        'calendar/views/Application',
        'calendar/helpers/to_iso8601',
        'jquery-ui'],
function($, _, Backbone, Appointments, Application, to_iso8601) {
  return function(root_el) { /* ... */ };
});
I am trying my best to format that nicely, but it is really hard to visually map the vertically-challenged list of library paths to the corresponding horizontally challenged variable names.

Using the new format, I get:
define(function(require) {
  var $ = require('jquery')
    , _ = require('underscore')
    , Backbone = require('backbone')
    , Router = require('calendar/router')
    , Appointments = require('calendar/collections/appointments')
    , Application = require('calendar/views/Application')
    , to_iso8601 = require('calendar/helpers/to_iso8601');

  require('jquery-ui');

  return function(root_el) { /* ... */  };
});
Ahhh... much better. Big thanks to James Burke for turning me onto to that format.

Anyhow, the last thing up in my refactoring escapade is extracting two Singleton Views:
  return function(root_el) {
    var Views = (function() {
      var AppointmentEdit = new (Backbone.View.extend({ /* ... */ }));
      var AppointmentAdd = new (Backbone.View.extend({ /* ... */ }));
    })();
    // ...
  }
Extracting them into libraries is little more than copying and pasting those two classes into two separate files:
// public/javascripts/calendar/views/AppointmentAdd.js
define(function(require) {
  var $ = require('jquery')
    , Backbone = require('backbone');

  return new (Backbone.View.extend({ /* ... */ }));
});
And:
// public/javascripts/calendar/views/AppointmentEdit.js
define(function(require) {
  var $ = require('jquery')
    , Backbone = require('backbone');

  return new (Backbone.View.extend({ /* ... */ }));
});
I am returning anonymous objects in both, hence the name Singleton (or Instantiated) view. I am doing this because I only want a single instance of each. Both will tie themselves to jQuery-UI dialogs that are hidden, not destroyed, when not in use. I do not want to bother recreating the dialog each time, which is why I am using the singleton view.

I had worried that it would be a problem to use these, but it turns out to be a simple matter of requiring the instantiated view and then using it normally:
define(function(require) {
  var Backbone = require('backbone')
    , _ = require('underscore')
    , to_iso8601 = require('calendar/helpers/to_iso8601')
    , AppointmentAdd = require('calendar/views/AppointmentAdd');

  return Backbone.View.extend({
    // ...
    addClick: function(e) {
      console.log("addClick");

      AppointmentAdd.reset({startDate: this.el.id});
    }
  });
});
I think that it would be a problem if I were trying to use these instantiated views across different view classes. Each calling context would have its own require(), which (I think) would cause different anonymous objects to be instantiated. If that were the case, I might have a problem on my hands.

In practice, however, I do not think it a common use case to require() an instantiated view across views. I think I can live with this.

That concludes my refactoring into require.js. My primary Backbone class now has been reduced from a whopping 571(!) lines all the way down to 33:
define(function(require) {
  var $ = require('jquery')
    , _ = require('underscore')
    , Backbone = require('backbone')
    , Router = require('calendar/router')
    , Appointments = require('calendar/collections/appointments')
    , Application = require('calendar/views/Application')
    , to_iso8601 = require('calendar/helpers/to_iso8601');

  require('jquery-ui');

  return function(root_el) {
    var year_and_month = to_iso8601(new Date()).substr(0,7)
      , appointments = new Appointments([], {date: year_and_month})
      , application = new Application({
          collection: appointments,
          el: root_el
        });

    new Router({application: application});
    try {
      Backbone.history.start();
    }
    catch (x) {
      console.log(x);
    }

    return {
      application: application,
      appointments: appointments
    };
  };
});
It is definitely much easier to figure out what is going on without wading through 500+ lines that are better maintained elsewhere. Up tomorrow, I plan to explore what this has done to my jasmine tests.


Day #230

7 comments:

  1. From the above:

    > I think that it would be a problem if I were trying to use these instantiated views across different view classes. Each calling context would have its own require(), which (I think) would cause different anonymous objects to be instantiated. If that were the case, I might have a problem on my hands.

    While each factory function gets its own require() function, it used mostly just to know now to resolve relative dependencies, but they all pull module exports from the same list of module exports.

    So, the return value from a factory function acts like a singleton -- there is only one value, the same value no matter how many other modules require it.

    ReplyDelete
    Replies
    1. if the return value from a factory function acts like a singleton, why am i and others having issues with 'ghost' or 'zombie' views?

      http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/
      http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/

      Delete
  2. These are really great tutorials. As a reader of your book, I am hoping that you are planning on updating the namespacing and packaging chapters to use RequireJS. I'm just starting out with RequireJS and Backbone but I'm already seeing a huge productivity boost.

    One comment:

    > I think that it would be a problem if I were trying to use these instantiated views across different view classes. Each calling context would have its own require(), which (I think) would cause different anonymous objects to be instantiated. If that were the case, I might have a problem on my hands.

    From what I have tested, this isn't the case: RequireJS caches each module internally to be served up whenever it is require'd. As a result, the line

    var AppointmentAdd = require('calendar/views/AppointmentAdd')

    would return the same object in every module you require it.

    Whilst this is a good thing for your singleton views, it is something to bear in mind for Models and Collections. To begin with, I was returning new Model() rather than Model from my Model modules, as I thought it would save time - until I realised that due to RequireJS caching the new Model, I could never have more than one Model in my app... It's therefore important to always just return the raw Backbone class from any module that you wish to use in a non-singleton pattern and instantiate what you need when you are using the module itself. Otherwise you could end up in a situation where you are trying to save 10 Models but only 1 is ever saved...

    ReplyDelete
  3. Ha, should have read James' comment first. What he said. :)

    ReplyDelete
  4. @Tom don't tell anyone, but this is for an entirely new recipe. But it's still a secret at this point, so sssh!

    Yah, I was pretty excited when James said that singletons would work. This is pretty cool stuff!

    ReplyDelete
  5. @Chris my lips are sealed...

    I have another question. I like the idea from your book of Dependency Injection when creating Views. I think it is a nice pattern to inject el into the view when it is instantiated, rather than explicitly define el in the view, to promote decoupling.

    The problem is, with RequireJS I don't see how this will play nicely with the InstantiatedView. The View is created within its View module and then instantly returned, and to separate concerns I'd prefer not to reference el at this point - I'd prefer to inject el from the Router where the view is being set up.

    My solution so far (short of giving up and explicitly defining el in the module) is a setEl() function on the Backbone View to set el once the View module has been required. I wonder what your thoughts are on this?

    ReplyDelete
  6. @Tom Something along the lines of setEl() or some other convenience method that sets the element and does some setup sounds like a reasonable approach.

    The use-case that I used to explore instantiated views was jQuery-UI dialogs. With those, I didn't need to inject elements -- just the model being updated by the form. The el auto-created by Backbone worked brilliantly for that and, once created, I could hide/show and simply reset with a different model in between dialog calls.

    See https://github.com/eee-c/Funky-Backbone.js-Calendar/blob/requirejs/public/scripts/Calendar/Views.AppointmentEdit.js and https://github.com/eee-c/Funky-Backbone.js-Calendar/blob/requirejs/public/scripts/Calendar/Views.Appointment.js for examples.

    ReplyDelete