Search code examples
javascriptweb-componentshadow-domcustom-elementslot

duplicated named slot inside shadow dom not working


I have a custom element that looks something like this:

class RepeatMe extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });

        let slot = document.createElement('slot');
        slot.setAttribute('name', 'bar');
        this.shadowRoot.append(slot);

        slot = document.createElement('slot');
        slot.setAttribute('name', 'content');
        this.shadowRoot.append(slot);

        slot = document.createElement('slot');
        slot.setAttribute('name', 'bar');
        this.shadowRoot.append(slot);
    }
}

window.customElements.define('repeat-me', RepeatMe);

And I'm using it as follows:

<repeat-me>
    <div slot="bar">I'm a bar</div>
    <div slot="content">I'm some content</div>
</repeat-me>

I would like to repeat the slot bar in the shadow dom (because the contents are the same at the beginning and at the end), but what I'm getting is that only the first slot is rendered and the second is empty. Is what I'm trying to do possible with shadow dom slots, or do you know some way to achieve something like this?


Solution

  • Nope, SLOTs are unique,
    and multiple lightDOM elements can be slotted to the same SLOT

    Think of SLOTs as your mailbox; would you want your mail to always duplicate to another mailbox?

    If you have that requirement you need a filter to copy emails (or slot content)

    With WebComponents there are 2 options to duplicate:

    1. Clone <span slot="bar"> to other (non-slot) DOM elements inside the shadowRoot

    2. Clone to new lightDOM elements <span slot="duplicate_bar1"> for newly slotted content

    Code below has both options for slotchange Events occuring on <span slot="bar"> (new content in slot)

    1. on slotchange the slot content is cloned in class="duplicate_bar" (let dups = ...)

    2. on slotchange new lightDOM elements are created (let dupslots = ...)

    Note that only method 2. uses <slot> functionality, and you can use :slotted styling

    This code only duplicates content; it does not clean up removed SLOT content.

    <my-element>
      <span slot="bar"> ONE </span>
      <span slot="bar"> TWO </span>
      <span slot="content"> content </span>
    </my-element>
    <template id="MY-ELEMENT">
      <style>
        ::slotted(*) { background: lightgreen }
      </style>
      slot bar: <slot name="bar"></slot>
      <br> slot: content: <slot name="content"></slot>
      <br>Duplicate in SPAN: <span class="duplicate_bar"></span>
      <br>Duplicate in B:<b class="duplicate_bar"></b>
      <br>duplicate_bar1:<slot name="duplicate_bar1"></slot>
      <br>duplicate_bar2:<slot name="duplicate_bar2"></slot>
    </template>
    <script>
      customElements.define('my-element', class extends HTMLElement {
        constructor() {
          let template = id => document.getElementById(id).content.cloneNode(true);
          super().attachShadow({mode: 'open'}).append(template(this.nodeName));
          let slotname = "bar";
          let slot = this.shadowRoot.querySelector(`[name="${slotname}"]`);
          slot.addEventListener("slotchange", (evt) => {
            let assigned = slot.assignedNodes();
            let dups = [...this.shadowRoot.querySelectorAll(".duplicate_" + slotname)];
            let dupslots = [...this.shadowRoot.querySelectorAll(`slot[name*="duplicate_bar"]`)];
            assigned.forEach(node => {
              dups.forEach(el => el.append(node.cloneNode(true)));
              dupslots.forEach(duplicateslot => {
                let newNode = node.cloneNode(true);
                newNode.slot = duplicateslot.name; // set BEFORE adding to DOM! otherwise 'bar' slotchange Events triggers on it
                this.append(newNode);
              })
            });
          });
        }
      });
    </script>