JavaScript remove event listener with params - javascript

I am adding an event listener which needs the event properties as well as other parameters
document.body.addEventListener("click", (e) =>
listenForMembersOpen(e, buttonReplacement, openEditModal)
);
I need to remove this event listener when my component unmounts but running:
document.body.removeEventListener("click", (e) =>
listenForMembersOpen(e, buttonReplacement, openEditModal)
);
doesn't seem to get the job done. I'm thinking it is because the function declaration within the event listener. Any advice on how I can remove the event lister shown?
export function addListenersForButtonReplacement(buttonReplacement, openEditModal) {
document.body.addEventListener("click", (e) =>
listenForMembersOpen(e, buttonReplacement, openEditModal)
);
document.body.addEventListener("keydown", (e) =>
listenForMembersOpen(e, buttonReplacement, openEditModal)
);
}

If you've ever tried comparing equality for an object, you know that:
const a = { v: 1 };
const b = { v: 1 };
a !== b
a != b
a === a
b === b
And that applies to functions as well.
When you call addEventListener, it doesn't toString() the input function, and when the event listener is removed, check for equality. Instead, it stores the "reference", the function you passed to as an argument, and compares that.
The way you'd use it, then, is by passing the same variable to removeEventListener as to addEventListener, like this:
// create a separate function to handle the event
const eventHandler = (e) => listenForMembersOpen(e, buttonReplacement, openEditModal);
document.body.addEventListener("click", eventHandler);
// later...
document.body.removeEventListener("click", eventHandler);
Simply passing the same function body content (meaning when you toString() it the value will be the same) will not be recognized as the same event listener.
In your context, you'll need to export that function to be used elsewhere, perhaps by returning that value from the exported function, like this:
export function addListenersForButtonReplacement(buttonReplacement, openEditModal) {
const eventHandler = (e) => listenForMembersOpen(e, buttonReplacement, openEditModal);
document.body.addEventListener("click", eventHandler);
document.body.addEventListener("keydown", eventHandler);
// "export" it by returning:
return eventHandler;
}
Later when you're using it elsewhere...
import { addListenersForButtonReplacement } from "./somewhere";
// later...
const evFn = addListenersForButtonReplacement(/* ... */);
// later... (we have the same "reference" to the function)
document.body.removeEventListener("click", evFn);
document.body.removeEventListener("keydown", evFn);

yeah. you need to have a single function otherwise javascript won't be able to find the function reference
function listener (e) {
listenForMembersOpen(e, buttonReplacement, openEditModal)
}
document.body.addEventListener("click", listener);
document.body.removeEventListener("click", listener);

Related

How to bind an event callback passed as a string parameter?

I need to dynamically pass in a function name that will be added as an onclick event handler as part of a dynamic UI creator. Most of the function is easy but I can't work out how to turn the string function name into the function that is bound to the event handler.
I've tried things like:
// Add event handlers
Object.keys(compToAdd.events).forEach( (type) => {
newEl.addEventListener( type, Function.prototype.bind( compToAdd.events[type] ) )
})
But that doesn't work.
Also tried:
window.mycb = function() {
console.log('>>>> hello >>>>')
}
// ...
Object.keys(compToAdd.events).forEach( (type) => {
newEl.addEventListener( type, window['mycb'] )
})
Using window['mycb']() immediately executes the fn when applied to the event listener which is obviously not correct. Without (), nothing happens when the click event fires.
A simplest and arguably best approach would be loading all your callback functions into a single object, versus create them in global scope:
const compToAdd =
{
events:
{
click: "onClick",
mousedown: "onMouseDown",
mouseup: "onMouseUp",
}
}
const myCallbacks =
{
onClick: function(e)
{
console.log("onClick type:", e.type)
},
onMouseDown: function(e)
{
console.log("onMouseDown type:", e.type)
},
onMouseUp: "this is not a function"
}
// ...
Object.keys(compToAdd.events).forEach( (type) => {
const callback = myCallbacks[compToAdd.events[type]];
if (callback instanceof Function) //make sure it's a function
newEl.addEventListener( type, callback )
})
<div id="newEl" style="height: 100vh">click here</div>
P.S.
your second example works fine though. If it executed without () it means something triggered the event.
I have managed to find a couple of possible answers. However, I don't know that I like either of them and I am sure there are better ones.
Using eval
// Add event handlers
Object.keys(compToAdd.events).forEach( (type) => {
// Add the event listener - hate eval but it is the only way I can get it to work
try {
newEl.addEventListener( type, (evt) => {
eval(`${compToAdd.events[type]}(evt)`)
} )
} catch (err) {
console.error(`Add event '${type}' for element '${compToAdd.type}': Cannot add event handler. ${err.message}`)
}
})
Using setAttribute instead of addEventListener
// Add event handlers
Object.keys(compToAdd.events).forEach( (type) => {
if (type.toLowerCase === 'onclick' || type.toLowerCase === 'click') type = 'click'
// Add the event listener
try {
newEl.setAttribute( type, `${compToAdd.events[type]}()` )
} catch (err) {
console.error(`Add event '${type}' for element '${compToAdd.type}': Cannot add event handler. ${err.message}`)
}
})
I'm preferring (1) since it seems a bit more flexible and allows the use of pre-defined functions in any context reachable from the context when setting the event listener. But it is far from ideal.

pass the event and the element to the handler via event listener

