Search code examples
backbone.jsbackbone-views

Backbone.js: undelegateEvents not removing events


In the following code:

HTML

<div id="myView">
  <button id="test_button">
    Test Button
  </button>
  <ul id="output"></ul>
</div>

JavaScript

var myView = Backbone.View.extend({

    initialize: function() {
        // why doesn't this remove the previously delegated events?
        this.undelegateEvents();

        this.delegateEvents({
            'click #test_button': 'buttonClicked'
        });
    },

    // this event fires twice for one button click    
    buttonClicked: function() {
        $("#output").append('<li>Button was clicked</li>'); 
    }
});

$(document).ready(function(){
    new myView({el: "#myView"});    

  // instantiate view again
  new myView({el: "#myView"});  
});

why does

this.undelegateEvents();

in the initialize() method of the Backbone View not remove the previously delegated events from the previous instantiation of the View?

JSFiddle example of above code: https://jsfiddle.net/billb123/o43zruea/28/


Solution

  • I'll try not to shout but please stop trying to bind views to existing elements. Let the view create and own its own el, then call view.remove() to kill it off before replacing it. This simple change solves so many problems with view events that you should always think twice (and twice more) if you don't do it this way.

    In your case, you'd have HTML like this:

    <script id="t" type="text/x-underscore">
      <div id="myView">
        <button id="test_button">
          Test Button
        </button>
      </div>
    </script>
    <div id="container">
    </div>
    <ul id="output"> <!-- This is outside the container because we're going to empty and refill it -->
    </ul>
    

    And your JavaScript would look like this:

    var myView = Backbone.View.extend({
      events: {
        'click #test_button': 'buttonClicked'
      },
      render: function() {
        this.$el.html($('#t').html());
        return this;
      },
      buttonClicked: function() {
        $("#output").append('<li>Button was clicked</li>'); 
      }
    });
    
    $(document).ready(function(){
      var v = new myView();
      $('#container').append(v.render().el);
    
      v.remove(); // <----------------- Clean things up before adding a new one
    
      v = new myView();
      $('#container').append(v.render().el);
    });
    

    Points of interest:

    1. Create the view then render it then put it on the page.
    2. Call remove on the view when you're done with it.
    3. The view goes inside the container. The caller owns the container, the view owns its el.
    4. There are no delegateEvents or undelegateEvents calls anywhere. The presence of those almost always point to structural problems in your application IMO.
    5. Each view is self contained: the outside world doesn't play with anything inside the view and the view keeps its hands to itself.

    Updated fiddle: https://jsfiddle.net/bp8fqdgm/


    But why didn't your attempted undelegateEvents do anything? undelegateEvents looks like this:

    undelegateEvents: function() {
      if (this.$el) this.$el.off('.delegateEvents' + this.cid);
      return this;
    },
    

    The cid is unique per view instance so each view instance uses its own unique namespace for events that delegateEvents binds. That means that this:

    this.undelegateEvents();
    this.delegateEvents();
    

    is saying:

    1. Remove the events that this instance of the view has bound. These events will be found in the the '.delegateEvents' + this.cid namespace where cid is unique for each view instance.
    2. Bind the events that this instance of the view defines (or the events in the delegateEvents call). These events will be attached using the '.delegateEvents' + this.cid namespace.

    So your undelegateEvents call is removing events but not all of them, only the specific event bindings that that view instance adds are removed.

    Your this.undelegateEvents() call doesn't actually accomplish anything because it is in the wrong place and called at the wrong time. If the new View caller did the undelegateEvents call:

    var v = new myView({el: "#myView"});    
    v.undelegateEvents();
    new myView({el: "#myView"});
    

    then it would happen in the right place and at the right time. Of course this means that your router needs to keep track of the current view so that it can currentView.undelegateEvents() at the right time; but if you're doing that then you'd be better off (IMO) taking the approach I outlined at the top of the answer.