Search code examples
web-componentshadow-domevent-bubblingslot

Using event delegation (bubbling) in web component with slot problem


I create this simple example (pure JS, not in Vue or React, etc.):

<body>
  <script>
    (function () {
      class MyComponent extends HTMLElement {
        constructor() {
          super();
          const shadowRoot = this.attachShadow({ mode: 'open' });
          const template = document.createElement('template');
          template.innerHTML = `
            <div>
              <button>
                <slot> <em>left</em> </slot>
              </button>
            </div>
          `;
          shadowRoot.append(template.content.cloneNode(true))

          shadowRoot.addEventListener('click', (event) => {
            // normal event delegation stuff,
            const button = event.target.closest('button');
            if (!button) return;
            // do somthing with button
            console.log(button);
          });
        }
      }

      customElements.define('my-component', MyComponent);
    }());
  </script>

  <my-component id="myComponent"></my-component>
</body>

Currently it works well.

But, after the slot added:

<my-component id="myComponent"> <span>previous</span> </my-component>

The event delegation code broken, because what I clicked is the Light DOM but Shadow DOM, so, I got null with const button = event.target.closest('button');

Any advise to use event delegation within slot?

If there is any problom with grammar, I am Chinese :) Thank you for reading


Solution

  • The event.composedPath() output has all the elements the click Event passed.
    Crossing all shadowRoot boundaries in between.

    I condensed your code a bit...

    Note: The SO snippet console is slow and verbose; check the TOP OF the F12 console (SO snippet could also list sandbox errors there)

    <script>
      customElements.define('my-component', 
       class extends HTMLElement {
        constructor() {
          super()
           .attachShadow({mode:'open'})
           .innerHTML = `<div>
                           <button><slot>Left</slot></button>
                         </div>`;
          this.shadowRoot.addEventListener('click', (evt) => {
            console.log(evt.composedPath());
          });
        }
      });
    </script>
    
    <my-component></my-component>
    <my-component>Right</my-component>