Search code examples
javascripthtmlweb-component

Javascript, misunderstanding the lifecycle of a CustomHTMLElement: clarification needed


I am currently studying CustomHtmlElements and I have a misunderstanding about the order in which the constructors are invoked.

For example, if I have two CustomHtmlElements:

class ExForm extends HTMLElement {
  constructor() {
    super();
    console.log(`${this.getAttribute('name')} constructor ${performance.now()}`);
    //debugger;
  }
  
  connectedCallback() {
    console.log(`${this.getAttribute('name')} connected ${performance.now()}`);
  }
}

customElements.define("ex-form", ExForm);

class ExInput extends HTMLElement {
  constructor() {
    super();
    
    console.log(`${this.getAttribute('name')} constructor ${performance.now()}`);
    //debugger;
  }
  
   connectedCallback() {
    console.log(`${this.getAttribute('name')} connected ${performance.now()}`);
  }
}

customElements.define("ex-input", ExInput);
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ex-form name="form1">
      <ex-input name="input1" ></ex-input>
    </ex-form>
    <ex-form name="form2">
      <ex-input name="input2"></ex-input>
    </ex-form>
  </body>
  <script src="./index.js"></script>
</html>

I was expecting that the order of constructors execution would be:

form1, input1, form2, input2

However, when I executed the code, the order was:

form1, form2, input1, input2

Can someone clarify why there is a discrepancy between the order of constructor execution and the order in which the HTML elements are rendered to the page?


Solution

  • It depends on when the Web Component is defined.

    Your JS code in a StackOverflow Code snippet will run after the DOM is parsed, thus will upgrade the existing Custom Elements (initially parsed as HTMLUnknownElement)

    ex-form is defined first, so all existing <ex-form> will be upgraded first.
    Then ex-input is defined, and all existing <ex-input> are upgraded next.

    (see snippet below) If you move the declaration before the DOM is parsed, you get the order you expect:

    In general: don't rely on order in the DOM, you have no control on when the user/developer loads your Web Component file.

    There is customElements.whenDefined( ) if you need to check for dependencies.

    <script>
      class BaseClass extends HTMLElement {
        constructor() {
          super();
          console.log(`constructor ${this.nodeName} `, this.innerHTML);
        }
    
        connectedCallback() {
          console.log(`${this.nodeName} connected ${this.innerHTML}`);
        }
      }
      customElements.define("ex-form", class extends BaseClass {});
      customElements.define("ex-input", class extends BaseClass {});
    </script>
    <ex-form>
      <ex-input></ex-input>
      <ex-input></ex-input>
    </ex-form>
    <ex-form>
      <ex-input></ex-input>
    </ex-form>