Search code examples
javascriptweb-component

HTML Custom Element Child Container


I'm trying to make a classic menubar and playing around with web components at the same time. I've never played with them and this is my first foray into the subject. They seem to be a pretty powerful tool but not much info appears to be available as to what I'm trying to do.

I have a tree of custom html elements which currently looks like this (after the constructor is called):

<menu-bar>
  <sub-menu label="some label">
    <label>some label</label>
    <menu-item></menu-item>
    <menu-item></menu-item>
    <menu-item></menu-item>
  </sub-menu>
</menu-bar>

How can I make the custom element turn this into...

<menu-bar>
  <sub-menu label="some label">
    <label>some label</label>
    <div>
      <menu-item></menu-item>
      <menu-item></menu-item>
      <menu-item></menu-item>
    </div>
  </sub-menu>
</menu-bar>

...when sub-menu's constructor is called? Also is it possible to make the div be part of the shadow dom without the menu-item elements being in the shadow dom?

Relevant Sample Code

class MenuBar extends HTMLElement {
  constructor() {
    super();
  }
};

class SubMenu extends HTMLElement {
  constructor() {
    super();
    let shadowRoot = this.attachShadow({mode: "open"});
    this.labelElement = document.createElement("label");
    shadowRoot.appendChild(this.labelElement);
  }

  static get observedAttributes() {
    return ["label"];
  }
  attributeChangedCallback(pName, pOldValue, pNewValue) {
    switch (pName) {
      case "label":
        this.labelElement.innerHTML = pNewValue;
        break;
    }
  }
};

class MenuItem extends HTMLElement {
  constructor() {
    super();
  }
};

window.customElements.define("menu-bar", MenuBar);
window.customElements.define("sub-menu", SubMenu);
window.customElements.define("menu-item", MenuItem);

Any help is greatly appreciated as I'm just learning how these work and am looking more for details on how manipulating the predefined html using web components would work more so than just a direct answer to this exact example per se.


Solution

  • You need to use <slot> to allow the non-shadowDOM children to show in the shadowDOM of an element.

    Example:

    class MenuBar extends HTMLElement {
      constructor() {
        super();
      }
    };
    
    class SubMenu extends HTMLElement {
      constructor() {
        super();
        let shadowRoot = this.attachShadow({mode: "open"});
        this.labelElement = document.createElement("label");
        let temp = document.createElement("div");
        temp.innerHTML = '<slot></slot>';
        shadowRoot.appendChild(this.labelElement);
        shadowRoot.appendChild(temp);
      }
    
      static get observedAttributes() {
        return ["label"];
      }
      attributeChangedCallback(pName, pOldValue, pNewValue) {
        switch (pName) {
          case "label":
            this.labelElement.innerHTML = pNewValue;
            break;
        }
      }
    };
    
    class MenuItem extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({mode: "open"}).innerHTML = `
        <style>
        :host {
          background-color: #eee;
          border: 1px solid #ddd;
          display:block;
          margin: 1px 0;
        }
        </style>
        <slot></slot>
        `;
      }
    };
    
    window.customElements.define("menu-bar", MenuBar);
    window.customElements.define("sub-menu", SubMenu);
    window.customElements.define("menu-item", MenuItem);
    <menu-bar>
      <sub-menu label="some label">
        <menu-item>1</menu-item>
        <menu-item>2</menu-item>
        <menu-item>3</menu-item>
      </sub-menu>
    </menu-bar>

    In your SubMenu I added the <div> with a <slot> to hold all of the <menu-item> components.

    I also added some minor CSS to make it easier to see the sub elements.