25

[EDIT: I solved earlier problem by calling delegateEvents(), but shouldn't have to. Re-posting w/more info.]

I have a View that when rendered has a login button on it with a click event attached. First render all works: click on button and upon successful ajax call the login prompts disappear and welcome view (LoggedInView) is displayed. But, if I navigate back to this View later (#foo) the UI renders but the event association is gone without manually forcing the issue by calling delegateEents().

What happened that my events didn't re-associate themselves?

LoginView = Backbone.View.extend({
    template: _.template($("#loginTemplate").html()),
    initialize: function(stuff,router) {
        _.bindAll(this,'render','rc','gotProfile');
        this.model.bind("rc",this.rc)
        this.router = router;
    },
    events: {
        'click .loginButton': 'login'
    },  
    render: function() {
        $(this.el).html(this.template(this.model.toJSON()));
        $(this.el).find(".actionButton").button();  // Button
//      this.delegateEvents(this.events);  // NEEDED!  Re-wire events on re-render
        return this;
    },
    rc: function(code) {
        switch(code) {
            case errCodes.USER_NOT_FOUND:   this.notFound(); break;
            case errCodes.OK:               this.loginOk(); break;
            default:                            this.otherErr(); break;
        }
    },
    login: function() {
        clearErrors( $('[rel="req"]') );
        var okReq = validate( $('#login [rel="req"]'), validateReq );
        var okEmail = validate( [$('#uid')], validateEmail );
        if( okReq && okEmail ) {
            this.model.set({'uid':$('#uid').val().trim(),     'pwd':$('#pwd').val().trim()});
            this.model.fetch();
        }
    },
    notFound: function() {
        validate( [$('#uid'),$('#pwd')], function(){return[false,"Invalid user / password"]} );
    },
    otherErr: function() {
        validate( [$('#uid'),$('#pwd')], function(){return[false,"Please contact support for help logging in."]} );
    },
    loginOk: function() {
        this.profile = new Profile({uid:this.model.get('uid'),token:this.model.get('key')});
        this.profile.bind("rc",this.gotProfile)
        this.profile.fetch();
    },
    gotProfile: function() {
        this.router.navigate('/',true);
    }
});

LoggedInView = Backbone.View.extend({
    template: _.template($("#loggedInTemplate").html()),
    uList: new ProfileList(),
    initialize: function() {
        _.bindAll(this,'render','renderUserList','renderUser');
        this.model.bind('show', this.render);
        this.uList.bind('rc', this.render);
    },
    events: {
        'click #ufa': 'getUsersForHouse'
    },
    render: function() {
        $(this.el).html(this.template(this.model.toJSON()));
        this.renderUserList();
//      this.delegateEvents(this.events); // NEEDED!  Re-wire events on re-render
        return this;
    },
    renderUserList: function() {
        $(this.el).find('ul').empty();
        this.uList.each(this.renderUser);
    },
    renderUser: function(aUser) {
        $(this.el).find('#userList').append("<li>"+aUser.get('person').firstName+"</li>");
    },
    getUsersForHouse: function() {
        this.uList.fetch(this.model.get('token'),"house");
    }
});

Main = Backbone.Router.extend({
    routes: {
        'foo': 'foo',
        '*all': 'home'
    },
    initialize: function() {
        this.token = new Token();
        this.loginView = new LoginView({model:this.token},this);
    },
    foo: function(){  // naving here looses click event on login button
        $('#login').empty();
        $("#login").append(this.loginView.render().el);
    },
    home: function() {
        $('#login').empty();
        if( this.loginView.profile == null ) 
            $("#login").append(this.loginView.render().el);
        else {
            this.loggedInView = new LoggedInView({model:this.loginView.profile});
            $("#login").append(this.loggedInView.render().el);
        }
    }
});
Chris Seymour
  • 83,387
  • 30
  • 160
  • 202
Greg
  • 10,696
  • 22
  • 68
  • 98
  • you need to supply more information to know what's going on and why. can you post more of your view's code, and your router code? – Derick Bailey Sep 08 '11 at 13:50
  • Here's my Router and affected View: Main = Backbone.Router.extend({ routes: { '': 'home', 'ok': 'loggedIn' }, initialize: function() { this.token = new Token(); this.loginView = new LoginView({model:this.token},this); }, home: function() { $('#login').empty(); $("#login").append(this.loginView.render().el); }, loggedIn: function() { this.loggedInView = new LoggedInView({model:this.loginView.profile}); $('#login').empty(); $("#login").append(this.loggedInView.render().el); } }); – Greg Sep 08 '11 at 17:27
  • I can't added the View--too big for this editor to accept. The LoginView has the events clause. – Greg Sep 08 '11 at 17:29
  • can you edit your original post to include that code? it's not really readable / understandable as a comment – Derick Bailey Sep 08 '11 at 17:52
  • 2
    Ah... I see. Didn't notice the edit link...thank yoy. I did find a solution though. Looking at the backbone code there is a function delgateEvents() that wires these up, and it seems to only be called on View creation. If I call this manually on re-render everything works. Would've been kinda need if that was wired into an event someplace but I can easily do that in my code. – Greg Sep 08 '11 at 19:31
  • @Greg Did you manage to solve this? I got the latest version of Backbone event that has this problem. I tried calling `this.delegateEvents(this.events)` inside the view render but no hope. – Deeptechtons Jul 03 '12 at 10:19

4 Answers4

19

You empty the the #login div. As the jQuery doc says:

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 are effectively removing events from your views. I prefer to use detach because it keeps events. http://api.jquery.com/detach/

You can implement a show/hide in your views that will deal with this.

Julien
  • 9,216
  • 4
  • 37
  • 38
2

I found a solution to this, because I was having the same problem, actually detach() is not the solution because it will bring back the old DOM elements for you which is not useful.

this is what you need to do

render: function(){ this.$el.empty();  ......  this.$el.appendTo($DOM);   }

if you use the .html() method your events will not trigger. you need to attach the newly created HTML to the DOM, by using appendTo().

by the way this only happened to me if you are using tagName and className to build your view dynamically.

Mahdi Pedram
  • 689
  • 6
  • 12
1

I'm facing the same problem. The way I solved it for now is completely re- instantiating the View object with new MyView() this ensures that the events are rebound.

Hope that helps?

amit
  • 1,991
  • 1
  • 18
  • 29
1

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 the following for further reference: http://stackoverflow.com/a/12029250/2728686 and http://www.jquerybyexample.net/2012/05/empty-vs-remove-vs-detach-jquery.html – AmpT Mar 12 '14 at 12:52