web components - custom events on slotchange

228 Views Asked by At

can someone please explain what happens here? am I missing something?

#handleClick() {
  this.dispatchEvent(new Event('onClicked'));
}
mySlot.addEventListener('slotchange', () => {
   mySlot.addEventListener('click', this.#handleClick); // doesn't work
   mySlot.addEventListener('click', () => this.#handleClick()); // works
});
...
myCustomElement.addEventListener('onClicked', (e) => {
  console.log(e, e.detail);
});
#handleClick() {
  this.dispatchEvent(new Event('onClicked', {bubbles: true, composed: true}));
}
mySlot.addEventListener('slotchange', () => {
   mySlot.addEventListener('click', this.#handleClick); // works
});
...
...
myCustomElement.addEventListener('onClicked', (e) => {
  console.log(e, e.detail);
});

I know that custom events don't reach the "light-DOM" out of the box, that's why I can understand why we should put "composed: true and bubbles: true". But why do mySlot.addEventListener('click', () => this.#handleClick()) works without the need of making the event "composable"?

Here is a fiddle to better understand the problem: https://jsfiddle.net/rv6w3xj1/1/

1

There are 1 best solutions below

4
On

Your problem is scope

A Function reference gets a different this scope than a Arrow Function definition,
thus your this.dispatchEvent is executed on/from different DOM Elements.

When that element is inside shadowDOM, it needs bubbles:true;composed:true to escape through the shadowRoot

I pruned your code to the bare minimum, slot and slotchange have got nothing to do with this

<custom-element><h2>Click SLOT</h2></custom-element>

<script>
  customElements.define('custom-element', class extends HTMLElement {
    constructor() {
      super().attachShadow({ mode: "open" })
             .innerHTML = `<h1>Click H1</h1><slot></slot>`;
      const addClick = (name) => {
        const el = this.shadowRoot.querySelector(name);
        el.addEventListener('click',        this.#handleClick    ); // func REF
        el.addEventListener('click', (e) => this.#handleClick(e) ); // func DEF
      }
      addClick("h1");
      addClick("slot");
    }
    #handleClick(e) {
      console.error(e.type, 'handleClick scope:', this);
      this.dispatchEvent(new CustomEvent('onClicked', {
        bubbles: true, composed: true,
      }));
    }
  });

  document.querySelector('custom-element').addEventListener('onClicked', (e) => {
    console.log("target:", e.target, e.target.getRootNode(), 
                "\ncomposedPath[0]:", e.composedPath()[0]);
  });
</script>

You can fix it with oldskool this.handleClick.bind(this),
but that is more error-prone than (e) => this.handleClick(e)
because most junior DEVs won't understand what is going on with bind

To make 'scope' clearer

Below code displays where this.dispatchEvent(new CustomEvent('onClicked' originates from
Thus showing what this is in this.dispatchEvent

When inside shadowDOM it needs bubbles:true;composed to escape shadowDOM

The e.composedPath() clearly shows a different path the "click" Event passed.
One click came from <slot> (inside shadowDOM) the other from your <custom-element> (outside shadowDOM)

<custom-element>
  Slotted Content, click me!
</custom-element>

<script>
  const log = (...x) => document.body.append(document.createElement("BR"), ...x);
  customElements.define('custom-element', class extends HTMLElement {
    constructor() {
      super().attachShadow({mode:"open"}).innerHTML = `<slot></slot>`;
    }

    #handleClick(e) {
      log("CLICKED! ", "'this' scope is:", this.nodeName);
      this.dispatchEvent(new CustomEvent('onClicked', {
        bubbles: true,
        composed: true,
      }));
    }

    connectedCallback() {
      const mySlot = this.shadowRoot.querySelector('slot');
      mySlot.addEventListener('click', this.#handleClick);
      mySlot.addEventListener('click', () => this.#handleClick());
    }
  });

  document.querySelector('custom-element').addEventListener('onClicked', (e) => {
    log(e.composedPath().map((x, i) => i + " " + x.nodeName));
  });
</script>