An interesting fact on using removeEventListener() inside a web component
Last few days I was working on this pods-tooltip
web components hoping to replace existing tooltip inside Pods Framework to have styles encapsulated inside shadowRoot, so it’d easier to update tooltip styles whenever it’s needed without worrying about breaking styles on other UI elements.
Let’s look at some codes.
class PodsTooltip extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
// ...
}
closeTooltip() {
this.tooltipContent.classList.remove("visible");
this.removeEventListener('focusout', this.closeTooltip.bind(this));
}
openTooltip() {
this.tooltipContent.classList.add("visible");
this.addEventListener('focusout', this.closeTooltip.bind(this));
}
}
customElements.define("pods-tooltip", PodsTooltip);
- When tooltip is opened, I’ve added a
focusout
event and callcloseTooltip()
called withbind(this)
as I wantthis
to have the correct context insidecloseTooltip()
method. - Then then this event listener when tooltip is closed.
But the way I use removeEventListener()
does not work as expected, it won’t remove the event listener when testing.
In case you’re wondering, this failed as well.
class PodsTooltip extends HTMLElement {
// ...
closeTooltip() {
// ...
this.removeEventListener('focusout', this.closeTooltip);
}
openTooltip() {
// ...
this.addEventListener('focusout', this.closeTooltip.bind(this));
}
}
According to Function.prototype.bind() on MDN website.
The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
So we need to store this new function in our component, or get rid of the bind()
call. I think of 3 solutions for this problem.
1. Store this new function inside constructor()
class PodsTooltip extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
// Store the new function here
this.closeTooltip = this.closeTooltip.bind(this);
}
connectedCallback() {
// ...
}
closeTooltip() {
// ...
this.removeEventListener('focusout', this.closeTooltip);
}
openTooltip() {
// ...
this.addEventListener('focusout', this.closeTooltip);
}
}
Now inside removeEventListener()
, we’re reference the correct function. If you print out this.closeTooltip.name
, it’s “bound closeTooltip”.
console.log(this.closeTooltip.name); // bound closeTooltip
But this solution doesn’t feel intuitive as we might forget to store the new function inside constructor
. Plus, we need to update function names in two places.
2. Use arrow function
class PodsTooltip extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
// ...
}
/**
* Change to arrow function here
*/
closeTooltip = () => {
// ...
this.removeEventListener('focusout', this.closeTooltip);
}
openTooltip() {
// ...
this.addEventListener('focusout', this.closeTooltip);
}
}
For arrow function inside closeTooltip()
, this
context will be component itself, so we no longer need to bind()
the function when adding event listener.
3. Use static function
class PodsTooltip extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
// ...
}
/**
* Change to static function here
*/
static closeTooltip() {
// ...
this.removeEventListener('focusout', PodsTooltip.closeTooltip);
}
openTooltip() {
// ...
this.addEventListener('focusout', PodsTooltip.closeTooltip);
}
}
For static method, we can reference it by PodsTooltip.closeTooltip
, and another possible way to reference it is like this.
class PodsTooltip extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
// ...
}
static closeTooltip() {
// ...
this.removeEventListener('focusout', this.constructor.closeTooltip);
}
openTooltip() {
// ...
this.addEventListener('focusout', this.constructor.closeTooltip);
}
}