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.

PodsTooltip.js
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);
  1. When tooltip is opened, I’ve added a focusout event and call closeTooltip() called with bind(this) as I want this to have the correct context inside closeTooltip() method.
  2. 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.

PodsTooltip.js
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()

PodsTooltip.js
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

PodsTooltip.js
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

PodsTooltip.js
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.

PodsTooltip.js
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);
  }
}