14

I have a render method in Backbone that goes basically like this:

render: function () {
  $.tmpl(this.template, attrs).appendTo(this.el);
  return this;
},

which is called from a router action:

action: function () {
  $('#container').empty();
  $('#container').append(myView.render().el);
},

Now, I want to apply a plugin on label elements inside this view. My first thought was to call the plugin inside render:

render: function () {
  $.tmpl(this.template, attrs).appendTo(this.el);
  this.$('label').inFieldLabels();
  return this;
},

but this doesn't work (I'm assuming this is because the element hasn't been added to the DOM yet). It does work if I call the plugin in the router action though:

action: function () {
  $('#container').empty();
  $('#container').append(myView.render().el);
  myView.$('label').inFieldLabels();
},

I'd rather not do this, because the plugin is part of the view, not the router, so it doesn't make sense to be calling it inside the action. Is there a better way to do this?

Skilldrick
  • 69,215
  • 34
  • 177
  • 229

5 Answers5

5

Beter do it this way:

action: function () {
    var container = $('#container');

    container.empty();
    myView.render(container);
},

render: function (container) {
    $(this.el)
        .append($.tmpl(this.template, attrs))
        .appendTo(container);
    $('label', this.el).inFieldLabels();
    return this;
},
ant_Ti
  • 2,385
  • 16
  • 14
  • 1
    Thanks - that's essentially what I mean by "modify the code so the element gets added to the DOM in the `render` method". Incidentally, you can just do `this.$('label')`, which is shorthand for `$('label', this.el)`. – Skilldrick Aug 18 '11 at 15:01
5

I ran into a similar issue setting up the jQuery Tools Tooltip plugin. But I had a much different approach that works well: triggering a custom event on the view. As far as I know, there is no 'inserted into dom' event fired built into Backbone, so I just did it myself. I don't use a Router but the modified code above would look something like this:

// In the router:
action: function () {
    var container = $('#container');

    container.append(myView.render().el));
    myView.trigger('rendered');
},

// In the view:
initialize: function() {
    this.bind('rendered', this.afterRender, this);
},
render: function (container) {
    $(this.el).append($.tmpl(this.template, attrs))
    return this;
},
afterRender: function() {
    $('label', this.el).inFieldLabels();
}

I suppose the advantage is that the view stays ignorant of its container. The disadvantage is that it's like an extra line or two of code. But if you need to setup a lot of plugins or do more stuff that requires the element to be in the DOM, it will work well (and it keeps logic separated).

I actually like @Skilldrick's method of passing the container to the view, but I still feel as if it makes sense to have the parent view be responsible for inserting the children. I'm new to Backbone so please feel free to comment.

Matt De Leon
  • 747
  • 1
  • 7
  • 15
  • It's an interesting idea, but I don't like the idea of the router having to tell the view that it's been rendered. That just fundamentally seems like something the view should know about! – Skilldrick Aug 25 '11 at 13:32
  • It's a good compromise if you want to keep the container element out of the view though. – Skilldrick Aug 25 '11 at 13:33
  • Right, it's a tradeoff. I don't really feel a pull towards one way or the other. I'll comment again if something comes my way as I run my app. – Matt De Leon Aug 25 '11 at 18:24
  • Hey! What is the advantage to call myView.trigger('rendered') instead of myView.afterRender()? – Pablo Cantero Mar 24 '12 at 20:29
  • Well, it's good practice for code outside of myView to not be too aware of what's happening inside of myView. Technically, both of those would achieve the same thing, but what if we have multiple methods in myView that need to be run after rendering? E.g. setupTooltip and setupDialog. Then we'd have to rewrite all of our code wherever we call afterRender. Instead, I'm choosing to trigger an event called 'rendered' and then myView can choose to do whatever it likes when receiving that event. – Matt De Leon Mar 25 '12 at 17:22
2

I was able to get this to work for my jQuery plugin only by specifying the context for jQuery in the render function:

    render: function () {
    $(this.el).html(this.template(this.model.toJSON()));
    $('#email1dropdown', this.el).dropdownify();

    return this;
},
Todd H. Gardner
  • 630
  • 4
  • 18
0

I've 'solved' this problem by adding a loadPlugins method to my view, so I can do myView.loadPlugins() after rendering in the action. It's not ideal, but it works.


Edit: Well, I've heard from the horse's mouth and it looks like I can't apply the plugin before the element's been added to the DOM, so either I can do as above (with loadPlugins) or I can modify the code so the element gets added to the DOM in the render method. Hope this helps someone in a similar position.

Here's how I'm doing it now:

//in router
action: function () {
  var myView = new MyView({
    el: '#container'
  });
}

//in view
render: function () {
  $.tmpl(this.template, attrs).appendTo(this.el); //this now appends to the DOM
  this.loadPlugins();
  return this;
},

loadPlugins: function () {
  this.$('label').inFieldLabels();
  //and other plugins
},
Skilldrick
  • 69,215
  • 34
  • 177
  • 229
0

I was facing a similar issue but in my case even the above solutions wouldn't work because the parent views hadn't been added to the DOM yet.

Sticking with the convention of having the parent view add the children to the DOM, doing loadPlugins() in the render method didn't work.

This is what I did. It feels kind of hacky but could help, depending on how you're managing your views throughout the hierarchy:

render: function() {
  var self = this;

  setTimeout(function() {
    $(self.el).setupPlugin();
  }, 0);

  return this;
}

Using a setTimeout of 0 allows the current call stack to finish, so by the time the timeout function gets called the view and all it's parents have been added to the DOM. (If you stick to the convention mentioned above).

evilcelery
  • 15,941
  • 8
  • 42
  • 54
  • It would be better if there was a way of attaching an event listener that fires when a certain element has actually been added to the DOM. Does anyone know of a way of doing this? – evilcelery Aug 22 '11 at 16:01
  • Yes, I'd been thinking of doing a setTimeout as well - it'll definitely work but it does feel horribly hacky as well. – Skilldrick Aug 23 '11 at 15:12
  • It looks like there *are* events for when [an element has been added](http://en.wikipedia.org/wiki/DOM_events) e.g. `DOMNodeInserted`, but they're not supported by IE. As always. Some more info: http://stackoverflow.com/questions/2143929/domnodeinserted-equivalent-in-ie – Skilldrick Aug 23 '11 at 15:14
  • 1
    Don't forget that [`_.defer`](http://documentcloud.github.com/underscore/#defer) is available, since Backbone depends on Underscore. – Matthew Mar 21 '12 at 01:03