Search code examples
javascripthtmlweb-componentcustom-element

The right place to set attributes in custom elements


Minimum reproduction - the js code part, line 5.

class Bazooka extends HTMLElement {
  host;
  constructor() {
    super();
    this.className = "hi"; // why ?? see bullet number - 5 --> https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance
    this.host = this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    let foo = document.querySelector(".test");
    this.host.appendChild(foo.content);
  }
}

customElements.define("bazooka-show", Bazooka);
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Home</title>
  </head>
  <body>
    <main>
      <bazooka-show></bazooka-show>
    </main>
    <template class="test">
      <p>A shadow DOM.</p>
    </template>
  </body>
</html>

According to the html spec - https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance bullet point - 5, constructor() shouldn't gain attributes right ?

The element must not gain any attributes or children

My question is - why the custom element is accepting an attribute(className)? Shouldn't the browser throw an error ?


Solution

  • It actually does throw when you call document.createElement("bazooka-show"):

    class Bazooka extends HTMLElement {
      host;
      constructor() {
        super();
        this.className = "hi"; // why ?? see bullet number - 5 --> https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance
        this.host = this.attachShadow({ mode: "open" });
      }
    
      connectedCallback() {
        let foo = document.querySelector(".test");
        this.host.appendChild(foo.content);
      }
    }
    
    customElements.define("bazooka-show", Bazooka);
    console.log(document.createElement("bazooka-show"))

    Or if the element has already been registered when the parser meets it:

    <script>
    class Bazooka extends HTMLElement {
      host;
      constructor() {
        super();
        this.className = "hi"; // why ?? see bullet number - 5 --> https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance
        this.host = this.attachShadow({ mode: "open" });
      }
    
      connectedCallback() {
        let foo = document.querySelector(".test");
        this.host.appendChild(foo.content);
      }
    }
    
    customElements.define("bazooka-show", Bazooka);
    </script>
    <bazooka-show></bazooka-show>
    <template class="test">
      <p>A shadow DOM.</p>
    </template>

    The thing here is that when the HTML parser meets your <bazooka-show> tag, your custom element has not been registered yet (because the script will be ran after).
    So when it will create the element, it will create an HTMLUnknownElement, which will get upgraded when the custom element is defined (step 19.).

    When the constructor is constructed in upgrade an element (step 8.3) there is no check regarding the element's attributes. At this point it could even already have attributes, set by the parser.

    On the other hand when we call createElement(), or createElementNS(), or when the element has already been registered when the parser meets it, we'll encounter the step 6.5 of create an element:

    If result’s attribute list is not empty, then throw a "NotSupportedError" DOMException.