3

I have a chunk of markup in my page that represents a view, and a JS controller function which is associated with that view. (These are Angular, but I don't believe that matters.) The controller code listens for a custom event fired from elsewhere in the app, and handles that event with some controller-specific logic.

My problem is that the controller's event handler is getting attached too many times: it gets attached every time the view is re-activated, resulting in the handler being run multiple times every time the custom event is fired. I only want the handler to run once per event.

I've tried using .off() to unbind the handler before binding it; I've tried .one() to ensure that the handler is only run once; and I've tried $.proxy() after reading about its interaction with .off() here.

Here's a sketch of my code:

// the code inside this controller is re-run every time its associated view is activated
function MyViewController() { 

    /* SNIP (lots of other controller code) */

    function myCustomEventHandler() {
        console.log('myCustomEventHandler has run');
        // the code inside this handler requires the controller's scope
    }

    // Three variants of the same misbehaving event attachment logic follow: 

    // first attempt
    $('body').off('myCustomEvent', myCustomEventHandler);
    $('body').on('myCustomEvent', myCustomEventHandler);
    // second attempt
    $('body').one('myCustomEvent', myCustomEventHandler);
    // third attempt
    $('body').off('myCustomEvent', $.proxy(myCustomEventHandler, this));
    $('body').on('myCustomEvent', $.proxy(myCustomEventHandler, this));
    // all of these result in too many event attachments

};

// ...meanwhile, elsewhere in the app, this function is run after a certain user action
function MyEventSender() {
    $('body').trigger('myCustomEvent');
    console.log('myCustomEvent has been triggered');
};

After clicking around in my app and switching to the troublesome view five times, then doing the action which runs MyEventSender, my console will look like this:

myCustomEvent has been triggered
myCustomEventHandler has run
myCustomEventHandler has run
myCustomEventHandler has run
myCustomEventHandler has run
myCustomEventHandler has run

How can I get it to look like this:

myCustomEvent has been triggered
myCustomEventHandler has run

???

10
  • Can you just make your controller not do anything if it's not visible? Doesn't fix your bug, but it's a different way to approach the problem Commented May 10, 2013 at 17:31
  • As far as I know, the Angular event cycle and jQuery event cycle are completely unrelated (except for the fact that they both hook into the DOM). You should attach your event to the <body> tag outside of your controller, in a $(function() { ... }) call. Commented May 10, 2013 at 17:33
  • Works for me: jsbin.com/adecul/1/edit Commented May 10, 2013 at 17:34
  • @KevinB Did you switch to the troublesome view a few times? Actually I just tried, stil just one... Commented May 10, 2013 at 17:37
  • I clicked run several times, it always gives one run and one triggered Commented May 10, 2013 at 17:38

3 Answers 3

3

Give your events a namespace, then simply remove all events with said namespace when you re-run the controller.

jsbin

$('body').off('.controller');
$('body').on('myCustomEvent.controller', myCustomEventHandler);
Sign up to request clarification or add additional context in comments.

7 Comments

This can be easier then figuring out which proxied handlers to unregister
See my answer for an explanation of why the OP's code wasn't unregistering
This doesn't work: jsbin.com/adecul/17/edit :( I still see "myCustomEventHandler has run" x7
get rid of the handler when unbinding. jsbin.com/adecul/18/edit If you want to pass the handler, you'll ahve to have one line for each event binding that you do which doesn't seem very maintainable to me.
OK, it works after removing the handler, and I agree with your point. Thanks for the answer!
|
2

You could listen in on the scope destroy event in your Main controller

function MyViewController($scope) { 
    function myCustomEventHandler() {
        console.log('myCustomEventHandler has run');
        // the code inside this handler requires the controller's scope
    }

    $('body').on('myCustomEvent', myCustomEventHandler);    

    $scope.$on("$destroy", function(){
        $('body').off('myCustomEvent', myCustomEventHandler);   
        //scope destroyed, no longer in ng view
    });
}

edit This is an angularJS solution. The ngview is constantly being loaded as you move from page to page. It will attach the event over and over again as the function is repeatedly called. What you want to do is unbind/remove the event when someone leaves the view. You can do this by hooking into a scopes $destroy (with the dollar sign) event. You can read up more on that here: $destroy docs

3 Comments

I love this answer, and now I'm sorry my question referred so specifically to jQuery. My curiosity about why jQuery wasn't working as expected didn't exactly align with the problem I needed to solve, but this answer solves my problem very well.
This is nice, I suggested this in my answer, but I didn't know enough about angular to know about the $destroy event. No way to create collisions (as you could with event namespaces)
@tuff As much as I don't want to come off as elitist (sorry if I am), I would suggest my answer over the one you chose. The reason being that if you use the top one and someone goes to the view and then leaves you will have this event handler still bound somewhere. Even if they never go to the view again. In my answer though it will be removed permanently.
2

The problem is that when function MyViewController(){} is called multiple times, you get a separate instance of myCustomEventHandler (attached to the current closure), so passing that to $.off doesn't unregister the previous handler.

KevinB's answer, event namespaces, is what I suggest for removing specific handlers without requiring knowledge of which handler was installed. It'd be nicer if you could unregister the events when the element is removed/hidden, then you would have the reference to the function you want to unregister, without risking removing handlers that other code may have added to the same event namespace. After all, event namespace is just a global pool of string and is susceptible to name collision.

If you make your function global, it will also work (except that it looks like you need the closure), but I'm just showing it to explain the problem, use namespaces

function myCustomEventHandler() {
    console.log('myCustomEventHandler has run');
    // the code inside this handler requires the controller's scope
}

function MyViewController() { 

    // first attempt
    $('body').off('myCustomEvent', myCustomEventHandler);
    $('body').on('myCustomEvent', myCustomEventHandler);
    // second attempt
    $('body').one('myCustomEvent', myCustomEventHandler);
    // third attempt
    $('body').off('myCustomEvent', $.proxy(myCustomEventHandler, this));
    $('body').on('myCustomEvent', $.proxy(myCustomEventHandler, this));

}

// ...meanwhile, elsewhere in the app, this function is run after a certain user action
function MyEventSender() {
    $('body').trigger('myCustomEvent');
    console.log('myCustomEvent has been triggered');
}
MyViewController();
MyViewController();
MyEventSender();

Previous Idea

One of the problems is that you're not passing the same function to $.on and $.off, so off is not unregistering anything in this case

Not the problem, leaving the answer up for reference since it's not exactly intuitive. $.proxy seems to return a reference to the same bound function if passed the same function and context. http://jsbin.com/adecul/9/edit

4 Comments

That's what i expected(i expected to see it executed 3 times) but instead i'm only seeing it once, see jsbin in my other comment. The fact that he's even getting more than three suggests something else is going on outside of what is being shown.
@KevinB I ran your jsbin but I'm not sure that the view is being reloaded in that case. I called MyEventSender(); 3 times but couldn't reproduce the problem.
Yeah, nothing i do with the given code results in what the OP is seeing.
here it is: jsbin.com/adecul/1 when you run the controller multiple times, it isn't cleaning up the old events.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.