Search code examples
javascriptdom-eventsweb-component

If an event issued on "this" bubbles up out of my component then why do we need the "composed" property on custom events?


So I have read several pieces that say if you want a custom event to traverse the shadow DOM boundary and cross into the light DOM you need to set the custom event's composed property to true. I noticed however that any events I dispatch from a web component's this. make it out of the shadowRoot component just fine, and ones that are dispatched from this.shadowRoot stay inside. So why do I need the "composed" property? Am I doing something wrong?

const internalEvent = new CustomEvent("internalEvent", {bubbles: true, cancelable: false})
const externalEvent = new CustomEvent("externalEvent", {bubbles: true, cancelable: false})

class MyComponent extends HTMLElement {
    constructor() {
        super()
        this.attachShadow({ mode: 'open' })
        this.shadowRoot.innerHTML = `
            <button id="internalButton">INTERNAL</button>
            <button id="externalButton">EXTERNAL</button>
        `
        this.internalButton = this.shadowRoot.getElementById("internalButton")
        this.externalButton = this.shadowRoot.getElementById("externalButton")
    }
    connectedCallback() {
        this.internalButton.addEventListener("click", ()=>{
            this.shadowRoot.dispatchEvent(internalEvent)
        })
        this.externalButton.addEventListener("click", ()=>{
            this.dispatchEvent(externalEvent)
        })
        this.shadowRoot.addEventListener("internalEvent", (event)=>{
            console.log("Internal event detected internally.")
        })
        this.shadowRoot.addEventListener("externalEvent", (event)=>{
            console.log("External event detected internally!")
        })
    }
}

document.addEventListener("internalEvent", ()=>console.log("Internal event detected externally!"))
document.addEventListener("externalEvent", ()=>console.log("External event detected externally."))
customElements.define('my-component', MyComponent)

edit: I'm just struggling to think of any reason where, to get a message to leave your component, you'd prefer to dispatch it within the shadowRoot and add a special property, rather than just dispatching it straight into the light DOM in the first place.


Solution

  • 'this' is the Custom Element/Web Component <my-component>,
    'this' is NOT inside the elements shadowRoot.

    So Events you dispatch from 'this', do not cross shadowDOM boundaries.

    You only need composed: true when Events need to cross (aka "escape") shadowDOM –

    <script>
      const EventName = "HelloFromComponent";
      customElements.define('my-component', class extends HTMLElement {
        constructor() {
          let attach = (btn, composed = false, el = this.shadowRoot.getElementById(btn)) =>
            el.onclick = () => {
              el.dispatchEvent(new CustomEvent(EventName, {
                bubbles: true,
                cancelable: false,
                composed: composed
              }))
            }
          super().attachShadow({mode: 'open'})
                 .innerHTML = `<button id="one">One</button><button id="two">Two</button>`;
          attach("one", /* composed = */ false );
          attach("two", /* composed = */ true  );
        }
        listen(where) {
          where.addEventListener(EventName, (evt) => {
            console.log(where.nodeName, evt.type, evt.composed, );
          })
        }
        connectedCallback() {
          this.listen(this);
          this.listen(document);
        }
      });
    </script>
    <my-component></my-component>