Nathan Knowler

Managing Event Listeners in Custom Elements

Permalink

When managing event listeners in custom elements, there tends to be a bit of boilerplate that needs to be done for every event listener:

  • Storing the bound event listener as a property of the class inevitably in the constructor (so remember to call super());
  • Adding the event listener;
  • Removing the event listener.

It might not seem like a lot, but your custom element’s class can easily get a bit bloated.

A little known and newer feature of EventTarget.addEventListener() is the signal property in the options object which it takes as a third paramter. It expects an AbortSignal which it will use to remove the event listener. You can get and call an AbortSignal with an AbortController. I store this on a base class which extends HTMLElement, create an easy accessor for the AbortSignal, and then call AbortController.abort() in disconnectedCallback():

class BaseElement extends HTMLElement {
	#disconnectionController = new AbortController();

	disconnectSignal = this.#disconnectionController.signal;

	disconnectedCallback() {
		this.#disconnectionController.abort("element disconnected");
	}
}

My custom elements can just extend the base element class and use the AbortSignal when adding event listeners:

class MyButton extends BaseElement {
	get button() {
		return this.querySelector(':scope > button');
	}

	connectedCallback() {
		this.button.addEventListener(
			'click',
			this.#onClick.bind(this),
			{ signal: this.disconnectSignal },
		);
	}

	#onClick(event) { /* Handle event */ }
}

Since you don’t need to remove the event listener manually, you can even use a closure instead of creating a class method. It’s up to you.

Browser Support

If you don’t need to support Safari prior to 15 on macOS and iOS then you are good to go. Take a look at the support table.