Skip to main content Accessibility Feedback

The handleEvent() method is the absolute best way to handle events in Web Components

Yesterday, I wrote about how I handle event binding in my Web Components, and it sparked a lot of great conversation on Mastodon!

A bunch of people shared their own approaches, but the one that absolutely blew my mind was the handleEvent() method, a platform-native method for managing all of the events on your Web Component.

Let’s dig in!

What is the handleEvent() method?

The handleEvent() method is part of the EventListener API, and has been around for decades.

If you listen for an event with the addEventListener() method, you can pass in an object instead of a callback function as the second argument.

As long as that object has a handleEvents() method, the event will be passed into it, but this will maintain it’s binding to the object.

document.addEventListener('click', {
	name: 'Merlin',
	handleEvent (event) {
		console.log(`The ${event.type} happened on ${this.name}.`);
	}
});

How can you use this in a Web Component?

Let’s say you have a <count-up> component that renders a button and displays the number of times it’s been clicked.

<count-up></count-up>
customElements.define('count-up', class extends HTMLElement {

	// Instantiate the component
	constructor () {

		// Get the parent properties
		super();

		// Define properties
		this.count = 0;
		this.button = document.createElement('button');

		// Inject the button
		this.append(this.button);
		this.render();

	}

	// Render the UI
	render () {
		this.button.textContent = `Clicked ${this.count} times`;
	}

});

Here’s a demo (it doesn’t have interactivity yet).

Using the technique I shared yesterday, here’s how I would typically have added an event listener.

customElements.define('count-up', class extends HTMLElement {

	// Instantiate the component
	constructor () {

		// Get the parent properties
		super();

		// Define properties
		this.count = 0;
		this.button = document.createElement('button');
		this.handler = this.createHandler();

		// Inject the button
		this.append(this.button);
		this.render();

	}

	// Create the click handler
	createHandler () {
		return (event) => {
			this.count++;
			this.render();
		};
	} 

	// Start listening
	connectedCallback () {
		this.button.addEventListener('click', this.handler);
	}

	// Stop listening
	disconnectedCallback () {
		this.button.removeEventListener('click', this.handler);
	}

	// Render the UI
	render () {
		this.button.textContent = `Clicked ${this.count} times`;
	}

});

Here’s an interactive demo.

Now, let’s look at how you could write this using the handlEvent() method.

Using the handleEvent() method in a Web Component

With this approach, we no longer need to setup a this.handler or use arrow functions.

We pass this, the Web Component class, directly into the addEventListener() method, which runs the handleEvent() method whenever the event is triggered.

customElements.define('count-up', class extends HTMLElement {

	// Instantiate the component
	constructor () {

		// Get the parent properties
		super();

		// Define properties
		this.count = 0;
		this.button = document.createElement('button');

		// Inject the button
		this.append(this.button);
		this.render();

	}

	// Create the click handler
	handleEvent (event) {
		this.count++;
		this.render();
	} 

	// Start listening
	connectedCallback () {
		this.button.addEventListener('click', this);
	}

	// Stop listening
	disconnectedCallback () {
		this.button.removeEventListener('click', this);
	}

	// Render the UI
	render () {
		this.button.textContent = `Clicked ${this.count} times`;
	}

});

Here’s a demo of the handleEvent() method.

As you can see, we’ve shaved a few steps off.

Handling multiple event types

What happens if you need to listen to multiple events in a Web Component?

customElements.define('awesome-sauce', class extends HTMLElement {

	// Instantiate the component
	constructor () {
		super();
	}

	// Handle events
	handleEvent (event) {
		// ...
	}

	// Listen for events
	connectedCallback () {
		this.addEventListener('click', this);
		this.addEventListener('input', this);
		this.addEventListener('close', this);
	}

});

You could handle all over your events inside the handleEvent() method using some if...else logic.

// Handle events
handleEvent (event) {
	if (event.type === 'click') {
		// ...
	} else if (event.type === 'input') {
		// ...
	} else {
		// ...
	}
}

But Andrea Giammarchi shares a more elegant technique.

You can create on* methods in your class for each of your different event types, and use the handleEvent() method to automatically route events to them.

// Handle events
handleEvent (event) {
	this[`on${event.type}`](event);
}

// Click events
onclick (event) {
	// ...
}

// Input events
oninput (event) {
	// ...
}

Update: some folks have reported conflicts or Typescript errors using the on*() naming convention. Switching to handle*() fixes it. I’ve not personally run into any issues, but thought it was worth pointing out.

Why is this the best approach for events in Web Components?

I like how simple and scalable it is. It makes the code much cleaner and easier to read (in my opinion).

Andrea cites a few other reasons.

In his testing, it used substantially less memory than using arrow functions, adding additional properties (like I do), or using .bind() (and approach that was shared with me by a few folks).

It also makes it much easier to add and remove events. You always pass this in to the addEventListner() and removeEventListener() methods, and let the handleEvent() method handle the rest.

The naming convention this enforces on your handler functions is clear and consistent.

And if you accidentally listen to the same event multiple times, with this approach, your handler function only runs once.

This is how I’ll be authoring event listeners in my web components going forward.