Web Platform

The EventListener interface

3 min read

Event listener code makes up a large portion of any web application. There exist two ways to attach an event listener; onX handlers and addEventListener:

button.onclick = () => alert("Clicked");
button.addEventListener("click", () => alert("Clicked"));

The onX form only supports a function (or null/undefined). addEventListener instead can be passed anything that matches the EventListener interface:

type EventListener = null | ((e: Event) => void) | { handleEvent(e: Event): void };

That’s right. addEventListener also supports object references which have a handleEvent method.

Ordinarily when using the callback form, this refers to the attached element. If you’re using handleEvent, this is always bound to the object you passed.

<button id="clickedTimes">Times clicked: 0</button>
<script>
  const counter = {
    count: 0,
    handleEvent(e) {
      this.count += 1;
      e.currentTarget.textContent = `Times clicked: ${this.count}`;
    },
  };
  clickedTimes.addEventListener("click", counter);
  // To remove, pass counter instead of counter.handleEvent:
  // clickedTimes.removeEventListener("click", counter);
</script>

handleEvent also makes it easier to have self-modifying code. The browser resolves handleEvent every time the event is called. This makes it easier to change the underlying function.

<button id="currentMood">What's your mood?</button>
<script>
  const ref = {};
  const wordCallbacks = ["network", "hammer", "walking", "violently", "mediocre"].map((word) => (e) => {
    e.currentTarget.textContent = `Current mood: ${word}`;
    ref.handleEvent = wordCallbacks[Math.floor(Math.random() * (wordCallbacks.length - 1))];
  });
  ref.handleEvent = wordCallbacks[0];
  currentMood.addEventListener("click", ref);
</script>

Should you ever do this? Of course not. In fact, in the example above, we are still making use of a closure anyway.

Custom elements, often referred to as Web Components, are the web’s native solution for component driven design. For simple components that only attach event listeners to a single element, handleEvent might not be a terrible idea after all.

<test-counter></test-counter>
<script>
  class TestCounter extends HTMLElement {
    clicked = 0;

    connectedCallback() {
      this.btn = document.createElement("button");
      this.btn.addEventListener("click", this);
      this.render();
      this.append(this.btn);
    }

    disconnectedCallback() {
      this.btn.removeEventListener(this);
    }

    render() {
      this.btn.textContent = `Times clicked: ${this.clicked}`;
    }

    handleEvent(e) {
      this.clicked += 1;
      this.render();
    }
  }
  customElements.define("test-counter", TestCounter);
</script>