3

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);
  });
},
Dan O
  • 6,022
  • 2
  • 32
  • 50
Akasha
  • 2,162
  • 1
  • 29
  • 47
  • can you add the code for the fetchTemplate function? – Derick Bailey May 01 '12 at 17:45
  • Why is the view acting on elements outside its scope? There should be no `$("#some-id")` in your view, all the elements the view should act on should be under `this.el` or `this.el` itself. Also, can't you just do `$('#content').html(view.el); view.render();`? – Esailija May 07 '12 at 20:18

4 Answers4

5

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

ggozad
  • 13,105
  • 3
  • 40
  • 49
  • wow, this is something that I definitely should implement, I hope that it'll solve my initial problem too – Akasha May 12 '12 at 19:54
  • what do you mean by "the original `fetchTemplate`"? My code above returns the rendered template, not a promise, and after reading the jquery docs it seems to me that promises are parts of "dom level" jquery objects (`$("#myid").promise()`), not of jQuery itself (`$.promise`). – Akasha May 12 '12 at 20:14
  • Promises are returned from every async operation (such as a `jQuery.get`) The original fetchTemplate (https://github.com/tbranyen/backbone-boilerplate/blob/master/app/namespace.js) does that and so does yours actually since you return the result of $.get but only when it's fetched (not when it's already fetched! For a more comprehensive exaplanation see also http://addyosmani.com/blog/digging-into-deferreds-1/ – ggozad May 13 '12 at 00:04
2

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.

Parth Thakkar
  • 5,427
  • 3
  • 25
  • 34
  • and on the sidenote, if you wanted a more general way, you _could_ use Backbone.Events, but for this problem, it seems an overkill to me. – Parth Thakkar May 09 '12 at 13:54
  • and, @Esailija said, you should not use $("selector") from within your view, it creates confusion. But still, if it is unavoidable, my method, at least as it seems to me, will suffice. – Parth Thakkar May 09 '12 at 13:56
  • unfortunately, I'm doing exactly what you propose, even a bit more, as I call postrender twice. Once at the end of the callback inside myView.render, and once at the end of showView – Akasha May 10 '12 at 20:28
  • this works kind of properly, but is problematic when I need code that should be run exclusively once, and postrender is called twice with both calls having access to a meaningful DOM tree – Akasha May 10 '12 at 20:29
0

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')
    });
  }
});
Vincent Briglia
  • 3,068
  • 17
  • 13
  • just a question. what if "#duel-new" is not inside this.$el and it actually contains it? i am not so sure if that would work as far as i know - but anyways, I am no expert am i? :) – Parth Thakkar May 10 '12 at 15:43
  • You're right to say that if #duel-new is not inside the view that you're currently trying to render, the above code will not work. But you might end up in all sorts of pain trying to access DOM elements outside of the scope of the current view. If that's the case however, at that point it would make a lot more sense to either employ events to inform an upper-view to render/show/hide ... and if you're feeling particularly adventurous, one might even consider a statemachine. – Vincent Briglia May 10 '12 at 16:18
  • Take a look at the high-level explanation on Wikipedia: http://en.wikipedia.org/wiki/Finite-state_machine or this basic example http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/example/ – Vincent Briglia May 10 '12 at 16:24
  • thanks for your suggestions, I'll rewrite my code, unfortunately, this doesn't seem to solve my problem – Akasha May 10 '12 at 20:30
0
  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.

JMM
  • 26,019
  • 3
  • 50
  • 55