What happens to child nodes if there is no `<slot />`, but a shadow root
Asked Answered
D

1

9

Consider this code:

//js
class FooBar extends HTMLElement {
  constructor(){
    super();
  }
}

customElements.define('foo-bar', FooBar);


<!-- html -->
<foo-bar>
  <h1>Test</h1>
</foo-bar>

This will show »Test« within the browser.

If the constructor is changed to:

constructor () {
  super();
  this.shadow = this.attachShadow({ mode: 'open' }) 
}

The »Test« disappears, since there is a shadow root now.

If the constructor is furthermore changed to

constructor () {
  super();
  this.shadow = this.attachShadow({ mode: 'open' });
  this.shadow.appendChild(document.createElement('slot')); 
}

The »Test« appears again, since there is now a default slot for all child Nodes of <foo-bar>

But what happens to the child nodes if there is no <slot /> within the shadow root. They still appear within this.children and its style.display property remains "". So they are within the dom, but not rendered, even thou thr css tells the opposite? What exactly happens here?

Dybbuk answered 17/5, 2020 at 13:3 Comment(0)
F
5

The full detailed explanation is at: ::slotted CSS selector for nested children in shadowDOM slot


<foo-bar>
  <h1>Test</h1>
</foo-bar>

H1 is lightDOM,
"added" to shadowDOM/root <SLOT> the content is reflected to shadowDOM, NOT moved!!!

H1 always remains in lightDOM :

  • invisible (in the page) in lightDOM for elements with shadowDOM/root,

  • visible (in the page) for Custom Elements without shadowDOM/root

  • unless you move it explicitly with appendChild (or any DOM move operation)

You say: So they are within the dom, but not rendered, even thou CSS tells the opposite?

No, they are rendered, just like any normal DOM element. Just not visible any more.

You can test by including a SCRIPT tag in lightDOM.. it will render and execute!


In code snippets below

You reference lightDOM with this.querySelector("span").innerHTML="weird";

But referencing shadowDOM with this.shadowRoot.querySelector("span").innerHTML="weird";

Does not work, because the DIV (with the SPAN inside) is black-boxed in a <SLOT>

<template id="MY-ELEMENT">
  <style>
    :host {
      display: inline-block;
      font-family: Arial;
    }
    ::slotted(div){
      color:blue;
    }
    ::slotted(span){
      color:gold; /* alas, you can style the 'box', not elements inside */
    }
  </style>
  <h3><slot></slot></h3>
</template>
<style>
  span {
    background:lightcoral; /* from global/host CSS, style slotted content lightDOM */
  }
</style>
<script>
  customElements.define('my-element', class extends HTMLElement {
    constructor() {
      super().attachShadow({mode: 'open'})  
             .append(document.getElementById(this.nodeName).content.cloneNode(true));
    }
  });
</script>
<my-element>
  <div>Hello <span>Component</span> World!</div>
</my-element>

Check the Component in F12 Dev Tools:

Chrome & Firefox:

The DIV is not in shadowDOM/root, remains invisible in lightDOM
all elements/styles will always reflect to shadowDOM/root

click 'reveal' takes you to the lightDOM

So to shadowDOM, slotted content is a black-box of elements & styles;
reflected from lightDOM
that is why ::slotted can only style the box, and not what is inside.

Note: edit that DIV in F12 Console, you will see changes immediately reflect to shadowDOM


SLOTs & lightDOM are LIVE connections

By changing <slot name=...> you can make interactions (think Routes, Tabs, Answers) which previously needed lots more coding (remember those jQuery show/hide days?)

<template id="MY-ELEMENT">
  Custom Element SLOTs are: 
  <slot name=answer></slot>
</template>
<style>
  img { /* style all IMGs in lightDOM */
    max-width: 100vw;
    max-height: 70vh;
  }
</style>
<script>
  customElements.define('my-element', class extends HTMLElement {
    connectedCallback() {
      this.attachShadow({mode: 'open'})
          .append(document.getElementById(this.nodeName).content.cloneNode(true));
      this.onclick = (evt) => {
           const answer = evt.composedPath()[0].innerText; // button label
           this.shadowRoot.querySelector('slot').name = answer;
           this.children[0].slot = answer;//include lightDOM buttons again
      }
    }
  });
</script>
<my-element>
  <span slot=answer><button>Cool</button><button><b>Awesome</b></button><button>Great</button></span>
  <div slot=Cool><img src="https://i.imgur.com/VUOujQT.jpg"></div>
  <span slot=Awesome> <h3>SUPER!</h3></span>
  <div slot=Awesome><img src="https://i.imgur.com/y95Jq5x.jpg"></div>
  <div slot=Great><img src="https://i.imgur.com/gUFZNQH.jpg"></div>
</my-element>

More SLOT related answers can be found with StackOverflow Search: Custom Elements SLOTs

Flow answered 17/5, 2020 at 13:48 Comment(7)
thanks a lot for this sophisticated answer! So attachShadow »switches of the light«, so contained elements in the light DOM become invisible? Is there a JS way to test if a given Element is visible for this scenario?Dybbuk
+1 for switches of the light, I am going to use that one! You can monitor SLOT Change Events... tada.. with the slotchange event: developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/…. If you type: WCSLOT in the StackOverflow searchbox, you get all my SLOT answersCourtenay
He he, that's good to know as well! But if I have a reference to the h1 (document.querySelector('h1')), is there any property telling me that this very element is not visible, because its parent has a shadowRoot without a slot, or, in other words: the h1 itself is not slotted?Dybbuk
AFAIK not (but I am learning every day, I'll investigate this), SLOT tag and slot/name attributes make the connection. content in lightDOM is just content in lightDOM, just junk inside the box. You could make the WebComponent check its lightDOM against content in SLOTs.Courtenay
Here is a background/deep dive Github issue discussion from 2016, where the devs from Google (hayatoito) and Apple (rniwa) comment on slotchange : github.com/w3c/webcomponents/issues/504Courtenay
Thanks again for this comprehensive answer! I guess I have to dive a bit deeper to discover the odds…Dybbuk
"Junk inside the box". Indeed! Everything looks fine - but you are left hunting for the reason it doesn't display. No clue provided. PITA. Include a slot if you want your web-component to display lightDOM children.Gasparo

© 2022 - 2024 — McMap. All rights reserved.