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.
Related
Sorry if I mess up anything, I'm new to coding.
I have an event listener that checks for actions within a video player in javascript.
There are a lot of events and instead of writing a new function for every one of them, I want to find a shorter way. The feedback from the listener should be displayed in the console (console.log)
It's a custom event listener but the code goes something like this:
const event = ["play", "pause", "mute"]
$player("myDiv").on(event, function() {
console.log(??)
})
So basically is there a way to log which event triggered the listener in a short way without writing a separate function for each event?
If you just want to know which event is fired at which action, you can register event listeners for each array.
const player = document.querySelector('video');
const eventTypes = ["play", "pause", "mute"];
// iterate over event types
eventTypes.forEach((eventType) => {
// add event listener to current event type
player.addEventListener(eventType, () => {
// log current event type if event is fired
console.log(`event of type ${eventType} was fired`);
});
});
But if you want to specify a separate function for each event, you can write the following code:
const player = document.querySelector('video');
const events = [
{
type: 'play',
callback() {
console.log('play event fired');
},
},
{
type: 'pause',
callback() {
console.log('pause event fired');
},
},
];
// iterate over event types
events.forEach(({type, callback}) => {
// add event listener for each event type
player.addEventListener(type, callback);
});
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);
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));
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.
I'm looking for a way to achieve the following. I could build some mechanism to do this, I'm asking for something built-in or a really simple way, if it exists and I'm missing it.
Edit:
Please note that I'm talking about events in random objects, not in DOM elements. The events order cannot be managed using the parent, etc. as suggested in the possible duplicate.
Edit 2:
Maybe an after-all-handlers-have-been-called callback? Or an always-last-to-be-executed handler?
Given:
var someObject={};
$(someObject).on("event",function() { console.log('Default handler'); });
...
$(someObject).on("event",function() { console.log('Other handler'); });
When doing:
$(someObject).triggerHandler("event");
The output is:
Default handler
Other handler
We all know this. The problem is: What if I would want to make the first event the "default" handler, to be executed if there aren't other event handlers (not a problem there) or if the other handlers didn't stop the event propagation (here is the problem).
I'm looking for a way to be able to do something like:
$(someObject).on("event",function(ev) { ev.preventDefault(); });
and prevent the first event handler from executing. In this example is not working given the execution order. Reversing the execution order is not the correct way to do it.
TL;DR
Is it possible to set a default handler, one to be called in case there's no other handlers and the event hasn't been canceled?
Edit 3: To give you a better idea, the current approach is the following (names are made up for this example):
var eventCanceled=false;
function doTheEvent() {
$(someObject).triggerHandler("event");
if(!eventCanceled) defaultEventHandler();
}
//To be called inside the handlers to stop the default handler
function cancelTheEvent() {
eventCanceled=true;
}
I just want to get rid of this and be able to use triggerHandler directly.
Hope this is what you are looking for. This is called observer pattern.
var someObj = {};
someObj.eventCallbackList = {};
someObj.createEventObject = function(name) {
return {
type: name,
preventDefault: function() {
this.callDefault = false;
},
callDefault: true
}
}
someObj.on = function(eventName, callback, defaultFlag) {
if (!this.eventCallbackList[eventName]) {
// there can be multiple other handlers
this.eventCallbackList[eventName] = {
other: [],
default: null
};
}
if (defaultFlag) {
this.eventCallbackList[eventName]['default'] = callback;
} else {
this.eventCallbackList[eventName]['other'].push(callback);
}
}
someObj.triggerHandler = function(eventName) {
var event = this.createEventObject(eventName);
var callbacks = this.eventCallbackList[eventName];
if (callbacks) {
if (callbacks['other']) {
for (var i = 0; i < callbacks['other'].length; i++) {
callbacks['other'][i](event);
}
}
if (event.callDefault && callbacks['default']) {
callbacks['default'](event);
}
}
}
// Test
someObj.on('myCustomEvent', function(event) {
console.log('OtherHandler');
event.preventDefault();
});
someObj.on('myCustomEvent', function(event) {
console.log('default');
}, true);
$(document).on('click', function() {
someObj.triggerHandler('myCustomEvent');
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
you can add a parameter to set what codes you want to execute inside your triggerHandler function.
you can refer to this thread in adding parameter.
jQuery: receive extraParameters from triggerHandler()
While there is a way to detect if the preventDefault() was called, there is no such thing as a "default handler". Event handlers are executed in the order they are registered. So the first one registered is the first one executed.
If your "default handler" doesn't need to be executed synchronously, you could delay the "default handler" to be executed after all the other handlers are done and in any of the other handlers revert that if necessary:
var defaultHandlerTimeout;
$element.on('event', function() {
defaultHandlerTimeout = setTimeout(function() {
// your actual handler
});
});
$element.on('event', function() {
// prevent the "default handler" from being executed
clearTimeout(defaultHandlerTimeout);
});
jQuery does store all of it's events internally and you can access specific element events using $._data(elem,'events').
Whether this will help your situation or not I'm not sure but is worth digging into. Personally i've never found the need to use it.
See https://stackoverflow.com/a/2518441/1175966