3

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.

2
  • "I noticed however that any events I dispatch from a web component's this. make it out of the shadowRoot just fine" quick double-check, did you test this on all browsers that support webcomponents? Also, a webcomponent's this shouldn't be in shadow dom, at least not it's own shadow dom. Commented Jan 20, 2022 at 14:49
  • @JaredSmith Great point, I hadn't, but I just tried it on OSX versions of Chrome, FF and Safari and it works the same on all of them. As to your second point yes, that's kind of what I'm getting at, if I can just dispatch from this why would I not just do that instead of adding extra properties to my event? Commented Jan 20, 2022 at 15:01

4 Answers 4

3

'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);
    }
    disconnectedCallback(){
      // remove any listeners attached *outside* this element!!!
    }
  });
</script>
<my-component></my-component>

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for your answer. I'm afraid I'm quite new to web components so my apologies for getting the language wrong. I understand you might want events from within a web component to escape their component. My question is, when that's the case, why not just dispatch regular events from this instead? As you point out this is not within the shadow DOM and it's available everywhere in your component, so if your event is destined for the outside world why bother dispatching from within the shadow DOM at all?
Because you want to know where Events came from. You can yell from your own kitchen FIRE! Or you can go to the top of the building and yell FIRE! In other words, you want to know WHICH button was clicked, not "something inside shadowDOM" was clicked. Also see MDN docs: developer.mozilla.org/en-US/docs/Web/API/Event/composedPath
Thanks, that's really interesting. I hadn't really considered that case as I thought relying on a child's implementation details and/or state generally leads to unwanted coupling, but I suppose events aren't actually either of those things. I guess it will all become clearer once I've used web components to build something more substantial than toy examples. Thanks for your help.
coupling is the key word. Most of us always did tight coupling. Events are about loose coupling. Think Poker cards on a table, the table shouldn't know where the cards are, instead the cards understand their state (I hate that hype word). A flop/river/turn Event makes the appropriate cards turn.
2

As others have mentioned, this is not in the shadow DOM of your component; it is the component that has this shadow DOM.

if I can just dispatch from this why would I not just do that instead of adding extra properties to my event?

It still wouldn't be able pass any possible surrounding shadowDOM boundaries (your web component may very well be a child or descendant of another web component that utilizes shadow DOM). This may be desirable or not, depending on where you want the event to be monitorable.

Also be aware that connectedCallback can be called multiple times, for example if an element is moved in the DOM; make sure to always remove any event listeners which you added in the connectedCallback in the disconnectedCallback, or even preferable, add the internal listeners in the constructor (which is guaranteed to only ever run once, and saves you the hassle of needing references to the listeners to be able to remove them).

10 Comments

"It still wouldn't be able pass any possible surrounding shadowDOM boundaries" I just tried it though, and it does. If I dispatch a normal event from the this of a web component slotted three levels deep inside other web components it triggers listeners in both parent components and outside of them, just the same as a composed event dispatched from shadowRoot. So still not sure what use the composed property actually has. Thanks very much for the tip re: putting listeners in the constructor btw, good to know! :)
That is incomplete advice; if you do document.addEventListener in the constructor you will still have to remove them in the disconnectedCallback. Only listeners attached "inside" the Element will be garbage collected.
@Danny'365CSI'Engelman That is correct and it's also the practice I've advised in other places here: stackoverflow.com/a/59970158/3744304 . I've added the word internal.
@RogerHeathcote Don't attach event listeners to elements outside of your component's scope (this and (elements in) the shadow root) in the constructor. Those should be attached in the connectedCallback, and properly cleaned up in the disconnectedCallback. Regarding the events, I was talking about custom events, not the standard events. click e.g. has composed: true by default. To try it just paste window.onclick= console.log in any Chrome console and click anywhere in the page.
Here is an (overengineered) JSFiddle playground for (composed) Events: jsfiddle.net/WebComponents/yc3r180m
|
2

You're not doing anything wrong, your events will stop at a shadow boundary when it's reached because the composed flag is off, this includes the externalEvent as soon as my-component is placed in another element's shadow tree. The composed option allows the event to bubble up through these boundaries to the document.

Most custom elements, and the nodes in their shadowRoot, don't have awareness of the branch of the tree they're in, and when that respective position might or might not be in one or more shadowRoots along its ancestry towards the document root. In other words elements are composable both inside shadow trees and not. So if we want those events to pass shadow boundaries the composable option is used. The intent is to separate concerns and control the message passing through the various boundaries to fit the need.

When used with bubbling then the composed path provides the nodes the event passes through--I haven't experimented to see what happens in the different scenarios, like having this stop at the shadow boundary, etc. I have used the event path (an array from event.composedPath()) to have different logic in the handler based on the context (but I don't necessarily recommend this, it's easy to make it overly specific).

this dispatches out to the document no matter if it's from nodes in a shadowRoot, or the shadowRoot, or light dom elements that are within other custom elements with and without their own shadow trees

node.dispatchEvent(new CustomEvent('externalEvent', {detail:{stuff:'things'}, composed: true, bubbles: true}))

https://developer.mozilla.org/docs/Web/API/Event/composed

https://developer.mozilla.org/docs/Web/API/Event/composedPath

Comments

0

It has just dawned on me that one reason (maybe The reason) would be if you wanted the event to be caught both inside AND outside of your component. Without the compose property you'd need to issue two separate events.

1 Comment

if you wanted the event to be caught both inside AND outside of your component I'd say that is a rather rare case. You want events that pierce shadowDOM if that is part of how your component communicates to the outside world.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.