Search code examples
javascripthtmlcustom-element

Attributes in custom element constructor missing dependent on script tag location


I try to get a html custom element to work:

When viewing the following example in the browser I must conclude that this.dataset.text attribute in the constructor() is:

  • present before the script defining the custom element
  • empty after the script defining the custom element

<html>
    <head>
        <title>myimage</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div>
        before custom element definition: <my-element data-text="Replaced and works as expected">Not Replaced</my-element>
        </div>
        <script>
          class MyElement extends HTMLElement {
            constructor() {
              super();
              this.text_attribute= this.dataset.text;
            }

            connectedCallback() {
              if (this.text_attribute) {
                this.innerText = this.text_attribute;
              }
            }
          }
          customElements.define('my-element', MyElement);
        </script>
        <div>
        after custom element definition: <my-element data-text="Replaced">Not Replaced</my-element>
        </div>
    </body>
</html>

This seems to be a common behavior in chrome and Firefox. I can provide some more facts:

  • the this.dataset.text is always present in connectedCallback()
  • accessing the text-attribut via this.getAttribute('data-text') does behave in the same way as above

Can anyone explain the purpose of this seemingly buggy behavior?


Solution

  • This isn't buggy behavior.

    Your first <my-element> is processed by the DOM parser before you define it.
    Thus all attributes are known (in the DOM) when the constructor executes.

    In general you don't access DOM in the constructor; as it also runs when you do:

     let foo = document.createElement("my-element"); // absolutly no DOM created
    

    Here is a trimmed down example:

    <style>
      my-element { display:block }
    </style>
    <my-element id="FOO"> ONE </my-element>
    <script>
      customElements.define('my-element', class extends HTMLElement {
        constructor() {
          super();
          document.body.append(` ► constructor id:${this.id} ◄ `);
        }
        connectedCallback() {
          document.body.append(` ► connected id:${this.id} ◄ `);
        }
      });
    </script>
    <my-element id="BAR"> TWO </my-element>
    <my-element id="BAZ"> THREE </my-element>

    NOTE

    • How FOO innerHTML is written (because it was parsed) to the DOM before its constructor runs

    • How BAR and BAZ constructor run before its innerHTML is written to the DOM
      and the id value doesn't exist yet

    • I have seen many courses where they "save" you from this standard behavior by executing the <script> async, thus after all DOM is parsed. They don't understand the technology...