38

I have super-View who is in charge of rendering sub-Views. When I re-render the super-View all the events in the sub-Views are lost.

This is an example:

var SubView = Backbone.View.extend({
    events: {
        "click": "click"
    },

    click: function(){
        console.log( "click!" );
    },

    render: function(){
        this.$el.html( "click me" );
        return this;
    }
});

var Composer = Backbone.View.extend({
    initialize: function(){
        this.subView = new SubView();
    },

    render: function(){
        this.$el.html( this.subView.render().el );             
    }
});


var composer = new Composer({el: $('#composer')});
composer.render();

When I click in the click me div the event is triggered. If I execute composer.render() again everything looks pretty the same but the click event is not triggered any more.

Check the working jsFiddle.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
fguillen
  • 36,125
  • 23
  • 149
  • 210

3 Answers3

66

When you do this:

this.$el.html( this.subView.render().el );

You're effectively saying this:

this.$el.empty();
this.$el.append( this.subView.render().el );

and empty kills the events on everything inside this.$el:

To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.

So you lose the delegate call that binds events on this.subView and the SubView#render won't rebind them.

You need to slip a this.subView.delegateEvents() call into this.$el.html() but you need it to happen after the empty(). You could do it like this:

render: function(){
    console.log( "Composer.render" );
    this.$el.empty();
    this.subView.delegateEvents();
    this.$el.append( this.subView.render().el );             
    return this;
}

Demo: http://jsfiddle.net/ambiguous/57maA/1/

Or like this:

render: function(){
    console.log( "Composer.render" );
    this.$el.html( this.subView.render().el );             
    this.subView.delegateEvents();
    return this;
}

Demo: http://jsfiddle.net/ambiguous/4qrRa/

Or you could remove and re-create the this.subView when rendering and sidestep the problem that way (but this might cause other problems...).

mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • I would never have found it by my self. It is a little insane :/. Thanks @mu – fguillen Aug 19 '12 at 19:34
  • @fguillen: I had to track down something similar and I think it involved a trip through the jQuery source and a fair bit of experimentation. The effort has permanently burned this into my memory. – mu is too short Aug 19 '12 at 20:00
  • This has led me to proper direction to fix my issue. Thank you @muistooshort – Amiga500 May 10 '15 at 19:15
  • 1
    @muistooshort similar problem but delegateEvents is not helping. http://jsfiddle.net/ismusidhu/kt9zr5z9/7/ – IsmailS Sep 16 '15 at 16:41
  • @IsmailS: What does your `render` call do to the view's `this.img` property? Here's a hint: it works if you use `this.$('#abc')` instead of trying to cache `this.img`. Toss a `console.log('clicked')` in your click handler and you'll see that you don't have an event problem. – mu is too short Sep 16 '15 at 18:40
11

There's a simpler solution here that doesn't blow away the event registrations in the first place: jQuery.detach().

http://jsfiddle.net/ypG8U/1/

this.subView.render().$el.detach().appendTo( this.$el );   

This variation is probably preferable for performance reasons though:

http://jsfiddle.net/ypG8U/2/

this.subView.$el.detach();

this.subView.render().$el.appendTo( this.$el );

// or

this.$el.append( this.subView.render().el );

Obviously this is a simplification that matches the example, where the sub view is the only content of the parent. If that was really the case, you could just re-render the sub view. If there were other content you could do something like:

var children = array[];

this.$el.children().detach();

children.push( subView.render().el );

// ...

this.$el.append( children );

or

_( this.subViews ).each( function ( subView ) {

  subView.$el.detach();

} );

// ...

Also, in your original code, and repeated in @mu's answer, a DOM object is passed to jQuery.html(), but that method is only documented as accepting strings of HTML:

this.$el.html( this.subView.render().el );

Documented signature for jQuery.html():

.html( htmlString )

http://api.jquery.com/html/#html2

JMM
  • 26,019
  • 3
  • 50
  • 55
  • 1
    Very interesting, _simpler solution_ is not the world that I'd use but it is really good to know. Thanks – fguillen Aug 21 '12 at 07:46
  • I guess whether it's "simpler" from the perspective of authoring the view classes is debatable. I think it's probably more efficient though since you're retaining the event listener registrations instead of re-applying them each time. If it's possible that you'll also have data stored with the element via `jQuery.data()` that you want to preserve upon re-rendering, that's a consideration too, as `detach()` will preserve the data without additional code. – JMM Aug 21 '12 at 21:42
0

When using $(el).empty() it removes all the child elements in the selected element AND removes ALL the events (and data) that are bound to any (child) elements inside of the selected element (el).

To keep the events bound to the child elements, but still remove the child elements, use:

$(el).children().detach(); instead of $(.el).empty();

This will allow your view to rerender successfully with the events still bound and working.

AmpT
  • 2,146
  • 1
  • 24
  • 25
  • Please see these for further reference: http://stackoverflow.com/a/7417078/2728686 and http://www.jquerybyexample.net/2012/05/empty-vs-remove-vs-detach-jquery.html – AmpT Mar 12 '14 at 12:51