Cannot unbind jQuery custom event handler - javascript

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
???

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);

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

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

Related

AddEventListener with wrapper listener

I am trying run custom code whenever a click event is triggered. This is what I have so far:
const origHandler = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (eventName, eventHandler, options) {
let handler = eventHandler;
const target = this;
origHandler.call(this, eventName, function (e) {
// Do something with e
doSomething(e);
// Run original function
handler.call(target, e);
}, options);
};
I am also using this multi-select dropdown plugin. With the code above, clicking on the dropdown element doesn't do anything until I click it a couple of times.
It works fine if I just do the following:
origHandler.call(this, eventName, handler, options);
However, the above doesn't allow me to run custom code whenever the handler is called. Is there anything I can do to create a wrapper that also works with these types of plugins?
This problem is not specific to this plugin, as I have seen a few other plugins in the application also breaking as a result of this code.
Incomplete Algorithm
The posted code calls (the original) addEventListener with an anonymous function argument. This means any removeEventListener in calling code which supplies handler as its function argument will fail - it never matches the anonymous function.
To successfully add a hook into addEventListener would require implementing a complementary hook into removeEventListener and additional logic to achieve correct removal of added listeners.
This doesn't mean the particular problems encountered are specifically caused by only patching addEventListener, but doing so is guaranteed to produce code failure.
In general patching prototype object properties of global functions is probably best avoided if at all possible.
Alternative using Capture
Adding a document click event listener that uses event capture, before including any library scripts, should allow inspection of every click event before being handled by anything else:
document.addEventListener("click", function(e){
// do something with event
console.log("click event type: %s on %s", e.type, e.target.tagName);
}, {capture:true});
body {background-color: white}
html {background-color: grey}
Click me!

Knockout event binding unexpected behaviour

I have an event binding for the scroll event on a DIV. For debouncing the handler I introduced a function on my model which creates the debounced handler, and I'm binding this factory function in my view.
I would expect that my factory creates the debounced function and knockout will bind that to the event. Instead, it seems like knockout recreates and calls my debounced function at every event trigger, so debouncing doesn't work at all.
My view
<div data-bind="event.scroll: getScrollHandler()"></div>
My model
var viewModel = {
getScrollHandler: function(data, evt) {
return debounceFunction(function(data, evt) {
// dp the actual handling...
});
}
};
I would expect that the getScrollHandler method would execute only once at binding initialization time, and it would bind the returned function.
Instead, it seems like knockout is wrapping it all to a new function so it runs on every scroll event.
How exactly does it work in Knockout?
UPDATE
As I'm using TypeScript and this handler is a member method of a class, I'm limited to this kind of function member assignment, I cannot directly assign the debounced function as a member (or actually I could, but in some uglier way only).
Assuming you have an implementation similar to this one, the idea is it creates a new function which you then use in place of your original function. Try changing your code to this:
getScrollHandler: debounceFunction(function(data, event) {
...
})
This will create the function once and re-use it every time the scroll is activated.

Namespacing Marionette's App event aggregator events jquery-like

In my Backbone Marionette App I'm filtering things in the router using 'before' and 'after' (backbone.routefilter). In order to avoid doing this in the router, I'm using App's vent to handle it somewhere else.
I have a method that add events to these handlers and everything work just fine, except if I need to remove some callbacks registered to run in the vent handler, for example:
// Some callback is registered to the router:before event.
App.registerRouterEventCallback.add({type: 'before', 'callback': function () { console.log("I'm Doe, John.") }});
// The URL changes and the callback is run.
App.vent.on('route:before', callback);
// In another site location, another callback is added to the events stack
// and attached to the route:before callback: there are 2 functions registered
// to this event now.
App.registerRouterEventCallback.add({type: 'before', 'callback': function () { console.log("I'm Doe, Josh.") }});
But what if the first event need to be removed? A call to App.vent.off('route:before') can't be done as it would wipe all the functions attached to this event.
The first thing I came up with is to namespace the events, so I can remove it without mess up with the other ones. But I want them to be added dynamically, not hardcoded in the router. In fact I would like to leave the router as clean as possible.
I wonder if there is something out of the box like the following:
// Listener for all route:before events
App.vent.on('route:before', fn);
// Listener for route:before:foo event
App.vent.on('route:before:foo', foo);
The idea here is to deal with it in a more jQuery-ish way, where you assign $('#foo').on('click.foo', fn) and have this listener removed by calling $('#foo').off('click.foo').
Any ideas would be great.

How to unbind $on in Angular?

I have a code that use $scope.$on one time on init and then in a function, so the code is executed multiple times. How can I unbind if first before I bind it again. I've try $scope.$off but there's not such function, https://docs.angularjs.org/api say nothing about $on. I'm using angular 1.0.6.
If you don't un-register the event, you will get a memory leak, as the function you pass to $on will not get cleaned up (as a reference to it still exists). More importantly, any variables that function references in its scope will also be leaked. This will cause your function to get called multiple times if your controller gets created/destroyed multiple times in an application.
Fortunately, AngularJS provides a couple of useful methods to avoid memory leaks and unwanted behavior:
The $on method returns a function which can be called to un-register the event listener.
Whenever a scope gets cleaned up in Angular (i.e. a controller gets destroyed) a $destroy event is fired on that scope. You can register to $scope's $destroy event and call your cleanUpFunc from that.
See the documentation
Sample Code:
angular.module("TestApp")
.controller("TestCtrl",function($scope,$rootScope){
var cleanUpFunc = $scope.$on('testListener', function() {
//write your listener here
});
//code for cleanup
$scope.$on('$destroy', function() {
cleanUpFunc();
};
})
$scope.$on returns a function which you can call to unregister.

Events on DOM elements not yet created, in Javascript?

I have a Javascript module the following Javascript:
EntryController = function$entry(args) {
MainView();
$('#target').click(function() {
alert('Handler called!');
});
}
MainView() has a callback that creates the #target button. Because of the callback the code will pick up and run through the rest of the code $('#target') ... before #target is created. If this is the case the event is never hooked up to the #target. If I put a breakpoint at $('#target') that'll give the callback enough time to return and build the #target, when I press play everything works as expected.
What's the best way to deal with this? I would like all events to take place in the controller so it can choose which view to send it to.
I was thinking about placing the entire $('#target').click ... inside MainView() and instead of alert('Handler called!'); I'd put a references to EntryController.TargetEventRaise(), but that started to look a bit like steady code. What's the best way to approach this?
You're looking for jQuery's live event handlers, which will handle an event on every element that matches the selector, no matter when the element was created.
For example:
$('#target').live('click', function() {
alert('Handler called!');
});
Alternatively, you could make the MainView function itself take a callback, and add the handler in the callback. You could then call the callback in MainView inside of its callback.

Categories