Monday, December 12, 2011

Loading Backbone.js Templates with Require.js

‹prev | My Chain | next›

I am nearing the end of my require.js exploration, so today I take some time to play with one of the supported plugins. Specifically, I would like to see if I can make use of the text plugin to extract the templates in my Backbone.js application out into standalone template files.

First up, I need to download the plugin. It needs to be installed in the same directory as the data-main entry library for my Backbone application. I am following the require.js convention of naming that entry library as main.js, which is located in the public/javascript directory:
➜  javascripts git:(requirejs) ✗ ls -1F                                                       
backbone.js
calendar/
calendar.js
css/
jquery-1.6.2.min.js
jquery.min.js@
jquery-ui-1.8.16.custom.min.js
jquery-ui.min.js@
main.js
require.js
text.js
underscore.js
In that same directory, I wget the plugin:
➜  javascripts git:(requirejs) wget http://requirejs.org/docs/release/1.0.2/comments/text.js
The first place that I would like use the text plugin is in the Appointment view of my application. The template in here is rather smallish, but still ought to suffice to see how this works:
define(function(require) {
  var Backbone = require('backbone')
    , _ = require('underscore')
    , template = require('calendar/helpers/template')
    , AppointmentEdit = require('calendar/views/AppointmentEdit');

  return Backbone.View.extend({
    template: template(
      '<span class="appointment" title="{{ description }}">' +
      '  <span class="title">{{title}}</span>' +
      '  <span class="delete">X</span>' +
      '</span>'
    ),
    // ...
  });
});
I extract that string template into a new Apppointment.html template:
// public/javascripts/calendar/views/Appointment.html
<span class="appointment" title="{{ description }}">
  <span class="title">{{title}}</span>
  <span class="delete">X</span>
</span>
I am storing that in the javascripts directory, which feels a bit wrong. I may rename that from "javascripts" to plain "scripts". Then I might rationalize that this is a script/template.

I briefly flirted with the idea of creating a public/templates directory. I rejected that idea on the basis that this template is part of my calendar Backbone application. As such, it ought to reside with the rest of the application files.

To pull that template into my Appointment view, I cannot do a simple require(), which normally works with Javascript source files. Instead, I have to signal to the require() statement that I want to use the newly installed text plugin. I do this with the addition of the string text! to my require() statement:
define(function(require) {
  var Backbone = require('backbone')
    , _ = require('underscore')
    , template = require('calendar/helpers/template')
    , html = require('text!calendar/views/Appointment.html')
    , AppointmentEdit = require('calendar/views/AppointmentEdit');
  
  return Backbone.View.extend({
    // ...
  });
});
In the class definition, I can then use the html just as if it was the HTML string that it is replacing:
define(function(require) {
  var Backbone = require('backbone')
    , _ = require('underscore')
    , template = require('calendar/helpers/template')
    , html = require('text!calendar/views/Appointment.html')
    , AppointmentEdit = require('calendar/views/AppointmentEdit');

  return Backbone.View.extend({
    template: template(html),
    // ...
  });
});
I do have a bit of a naming clash taking place in here. The template helper is a thin wrapper around the templater built into to underscore.js. In this case, my thin wrapper lets me do mustache-style templating. I have the feeling that, upon reading this code in 6 months, it will take me a while to decide if template is a function, the HTML string template, or the result of applying the template function to the template.

I think some more effective naming should help:
define(function(require) {
  var Backbone = require('backbone')
    , _ = require('underscore')
    , mustache_templater = require('calendar/helpers/template')
    , html_template = require('text!calendar/views/Appointment.html')
    , template = mustache_templater(html_template)
    , AppointmentEdit = require('calendar/views/AppointmentEdit');

  return Backbone.View.extend({
    template: template,
    // ...
  });
});
Now it is pretty clear that I am grabbing the mustache_templater function and the html_template. The only place that either of these two beasties is used occurs on the very next line, where the template variable is assigned to the result of my mustache templater interpreting the HTML template. I can then pass that template directly to the template attribute in my class. Any of the code that that might cause confusion is collected in the same place (at the top of the library). As a result, the class definition is almost trivial.

Last up, I check to make sure that appointments are still showing up in my application:


And, just to be certain that I am loading the template from the file system and not caching anything, I use Chrome's network resources inspector to see that the new template is, indeed, loading:


Nice! The only drawback that I can see to this approach is that my jasmine tests break. If I run from the filesystem, I get a cross-domain error:


Despite the text of the message, this is because I am making XHR requests (that's how the text plugin does its magic) of the file system. That simply will not work. Switching to the jasmine server is non trivial because it is not require.js aware (i.e. it does not supply a data-main attribute when loading require.js).

I will ruminate on that a bit. For now, I call it a night.


Day #233

2 comments:

  1. Chris, thanks for the informative posts. They convinced me to purchase your book last night to support further development which so far I am enjoying. Unfortunately the book covers both the advantages of Jasmine and of Require.js but we still run into the problems you've outlined above if trying to use both (which I would assume would be a fairly common use case).

    I was curious, have you resolved your cross-domain error while using the text! require.js plugin with Jasmine? We are trying to do exactly the same thing and was curious if you had any guidance.

    Thanks again for sharing your thoughts and insights via your prolific bloggin and books, speaking for myself and a few co-workers we all are better for them.

    -JRN

    ReplyDelete
    Replies
    1. Justin,

      Sorry, but I never did get back to this :(

      Unfortunately, it's not a problem that I normally run into -- I like keeping my templates small and inside the views themselves. I did somewhat address this with later posts on getting require.js and phantomjs working together: http://japhr.blogspot.com/2011/12/phantomjs-and-backbonejs-and-requirejs.html. Unfortunately, I did not specifically go back and try this out with the text plugin.

      The best that I can say is that it ought to work. Hope that is of some use.

      -Chris

      Delete