native-web-component

How to insert an element into a web component's slot via JavaSCript


In the below examples, foo is the parent component, and bar is the child that fits into foo's slot.

As you can see below I can slot an element if I place it in the HTML directly, but when I try slot an element via JavaScript, it is not slotted, and is not styled by the ::slotted(*) selector. How does one append an element to a web component's slot?

class ComponentFoo extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: "open" });
        this.shadowRoot.appendChild(TEMPLATE_FOO.content.cloneNode(true));

    }
    connectedCallback() {
        const slotEl = this.shadowRoot.getElementById("SLOT");
        const bar = document.createElement("wc-bar");
        bar.textContent = "Bar created via Javascript"
        slotEl.appendChild(bar)  // <--- Does not work
        console.log(bar)
    }
};
window.customElements.define("wc-foo", ComponentFoo);

class ComponentBar extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: "open" });
        this.shadowRoot.appendChild(TEMPLATE_BAR.content.cloneNode(true));
    }
};
window.customElements.define("wc-bar", ComponentBar);
<template id="TEMPLATE_FOO">
  <div>foo</div>
  <slot id="SLOT"></slot>
  <style>
    ::slotted(*) {
        color: red;
        border: 1px solid lime;
    }
  </style>
</template>

<template id="TEMPLATE_BAR">
  <div>bar</div>
</template>

<wc-foo>
    <div>bar created via HTML</div>
</wc-foo>


Solution

  • Because slotted content is NOT moved to a <slot>!

    slotted content is REFLECTED into the slot from its container lightDOM

    So if you assign content to a <slot> it becomes its default content, shown only when there is nothing reflected in the <slot> (thus :slotted won't style it, because there is nothing slotted)

    For :slotted deep dive, see: ::slotted CSS selector for nested children in shadowDOM slot

    const createElement = (t,p={}) => Object.assign(document.createElement(t),p);
    class MyBaseClass extends HTMLElement{
      constructor() {
          super()
           .attachShadow({mode:"open" })
           .append(
             document.getElementById(this.nodeName).content.cloneNode(true),
             createElement("style", {
               innerHTML: `:host{ display:block;margin:5px }`       
             })
           );
      }
    }
    customElements.define("wc-foo", class extends MyBaseClass {
      connectedCallback() {
          this.append(// TO: lightDOM!!!
            createElement("wc-bar", {
              innerHTML: "Bar created via Javascript"
            })      
          ); 
          this.shadowRoot
              .querySelector("slot[name='unused']")
              .append("Hello default content in an empty slot");
      }
    });
    
    customElements.define("wc-bar", class extends MyBaseClass {});
    <template id="WC-FOO">
      <div>div in WC-FOO</div>
      <slot></slot>
      <slot name="unused"></slot>
      <style>
        ::slotted(*) {
            background: pink;
        }
      </style>
    </template>
    
    <template id="WC-BAR">
      <style>* { background: lightgreen } </style>
      <div>div in WC-BAR</div>
      <slot></slot>
    </template>
    
    <wc-foo>
        <div>Line Foo 1</div>
        <wc-bar>Line Foo 2</wc-bar>
    </wc-foo>