I want to attach keypress event listener to each of words dom elements and pass the element itself ( I mean word) to the eventHandler ok?
So I tried this but it seems that I lose the event itself:
const eventHandler = (e, word) => {
console.log('keypressed', String.fromCharCode(e.which), word);
}
words.forEach((word) => {
window.addEventListener("keypress", eventHandler.bind(event, word));
});
String.fromCharCode(e.which) should bring the pressed key on keyboard but it returns nothing!!
How can I fix this?
Edit:
Here is the simpler reproduction of the issue: press any key on the keyboard and see the event is undefined:
const eventHandler = (event, word) => {
console.log('keypressed', event, word);
}
window.addEventListener("keypress", eventHandler.bind(this, event, 'word'));
The event is implicitly passed, and will be the last parameter after all the bound parameters.
So you would use eventHandler.bind(null, word) and then the handler should be declared eventHandler(word, e) {...}
const button = document.getElementById('b');
function handler(bound_arg, event){
console.log(event.target);
console.log(bound_arg);
}
button.addEventListener('click', handler.bind(null,'bound'));
<button type='button' id='b'>Bound</button>
But this isn't very intuitive, and since bind() creates a new function anyway you could simply use an anonymous function to pass the word.
window.addEventListener("keypress", (e) => eventHandler(e, word));

How does addEventListener uniquely identify the functions passed to it?

Say I have some code like this:
let myFunc = () => {
console.log("hello");
}
document.addEventListener("click", myFunc);
document.addEventListener("click", myFunc);
document.addEventListener("click", myFunc);
document.addEventListener("click", myFunc);
Why does clicking the document only console log once? I don't mind this behavior, but I'm just curious how it is implemented.
For example, if you did something like this:
let events = {};
function addEventListener(key, callback) {
if (!key) { return; }
if (!events.hasOwnProperty(key)) {
events[key] = {};
}
events[key][callback] = callback;
}
Then you're using a function as a key, but aren't only strings valid for keys? How does JavaScript uniquely identify the functions so that it knows not to add the same one multiple times?
A given event listener with a particular configuration can only be added to an element once - if you add it multiple times, as you can see, it will be as if only a single listener was added. This is described in the specification here:
If eventTarget’s event listener list does not contain an event listener whose type is listener’s type, callback is listener’s callback, and capture is listener’s capture, then append listener to eventTarget’s event listener list.
To expand on that, for a listener to be considered such a duplicate:
whose type is listener’s type
refers to the event name, eg 'click'
callback is listener’s callback
which must be the same function reference (=== to a prior listener added)
capture is listener’s capture
refers to whether the listener listens in the capturing phase or the bubbling phase. (This is set by a third boolean parameter to addEventListener, which defaults to true - bubbling, or with { capture: boolean } as the third argument)
If all of the above are the same as that of a listener added previously, then the new listener will be considered a duplicate, and will not be added again.
An easy way to add such a listener multiple times, if you wanted, would be to make an inline callback that calls your listener:
let myFunc = () => {
console.log("hello");
}
document.addEventListener("click", () => myFunc());
document.addEventListener("click", () => myFunc());
document.addEventListener("click", () => myFunc());
document.addEventListener("click", () => myFunc());
click me
The above will work because the callbacks passed to addEventListener are not equal: () => myFunc() is not === to () => myFunc().
The implementation could be something conceptually like this (I'm ignoring the details of the specification not relevant to the question):
function addEventListener(type, listener, useCapture = false) {
let typeListeners = this.eventListeners[type];
if (!typeListeners) {
this.eventListeners[type] = [{function: listener, useCapture: useCapture}];
} else {
let found = typeListeners.find(l => l.function === listener && l.useCapture == useCapture);
if (!found) {
typeListeners.push({function: listener, useCapture: useCapture});
}
}
}
It searches the list of listeners for the event type, for an existing match to the function and useCapture parameters. If it's not already there, it adds it.

remove event listener with parameters

I am trying to remove some listeners attached to certain array of elements.
As I am adding the event listeners I cannot get a reference to the parameters I needs. I have been doing some research and found out that can be solved using closures, but I can't figure out very well how to do it
here is my function reference
const editTask = function (element, taskIndex) {
const handler = function(event) {
// my code ...
}
}
and this is how I am adding the listeners
function addEditListeners() {
const editButtons = [].slice.call(document.getElementsByClassName("edit-btn"));
console.log('editbuttons', editButtons);
//editButtons.forEach(function (element) {
// element.removeEventListeners("click", editTask);
//});
editButtons.forEach(function (element, index) {
element.addEventListener("click", handler);
});
}
I have tried sending the parameters in parenthesis but the editTask is undefined, what am I doing wrong?
Notice that you are passing handler which only exists in the editTask function and nowhere else, your listener adding function should be like this
editButtons.forEach(function (element, index) {
element.addEventListener("click", (e) => editTask(element, index, e));
});
Also I see you want an event in there as well so you should pass it in edit task
const editTask = function (element, taskIndex, event) {
const handler = function(event) {
// my code ...
}
handler(event);
}

Removing an event listener that requires an argument

I have:
e.addEventListener("click",()=>{alert(this.innerText);});
and I need to be able to remove it.
If I don't use an anonymous function so that I can use removeEventListener, how to I pass this to the named function?
function f() {
//how do I access the "this"?
}
e.addEventListener("click", f);
The only problem you have is that you don't have any handle on your function since you're declaring it inline. All you need to do is move that same declaration and assign it to a variable:
const cb = () => alert(this.innerText);
e.addEventListener('click', cb);
e.removeEventListener('click', cb);
I usually just create a friendlier function to work with.
const listen = (el, ...args) => {
el.addEventListener(...args);
return{
remove: () => el.removeEventListener(...args)
};
};
So you can just do things like...
const listener = listen(document, 'click', () => {
console.log('click');
listener.remove();
});

Categories