Uncaught TypeError: Illegal constructor when using HTMDivElement class in JS

Viewed 1314

[EDIT (2)] The following change gets rid of the error, but the element does not show up in the browser (however it appears in the 'inspector' (developer tools), see image hereunder):

// test.ts (amended version)


class MyElement extends HTMLElement implements HTMLDivElement {
    align: string;
    constructor() {
        super();
        this.align = "center";
    }
    public connectedCallback(): void {
        this.style.backgroundColor = "red";
    }
}

window.customElements.define('my-element', MyElement);

const myElement = document.createElement("my-element");
myElement.style.width = "100px";
myElement.style.height = "100px";
myElement.style.backgroundColor = "red";

// connectedCallback is called when appended to another element
document.body.appendChild(myElement);

enter image description here

  • I have tried to use the JS HTMLDivElement class with the code hereunder

  • Is it possible to subclass HTMLDivElement?

  • If not, then how does one create (and subclass) a div (or any other HTML element in TS (or JS))?

The command used to compile the TS into JS is:

tsc

tsc version: Version 4.6.2 / Node version: v17.4.0

(with the config file hereunder)

I get the error message (in all browsers dev tools) :

Uncaught TypeError: Illegal constructor.
    MyElement file:///Users/sergehulne/Documents/code/JS/axino2/test.js:3
    <anonymous> file:///Users/sergehulne/Documents/code/JS/axino2/test.js:11
test.js:3:9

// test.ts

class MyElement extends HTMLDivElement {
    constructor() {
        super();
    }
    public connectedCallback(): void {
        this.classList.add(`myElement`);
        this.style.backgroundColor = "red";
    }
}

customElements.define('my-element', MyElement);

const myElement = new MyElement();
// connectedCallback is called when appended to another element
document.body.appendChild(myElement);

// test.js

class MyElement extends HTMLDivElement {
    constructor() {
        super();
    }
    connectedCallback() {
        customElements.define('my-element', MyElement);
        this.classList.add(`myElement`);
        this.style.backgroundColor = "red";
    }
}
const myElement = new MyElement();
// connectedCallback is called when appended to another element
document.body.appendChild(myElement);

// tsconfig.json

{
  "compilerOptions": {
    "target": "es6"
  }
}

// index.html

<!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>
    <script src="test.js"></script>
</body>
</html>
2 Answers

Your code has some problems, and by the looks of it you are unaware that custom elements by default have display: inline, and thus don't accept width or height. This changes when you no longer extend HTMLElement, but instead HTMLDivElement (because that is still a div which by default has display: block, and thus, accepts width and height).

To extend the HTMLDivElement, you need to define and use the element differently using the is-syntax:

class MyElement extends HTMLDivElement {
  connectedCallback() {
    console.log('MyElement alive and kicking');
    this.textContent = this.textContent || 'dynamically created';
    this.style.width = "100px";
    this.style.height = "100px";
    this.style.backgroundColor = "red";
  };
}

customElements.define('my-element', MyElement, { extends: 'div' });

const myElement = document.createElement('div', { is: 'my-element' });

document.body.appendChild(myElement);
<div is="my-element">declaratively created</div>
<hr>

Please note that extending built-in elements is not supported in any Safari version, and probably never will be. If you still want to use it, you'll need a polyfill.

If the only reason you were trying to bring HTMLDivElement into play was to get this block level behaviour (which is by the way only thing you gain when extending it instead of HTMLElement because HTMLDivElement neither has any properties or methods specific to it), you don't have to do that. Just create a so-called autonomous custom element (which has its own tag name), and set its tag to display: block via CSS:

class MyElement extends HTMLElement {
  connectedCallback() {
    console.log('MyElement alive and kicking');
    this.textContent = this.textContent || 'dynamically created';
    this.style.width = "100px";
    this.style.height = "100px";
    this.style.backgroundColor = "red";
  };
}

customElements.define('my-element', MyElement);

const myElement = document.createElement('my-element');

document.body.appendChild(myElement);
my-element { display: block; }
<my-element>declaratively created</my-element>
<hr>

Sidenote:

    constructor() {
        super();
    }

is totally useless and will be marked a linter error in most eslint configurations, see https://eslint.org/docs/rules/no-useless-constructor. Just remove such a constructor, it is not needed because it only does what JS will by default do if no constructor has been explicitly defined.

The following code seems to do the trick to some extents but, as mentioned in the comments, compiling from TS to JS via tsc seems to dump the :implements HTMLDivElement part of the initial test.ts class, as a consequence the width and height of the element are ignored.

The result looks like:

<div style="background-color: red;">My text!</div>

and not like

<div style="background-color: red; width: 100px; height: 100px;">my text!</div>
class MyElement extends HTMLElement implements HTMLDivElement {
    align: string;
    constructor() {
        super();
        this.align = "center";
    }
    public connectedCallback(): void {
        this.style.backgroundColor = "red";
        this.style.width = "100px";
        this.style.height = "100px";
    }
}

window.customElements.define('my-element', MyElement);

const myElement = document.createElement("my-element");
const Content = document.createTextNode("My text!");
myElement.appendChild(Content);
// connectedCallback is called when appended to another element
document.body.appendChild(myElement);
Related