Search code examples
javascriptjquerybackbone.jsbackbone-boilerplate

async template loading and post-render actions with Backbone


I'm using backbone boilerplate to render my templates, its fetchTemplate method caches the rendered templates.

I would like to run some extra code on the rendered content, like initialize accordions, etc, but to do this with an async compiled template is more tricky than I thought.

Here is an example:

Duel.Views.Home = Backbone.View.extend({
  template: "/templates/duel_home.jade",
  render: function() {
    var view = this;
    statusapp.fetchTemplate(this.template, function(tmpl) {
      $(view.el).html( tmpl({duels: view.collection.toJSON()}) );
      view.postrender();
    });
    return this;
  },
  postrender: function() {
    $('#duel-new').each(function() {
      console.log('Found something')
    });
  }
});

Beside the above I use a view handler as outlined at http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/

This I do something like

var view = Duel.Views.Home({model: mymodel})
viewHandler('#content').showView(view)

this calls

$('#content').html(view.render().el)

But what happens is that when the template is not cached yet, render is called first, and postrender is called on time. On the other hand, when the template is already cached, then the template is rendered immediately, postrender gets called, but view.el is not inserted in the DOM yet, thus $(this.el) is an empty list, and $('#duel-new').each() is "void".

Of course, I could add the postrender method after the viewHandler's render call, but this leads to the same problem, but on the first invocation of the render method. As the template is not compiled yet, postrender gets called before its elements would exist, thus no handlers could be defined on these non-existing elements.

Any ideas on how to properly overcome this problem? It's relatively straightforward for simple click events using .on for example, but what about more general structures, like $('#tabs').tabs() for example?

My fetchTemplate function is the following:

fetchTemplate: function(path, done) {
  window.JST = window.JST || {};

  // Should be an instant synchronous way of getting the template, if it
  // exists in the JST object.
  if (JST[path]) {
    return done(JST[path]);
  }

  // Fetch it asynchronously if not available from JST
  return $.get(path, function(contents) {
    var tmpl = jade.compile(contents,{other: "locals"});
    JST[path] = tmpl;

    return done(tmpl);
  });
},

Solution

  • There is no need for all these complications.

    The original fetchTemplate returns a jQuery promise. So should your version of it, if you don't know about jQuery's Deferreds and Promises it's a good time to look at them. Callbacks are dead ;)

    Using promises, everything get as simple as: In your initialize do fetch the template and assign the promise. Then render only when the promise has been fulfilled, for example:

    Duel.Views.Home = Backbone.View.extend({
        initialize: function () {
           this.templateFetched = statusapp.fetchTemplate(this.template);
    
        },
        ...
    
        render: function () {
            var view = this;
            this.templateFetched.done(function (tmpl) {
                view.$el.html( tmpl({duels: view.collection.toJSON()}) );
                ... // All your UI extras here...
            });
        }
    });
    

    Note that once the promise has been fulfilled, done will always simply run immediately. You can of course follow the same pattern if you modify your views $el outside the view, i.e. wrap the code in view.templatedFetched.done(...).