When is a Custom Element constructor invoke? (HTMLTemplateElement.content problem)

Take a look at this simple example (don't bother with this, go to the EDIT)

class MyElement extends HTMLElement {

  customProperty = "something";

  constructor() {
    super();

    console.log("My Element Constructor");
  }
}

customElements.define("my-element", MyElement);

document.body.innerHTML = "<my-element></my-element>";
var myElement = document.querySelector("my-element");
console.log(myElement); // 
console.log(myElement.customProperty); //

The output:

<my-element>
undefined
My Element Constructor

The custom element constructor it is not called (but the HTMLElement constructor it is) until the main call stack is done. Is this an expected behavior or a bug?

Thanks!

EDIT

In an attempt to simplify my real case, I proposed the previous example, which I now see is not appropriate to illustrate my problem (the example does work correctly). Thanks to @connexo's answer I was able to isolate the problem (it's a complex project with many dependencies) and translate it to the following example:

class SubElement extends HTMLElement {

    constructor() {
        super();

        console.log("Sub Element Constructor");
    }
}

customElements.define("sub-element", SubElement);

class MainElement extends HTMLElement {

    constructor() {
        super();

        this.attachShadow({mode: "open"});

        let template = document.createElement("template");
        
        template.innerHTML = "<sub-element></sub-element>";

        this.shadowRoot.appendChild(template.content);

        // The above line doesn't call the Sub Element constructor
        // But the bellow line does it

        // this.shadowRoot.innerHTML = "<sub-element></sub-element>";

        console.log("Main Element Constructor");
    }
}

customElements.define("main-element", MainElement);

var myElement = document.createElement("main-element");

// Sub Component constructor will not be called until
// the parent element is added to the DOM (next line)
// document.body.appendChild(myElement);

For some reason, when the content of a template element is added to a shadowRoot, these elements will not be parsed until the shadowRoot is added to the DOM. But when the innerHTML of the shadowRoot is modified directly, this content is parsed even when the shadowRoot does not belong to the DOM.

Thank you for your time.


Solution 1:

Q: When is a Custom Element constructor invoked?

A: In three situations:

  1. If your element is registered in the custom elements registry before the element gets parsed, its constructor is called when parsing the <my-element part of the HTML. Notice the missing > - for this case this is all the parser has parsed when calling the constructor - no attributes, no children).

  2. If your element is not yet registered in the custom elements registry before the element gets parsed, the constructor gets called right after your custom element registration is done. This is called the upgrade case. Before the upgrade, your custom element to the browser is just an HTMLUnknownElement.

  3. The constructor is also called when you dynamically create your custom element using either new MyElement() or document.createElement('my-element'). Notice that a) in both cases neither attributes nor children exist and b) when creating the element using document.createElement('my-element') the point in time of calling the constructor will be either 1. or 2. (depending on if the custom element has already been registered or not).

Your code will work as expected as long as you make sure it doesn't run before document.body is available.

The easiest way to achieve this is to include your script only immediately before the closing </body> tag.

An easy alternative, if your JS is in an external file, is to include it using the boolean attribute defer:

<script src="./path/to/my/script.js" defer></script>

A 2nd alternative is to wrap your code in a DOMContentLoaded listener (this way it doesn't matter where you put the script tag and you also don't need a defer attribute):

document.addEventListener('DOMContentLoaded', function() {
    class MyElement extends HTMLElement {
    
      customProperty = "something";
    
      constructor() {
        super();
    
        console.log("My Element Constructor");
      }
    }
    
    customElements.define("my-element", MyElement);
    
    document.body.innerHTML = "<my-element></my-element>";
    var myElement = document.querySelector("my-element");
    console.log(myElement); // 
    console.log(myElement.customProperty); //

});

All three methods basically enforce the upgrade case (2.) which in years of professional practice with authoring large web component libraries has proven to be both the most reliable and the most simple approach.

Edit

Since you've now totally changed your question, here's the answer to that:

The <template> HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.

Think of a template as a content fragment that is being stored for subsequent use in the document. While the parser does process the contents of the <template> element while loading the page, it does so only to ensure that those contents are valid; the element's contents are not rendered, however.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template

and

However, the HTMLTemplateElement has a content property, which is a read-only DocumentFragment containing the DOM subtree which the template represents. Note that directly using the value of the content could lead to unexpected behavior, see Avoiding DocumentFragment pitfall section below.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template#attributes