Search code examples
javascripthtmlweb-componentshadow-dom

How to create a custom element without attachShadow?


Let's say I have some code like this:

class MyElem extends HTMLElement {
  constructor() {
    super();
    
    let templateContent = document.getElementById('template-elem').content;
    this.innerHTML = templateContent.cloneNode(true);
  }
}

window.customElements.define('my-elem', MyElem);
<template id="template-elem">
  <div class="a">
    <div class="b">b</div>
    <div class="c">c</div>
  </div>
</template>

<my-elem></my-elem>

Why doesn't this work? In the Chrome inspector, the custom element has no HTML inside of it. I've also tried doing:

this.append(templateContent.cloneNode(true)); 

but that also resulted in an empty HTML tree.

All the tutorials mention using the shadow DOM like the following:

this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));

and while that works, it forces you to use the Shadow DOM for your custom element. Is there no way of just appending the template's HTML to your custom element without being required to use the Shadow DOM? I'd prefer to just use global CSS styling in my small use-case.


Solution

  • You are falling into multiple traps, like everyone in their first Component adventures.

    1. Custom Elements (strictly speaking only Elements with shadowDOM are Web Components) have lifecycle phases and Callbacks.
      This diagram: https://andyogo.github.io/custom-element-reactions-diagram/ is a MUST to understand.
      You want to add DOM content in the constructor phase; but there is no DOM Element yet in this phase.
      Only in the connectedCallback can DOM content be added.
      With shadowDOM this is another story, its "DocumentFragment" is available in the constructor, you can set content, But it is not a DOM Element yet! The connectedCallback tells you when your Custom Element was attached to the DOM.

    2. Templates content is a DocumentFragment, but your .innerHTML expects a string.
      Since (in your usage) <template> is a DOM element, you can read its innerHTML (see below)


    So, yes Custom Elements without shadowDOM are possible:

    You will see the <template> content twice, demonstrating the 2 ways of adding content.

    <script>
      customElements.define("my-element", class extends HTMLElement {
        connectedCallback() {
          let template = document.getElementById(this.nodeName);
          this.innerHTML = template.innerHTML;
          this.append(template.content.cloneNode(true))
        }
      })
    </script>
    
    <template id="MY-ELEMENT">
      Hello, I am an Element!
    </template>
    
    <!-- the TEMPLATE MUST exist in the DOM for <my-element> to use it! -->
    
    <my-element></my-element>


    The constructor is where you prepare your Element

    This constructor also runs when you do document.createElement("my-element").

    The connectedCallback runs when your Element is added to the DOM (note: the connectedCallback also runs again when you move DOM nodes)

    If you do not specify a method, the method from its Class parent runs, so in the above code the (default) constructor from HTMLElement is executed.
    That is why you need super() in your own constructor... to execute the constructor from HTMLElement.

    Note:

    constructor(){
     let template = document.getElementById("MY-ELEMENT").content.cloneNode(true);
     super().attachShadow({mode:"open"}).append(template);
    }
    

    is totally valid code; Google Documentation that says "super needs to run first" is wrong.
    You need to run super() before you can access the Elements own scope with this

    That is why I prefer:

    constructor(){
    
     // do anything you want here, but you can not use 'this'
    
     super() // Sets AND Returns 'this'
       .attachShadow({mode:"open"}) // both Sets AND Returns this.shadowRoot
       .innerHTML = ` ... `;
    }
    

    OR

    constructor(){
    
     // do anything you want here, but you can not use 'this'
    
     super() // Sets AND Returns 'this'
       .attachShadow({mode:"open") // both Sets AND Returns this.shadowRoot
       .append(document.getElementById(this.nodeName).content.cloneNode(true));
    }
    

    Note append() was not available in IE; so oldskool programmers won't know about its versatility: https://developer.mozilla.org/en-US/docs/Web/API/Element/append

    When your Component adventures are going to involve Class inheritance;
    you call parent methods with:

    connectedCallback(){
      super.connectedCallback()
    }
    

    For a deep dive on the connectedCallback, see My Dev.to post: developers do not connect with the connectedCallback (yet)