I want to keep the scope at a module level, while having a reference to a function, so that I can add it to an event listener (as a callback), and also remove it:
export default () => ({
init() {
// Detect escape key press
window.addEventListener('keyup', this.escapeKeyPressListener, true);
},
whatever() {
// Do whatever
...
// Remove event listener
window.removeEventListener('keyup', this.escapeKeyPressListener);
},
escapeKeyPressListener: (event) => {
console.log('added');
if (event.key === 'Escape' || event.key === 'Esc') {
event.preventDefault();
this.whatever();
}
}
});
Just as additional info, even if it shouldn't matter, the module is being used with Alpine, as follows:
import lightbox from "./alpine-modules/lightbox";
Alpine.data('lightbox', lightbox);
But this is not working, when this.whatever()
is reached, the scope is incorrect.
How should I structure this so that I can add and remove the function to the listener and also keep the scope working as I want it to?
Did I try something or am I just guessing?
Well... after learning about arrow functions and reading this similar but not the same question, I tried using:
window.addEventListener('keyup', (event) => this.escapeKeyPressListener, true);
... and defining the function just as a normal one:
escapeKeyPressListener(event) {
It works as in... it triggers the listener and the scope is fine, but then, how do I remove the listener without having a reference to it?
Based on Quentin's feedback, I did it as follows (the method names are different because my previous example was a simplified/generalized version) with the inital aim of keeping this simple:
/**
* @property isOpen - controls the display state of the lightbox
*/
let isOpen = false;
/**
* Responsible for opening the lightbox
*/
const open = () => {
isOpen = true;
// Detect escape key press
window.addEventListener('keyup', escapeKeyPressListener, true /* Capture it for any part in the DOM */);
};
/**
* Responsible for closing the lightbox
*/
const close = () => {
isOpen = false;
// Stop detecting escape key press
window.removeEventListener('keyup', escapeKeyPressListener);
};
/**
* Responsible for adding listener function for escape keypress
* @param {object} event - keyup event listener
*/
const escapeKeyPressListener = (event) => {
console.log('added');
if (event.key === 'Escape' || event.key === 'Esc') {
event.preventDefault();
close();
}
};
const lightbox = () => ({
isOpen,
open,
close,
escapeKeyPressListener,
});
export default lightbox;
The module stopped working but shows no errors.
Interestingly, if I change the initial let isOpen = false;
to let isOpen = open;
, Alpine renders the component as opened, so... Alpine is actually getting and processing part of this.
CodePudding user response:
You're breaking the value of this
in two different ways.
Arrow functions
escapeKeyPressListener
is an arrow function, so the value of this
inside it is lexical. It will be same as the this
value of the module (undefined
since modules run in strict mode) and not the object you are creating inside the module.
Passing functions
You are passing escapeKeyPressListener
to addEventListener
so if it wasn't an arrow function, this
would be the object on which it is listening: window
.
Neither of those will let you reference the object.
Use module scope
You said I want to keep the scope at a module level, but you haven't done that. This is easier if you do.
Everything has a name, and you don't need to use this
at all.
const init = () => {
// Detect escape key press
window.addEventListener('keyup', escapeKeyPressListener, true);
};
const whatever = () => {
// Do whatever
...
// Remove event listener
window.removeEventListener('keyup', escapeKeyPressListener);
};
const escapeKeyPressListener: (event) => {
console.log('added');
if (event.key === 'Escape' || event.key === 'Esc') {
event.preventDefault();
whatever();
}
};
const defaultObject = {
init,
whatever,
escapeKeyPressListener,
};
export default defaultObject;
This get more complex if you want to call init
multiple times and have them deal with independent instances. At that point you should start looking into classes instead of plain objects.
CodePudding user response:
This is not a scope problem. This is a this
problem.
The problem is that you are trying to be very terse (using an expression to immediately resolve to an anonymous object, which you are then immediately exporting), which denies you a handle to the this
you want.
Quentin points you to the idiomatic solution, but what if you were wedded to keeping everything in a single expression?
I think something like the following might work. Don't do this, by the way.
export default () => ((function unboundEscapeKeyPressListener(event) => {
console.log('added');
if (event.key === 'Escape' || event.key === 'Esc') {
event.preventDefault();
this.whatever();
}
}), {
init() {
this.escapeKeyPressListener ??= unboundEscapeKeyPressListener.bind(this)
// Detect escape key press
window.addEventListener('keyup', this.escapeKeyPressListener, true);
},
whatever() {
if(!this.escapeKeyPressListener)
throw 'Not initialized.'
// Do whatever
...
// Remove event listener
window.removeEventListener('keyup', this.escapeKeyPressListener);
}
});