I'm looking for a better solution to my problem.
I have a function that returns a function that dynamically attaching event handlers to the document, it is wrapped inside an init
function:
const init = (events) => {
const generateListener = listenForKey => ({ keyCode, key}) => {
if (keyCode === listenForKey) {
console.log(`action - ${key}`);
}
}
events.forEach(({name, keyCode}) => {
document.addEventListener(name, generateListener(keyCode))
});
}
Now when I run this code it works as expected:
const someEvents = [
{
name: 'keypress',
keyCode: 13
},
{
name: 'keypress',
keyCode: 43
}
];
init(someEvents);
The problems begins when I need to run init
again (which I do need):
init(someEvents);
// ... at a future point in time
init(someEvents);
In this case, the handler will get invoked twice because it got registered twice.
This is by designed because the browser thinks it's a new data being passed as I'm passing a new function instance each time.
You can see it here (just press the Enter or + keys):
const init = (events) => {
const generateListener = listenForKey => ({ keyCode, key}) => {
if (keyCode === listenForKey) {
console.log(`action - ${key}`);
}
}
events.forEach(({name, keyCode}) => {
document.addEventListener(name, generateListener(keyCode))
});
}
const someEvents = [
{
name: 'keypress',
keyCode: 13
},
{
name: 'keypress',
keyCode: 43
}
];
init(someEvents);
// this will register the events again and will invoke the handlers twice
//as we are passing "new" argument (a new function instance)
init(someEvents);
<div>Click the '+' or 'Enter' keys </div>
One possible solution is to use the object parameter or do something similar of my own, like storing the handlers in the window and attaching them to document as a single instance. This way the browser will ignore multiple registrations for the same handlers:
const init2 = (events) => {
const generateListener = listenForKey => ({ keyCode, key}) => {
if (keyCode === listenForKey) {
console.log(`action - ${key}`);
}
}
events.forEach(({name, keyCode}) => {
const handlerName = `on${name}${keyCode}`;
this[handlerName] = this[handlerName] || generateListener(keyCode);
document.addEventListener(name, this[handlerName]);
});
}
const someEvents = [
{
name: 'keypress',
keyCode: 13
},
{
name: 'keypress',
keyCode: 43
}
];
init2(someEvents);
init2(someEvents);
This works well, but we pollute the global window object.
I feel like I could be missing something here so I'm looking for a better approach.
According to MDN, multiple event listeners with identical params and on same target are discarded.
Your problem is due to the fact that you're doing the registration via anonymous functions (arrow funcs).
Now since you already have a global object (i.e. you don't need to create more global objects), you can use that to create named functions like:
const someEvents = [
{
name: 'keypress',
keyCode: 13,
onkeypress13 () {...}
},
{
name: 'keypress',
keyCode: 43,
onkeypress43 () {...}
}
];
and that should take care of dup registrations. Here's a working snippet (sans the keycode check if condition but it shows that each event handler is called only once)
const init = (events) => {
events.forEach((event) => {
document.addEventListener(event.name, event[`on${event.name}${event.keyCode}`])
});
}
const someEvents = [{
name: 'keypress',
keyCode: 13,
onkeypress13() {
console.log('13 pressed')
}
},
{
name: 'keypress',
keyCode: 43,
onkeypress43() {
console.log('43 pressed')
}
}
];
init(someEvents);
// this will register the events again and will invoke the handlers twice
//as we are passing "new" argument (a new function instance)
init(someEvents);
<div>Click the '+' or 'Enter' keys </div>