Search code examples
javascriptcustom-component

Unexpected `Uncaught TypeError: Illegal constructor` when creating a new vanilla Web Component


I'm testing the limits of the JavaScript Web Components API and I encountered an interesting issue.

I have the following wrapper around an HTMLElement object:

export class Wrapper {
  public el: HTMLElement;

  constructor(elementName: string) {
    this.el = document.createElement(elementName);
    customElements.get(elementName) ||
      customElements.define(elementName, HTMLElement);
  }
}

I use this implementation because HTMLElement has a series of limitations I want to bypass, e.g. the constructor must be empty.

Then, I instanciate the wrapper and append the actual element to the DOM:

let wrapper = new Wrapper("magic-component")
document.body.appendChild(wrapper.el);

To my surprise, this throws the following error:

Uncaught TypeError: Illegal constructor

It appears this error happens when a custom element is created via new before being defined via customElements.define(). However, this is not the case here: this.el is regularly created via document.createElement() and then defined via customElements.define(). Only the wrapper is created via new.

class Wrapper {
  constructor(elementName) {
    this.el = document.createElement(elementName);
    customElements.get(elementName) ||
      customElements.define(elementName, HTMLElement);
  }
}

let wrapper = new Wrapper("magic-component")
document.body.appendChild(wrapper.el);


Solution

  • I am not sure what magic you are after.

    With customElements.define you need to properly extend HTMLElement

    class Wrapper {
      constructor(elementName) {
        customElements.get(elementName) ||
          customElements.define(elementName, class extends HTMLElement {
            connectedCallback() {
              this.innerHTML = elementName
            }
          });
        return document.createElement(elementName);
      }
    }
    
    let wrapper = new Wrapper("magic-component")
    document.body.append(wrapper);

    But why the, kinda oldskool, new Wrapper?

    function newElement(elementName) {
      customElements.get(elementName) ||
        customElements.define(elementName, class extends HTMLElement {
          connectedCallback() {
            this.innerHTML = elementName
          }
        });
      return document.createElement(elementName);
    }
    
    document.body.append(newElement("magic-component"));

    Note: once magic-component exists the CustomElementsRegistry; you can also do:

    customElements.define(
      "another-element", 
      class extends customElements.get("magic-component") {
        connectedCallback() {
            this.innerHTML = this.nodeName; // ANOTHER-ELEMENT
        }
    });
    

    Mixins

    If you are after mixins, see: https://component.kitchen/elix/mixins