Event Delegation and connectedCallback are like Brother and Sister

The concept of event delegation in JavaScript boils down to this: rather than attach event handlers directly to the element triggering the event, attach it to an element higher up in the DOM. The event should bubble up there anyway, and you can test the event’s trigger element to ensure you’re responding to the event from the correct element. The purpose of this is that is saves you from having to re-bind events should the DOM change, for example with new HTML being added.

An Example Problem

Here we’ll attach a click handler to a button:

<div class="el"> 
  <button>Click me</button>
</div>Code language: HTML, XML (xml)
const buttons = document.querySelectorAll("button");
buttons.forEach((button) => {
  button.addEventListener("click", () => {
    // do something
  });
});Code language: JavaScript (javascript)

That will work. But then let’s say something happens that adds to the DOM, and now we’ve got two more buttons:

<div class="el"> 
  <button>Click me</button>
  <button>Click me</button>
  <button>Click me</button>
</div>
Code language: HTML, XML (xml)

This is the problem. The first button will have the click handler, the second two will not. It’s on us to re-bind those click handlers to those new buttons. Not a massive problem, but we need to remember to do it, thus it’s error-prone.

Event Delegation in jQuery

I realize jQuery isn’t exactly hot new technology here, but I’m bringing it up first for a reason. Eventually, jQuery’s API evolved to the point that this is how they suggested writing event handlers:

$(document).on("click", ".el button", function() {
  $(this).toggleClass("active");
});Code language: JavaScript (javascript)

Here, we’re actually binding the event to the document element (the <html> element) which is as high up in the DOM as you can go. But then that second parameter, ".el button", means that this function will only fire if the actual event was triggered from an element matching that selector. Literally event delegation. jQuery knew it was a good plan.

Event Delegation in Vanilla JavaScript

This is just a smidge more verbose in vanilla (no library) JavaScript.

document.querySelector(document.documentElement).addEventListener("click", (e) => {
  if (e.target.matches(".el button")) {
    e.target.classList.toggle("active");
  }
});Code language: JavaScript (javascript)

You don’t have to delegate all the way up to the top, as it were, if for some reason that doesn’t make sense for you. It is possible that some element along the way does a stopPropagation and screws up the event bubbling. You can delegate anywhere you want.

And Now… Web Components?

That’s right! Check it. Let’s make this our custom element:

<toggle-button></toggle-button>Code language: HTML, XML (xml)

Then we’ll plop in an actual <button> when the element is ready to go. A good place to do this is in the connectedCallback function, like so:

customElements.define(
  "toggle-button",
  class WebComponentButton extends HTMLElement {
    connectedCallback() {
      this.innerHTML = `<button>Button</button>`;
      const button = this.querySelector("button");
      button.addEventListener("click", () => {
        button.classList.toggle("active");
      });
    }
  }
);Code language: JavaScript (javascript)

How is this like event delegation, you ask? Because that connectedCallback runs anytime this custom element lands in the DOM, whether it is there when the page is loaded, or anytime after.

The binding of the event happens automatically as the Web Component is instantiated. Works great.

Demo or it didn’t happen

In the first set above, you’ll see as you “Add Button” to add additional buttons, they newly-added buttons do not have the proper click handler on them. Only the first one works.

All the rest of the set work without needing any re-binding event work.

Notes

Thanks to Dave Rupert for seeding my mind with this. I can’t remember where we were talking about this, but I think he called it like a “secret killer feature” of Web Components, or the like.

Also, you could put event handlers right on elements like:

<button
  onclick="this.classList.toggle('active')">
  Click Me
</button>Code language: HTML, XML (xml)

Despite that being essentially how JSX rolls (from an authoring perspective) this has never been a particularly favorable way to write HTML/JavaScript, and for good reason. It’s very awkward to write JavaScript within a string like that. It’s not particularly lint-able. It forces things to be global in a way you probably don’t. Just to name a few.

Leave a Reply

Your email address will not be published. Required fields are marked *