Search code examples
javascriptdomweb-componentcustom-element

How to use a custom element to wrap child custom elements into a div


I'm trying to create a wrapper Custom Element that wraps its child Custom Elements into a div.

But the child elements aren't wrapped. Instead, an empty div is inserted into the wrapper element before the child elements

<script>
  class ListItem extends HTMLElement {
    constructor() {
      super();
    }

    connectedCallback() {
      this.innerHTML = "<div>ListItem</div>";
    }
  }

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

    connectedCallback() {
      this.innerHTML = `<div class="list">${this.innerHTML}</div>`;
    }
  }

  customElements.define("list-item", ListItem);
  customElements.define("my-list", List);
</script>

<my-list>
  <list-item></list-item>
  <list-item></list-item>
  <list-item></list-item>
</my-list>

This is the result:

<my-list>
  <div class="list"></div>
  <list-item><div>ListItem</div></list-item>
  <list-item><div>ListItem</div></list-item>
  <list-item><div>ListItem</div></list-item>
</my-list>

I would have expected the following:

<my-list>
  <div class="list">
    <list-item><div>ListItem</div></list-item>
    <list-item><div>ListItem</div></list-item>
    <list-item><div>ListItem</div></list-item>
  </div>
</my-list>

You can try it out here.


Solution

  • It's due to the parsing execution sequence. When the <my-list> tag is detected, it is created (and connected) immediately, before its children are inserted.

    See issues addressed here WICG Web Components Issue #809 and here MDN connectedCallback.

    As a consequent ${this.innerHTML} will return an empty string in connectedCallback().

    You could wait for the children to be parsed, for example with the help of setTimeout():

    class List extends HTMLElement {
        connectedCallback() {
            setTimeout( () => 
                this.innerHTML = `<div class="list">${this.innerHTML}</div>` 
            )
        }
    }
    

    But you'd better use Shadow DOM with <slot> to insert elements of the light DOM:

    class List extends HTMLElement {
        connectedCallback() {
            this.attachShadow( { mode: 'open' } )
                .innerHTML = `<div class="list"><slot></slot></div>` 
        }
    }
    

    See the example below.

    class ListItem extends HTMLElement {
        connectedCallback() {
            this.innerHTML = "<div>ListItem</div>";
        }
    }
    
    class List extends HTMLElement {
        connectedCallback() {
            this.attachShadow( { mode: 'open' } )
                .innerHTML = `<div class="list"><slot></slot></div>` 
        }
    }
    
    customElements.define("list-item", ListItem);
    customElements.define("my-list", List);
    <my-list>
        <list-item></list-item>
        <list-item></list-item>
        <list-item></list-item>
    </my-list>