async template loading and post-render actions with Backbone

6k views Asked by At

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);
  });
},
4

There are 4 answers

3
ggozad On BEST ANSWER

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(...).

4
Parth Thakkar On

I read the article to which you gave the link - the Zombies one. Nice it was and as far as I can see, it already contains the answer to your question, all that is needed is it to be searched. What I can think after reading and re-reading your question several times is that you may like to use the .NET way (as suggested in the Zombies article). That is, something like:

// in showView method of viewHandler
if (this.currentView) {
    this.currentView.close();
}

this.currentView = view;
this.elem.html( this.currentView.render().el );
if ( this.currentView.onAddToDom )  // THIS IS THE IMPORTANT PART.
    this.currentView.onAddToDom();

In the view, you add an 'onAddToDom' method which will be called as and when your view is added to the dom. This can be used to call the postrender() method or you may rename postrender() to 'onAddToDom()'. This way, the problem is solved. How? Explanation follows.

You can redefine your view as:

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()}) );
    });
    return this;
  },
  onAddToDom: function() {
    $('#duel-new').each(function() {
      console.log('Found something')
    });
  }
});

Now when you do something like

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

this gets called

$('#content').html(view.render().el)
if(view.onAddToDom)
    view.onAddToDom();

which will call (what was previously known as) postrender() method.

Problem solved.

Warning: But mind well, this will fail (that is, onAddToDom -- or shall we call postrender()? -- won't be called ) if view.render() is called directly and not from within viewHandler('selector').showView since we call the onAddToDom from within that. But anyways, this is not needed, since if we wanted something to be called after rendering, we could add that to the render() method itself. I just wanted to make sure there wasn't any confusion, so gave this warning.

0
JMM On
  1. What version of Backbone are you using?

  2. Are you using jQuery, zepto, or something else? What version?

  3. The problem only happens when the template is already cached and not being retrieved asynchronously? If that's the case, can you create an example in a jsfiddle?

  4. What browser(s) is the problem occurring in?

  5. thus $(this.el) is an empty list, and $('#duel-new').each() is "void"

    Please define exactly what you mean by "empty list" and "void". Unless something is seriously screwed up, with jQuery $( this.el ) should be a jQuery object with length 1. With jQuery $( '#duel-new' ).each() should be a jQuery object, possibly with length 0.

    As @20100 mentioned, if your Backbone version supports it you're better off using this.$el instead of $( this.el ).

  6. this calls

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

    jQuery.html() is only documented as accepting a string argument, so I don't think this is a good idea if using jQuery.

  7. This I do something like

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

    Shouldn't this be new Duel.Views.Home( { model : mymodel } )? Otherwise, inside the constructor, this will be Duel.Views.

4
Vincent Briglia On

can you try changing the marked lines:

view.$el is the same as creating your own $(view.el), this is syntactic sugar in Backbone 0.9.0+

$('#duel-new') scours over the whole dom-tree whereas $('#duel-new', this.$el) only checks within the scope of your current view, largely reducing the amount of time spent on DOM traversal.

whilst this may not necessary fix your peculiar and particular issue, I've not had any issues myself with

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()}) ); // change this
      view.postrender();
    });
    return this;
  },
  postrender: function() {
    $('#duel-new', this.$el).each(function() { // change this
      console.log('Found something')
    });
  }
});