Failed to construct 'CustomElement' error when JavaScript file is placed in head
Asked Answered
L

4

40

I have a custom element defined like so:

class SquareLetter extends HTMLElement {
    constructor() {
        super();
        this.className = getRandomColor();
    }
}
customElements.define("square-letter", SquareLetter);

When the JavaScript file is included in HTML <head> tag, the Chrome console reports this error:

Uncaught DOMException: Failed to construct 'CustomElement': The result must not have attributes

But when the JavaScript file is included before the </body> ending tag, everything works fine. What's the reason?

<head>
    <script src="js/SquareLetter.js"></script> <!-- here -->
</head>
<body>
    <square-letter>A</square-letter>
    <script src="js/SquareLetter.js"></script> <!-- or here -->
</body>
Lorrainelorrayne answered 7/5, 2017 at 21:25 Comment(0)
T
73

The error is correct, and could occur in both cases. You're getting "lucky" because some current implementations of Custom Elements do not enforce this requirement.

The constructor for a custom element is not supposed to read or write its DOM. It shouldn't create child elements, or modify attributes. That work needs to be done later, usually in a connectedCallback() method (although note that connectedCallback() can be called multiple times if the element is removed and re-added to the DOM, so you may need to check for this, or undo changes in a disconnectedCallback()).

Quoting the WHATWG HTML specification, emphasis mine:

§ 4.13.2 Requirements for custom element constructors:

When authoring custom element constructors, authors are bound by the following conformance requirements:

  • A parameter-less call to super() must be the first statement in the constructor body, to establish the correct prototype chain and this value before any further code is run.

  • A return statement must not appear anywhere inside the constructor body, unless it is a simple early-return (return or return this).

  • The constructor must not use the document.write() or document.open() methods.

  • The element's attributes and children must not be inspected, as in the non-upgrade case none will be present, and relying on upgrades makes the element less usable.

  • The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.

  • In general, work should be deferred to connectedCallback as much as possible—especially work involving fetching resources or rendering. However, note that connectedCallback can be called more than once, so any initialization work that is truly one-time will need a guard to prevent it from running twice.

  • In general, the constructor should be used to set up initial state and default values, and to set up event listeners and possibly a shadow root.

Several of these requirements are checked during element creation, either directly or indirectly, and failing to follow them will result in a custom element that cannot be instantiated by the parser or DOM APIs. This is true even if the work is done inside a constructor-initiated microtask, as a microtask checkpoint can occur immediately after construction.

When you move the script to after the element in the DOM, you cause the existing elements to go through the "upgrade" process. When the script is before the element, the element goes through the standard construction process. This difference is apparently causing the error to not appear in all cases, but that's an implementation detail and may change.

Transmission answered 7/5, 2017 at 22:22 Comment(3)
So unfortunate spec quirk. Its very obvious flaw, become the shadow root can create children or modify attributesConceit
@CanonicEpicure, I think its because the element still doesnt have children in the DOM, and does not have attributes. The shadow root is a different document, so the contract is still maintained (no children create in the DOM or attributes set).Alforja
Note that this also includes things that may not "feel" like attributes, such as setting this.title = ... inside a custom element constructor.Happening
E
10

In most cases, the problem will be that you are trying to create an element that will magically have attributes when it is first added to the DOM, which is not expected behaviour for HTML elements. Think about:

const div = document.createElement("div");
document.body.append(div);

For divs, and all other element types, you will never have a DOM element created that already has attributes like <div class="my-class"></div>. Classes and all other attributes are always added after the element has been created with .createElement(), like:

const div = document.createElement("div");
div.className = "my-class";
document.body.append(div);

.createElement() will never create an element that already has attributes such as classes. The same applies for custom elements. It would therefore be unexpected if:

const myElement = document.createElement("my-element");
document.body.append(myElement);

added a DOM element like <my-element class="unexpected-attribute"></my-element>. You shouldn't be adding attributes like classes to the actual custom element. If you want to add attributes and styling, you should attach children to your element (in a shadowRoot for a web component), for example a div to which you can add whatever attributes you want, whilst leaving your actual custom element free of attributes.

Example:

class SquareLetter extends HTMLElement {
    constructor() {
        super();
        const div = document.createElement("div");
        div.className = getRandomColor();
        this.appendChild(div);
    }
}
customElements.define("square-letter", SquareLetter);

As a web component:

class SquareLetter extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({mode: "open"});
        const div = document.createElement("div");
        div.className = getRandomColor();
        this.shadowRoot.append(div);
    }
}
customElements.define("square-letter", SquareLetter);
Enthral answered 14/9, 2020 at 10:11 Comment(3)
Can you provide some documentation for that? We can't set attributes in normal tags? Why not? Maybe we should be using slots?Wivinia
@Wivinia Jeremy's answer provides references to the docs, I added my answer to provide a more common sense answer, as Jeremy's is quite technical. To answer your question, you can add attributes, but need to do so in the custom element's children (no not using slots). The OP is trying to add a class to the custom element itself, which is not possible. I'll add an example.Enthral
The [custom] element must not gain any attributes or children [in its constructor], as this violates the expectations of consumers who use the createElement or createElementNS methods. Your example violates this rule with respect to element childrenFiester
G
3

I face the same problem when CreatElement with CustomComponent. It Didn't fix until I remove everything in the constructor except super function to connectedCallback function.

Gladysglagolitic answered 5/4, 2020 at 7:19 Comment(0)
H
-2

The element hasn't loaded yet so it cannot be changed, loading the script below the element means it can be changed

Hame answered 7/5, 2017 at 21:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.