I'm wondering why new HTMLElement() and others (for example new HTMLDivElement()) throw illegal constructor error.
I want codes like this:
// create a square with a given length
class Square extends HTMLDivElement {
constructor(len) {
super();
this.style.width = `${len}px`;
this.style.height = `${len}px`;
this.style.border = '1px solid black';
}
}
const square = new Square(5);
document.body.appendChild(square);
However, the above code is no less than just a wish, because the above code throws illegal constructor error.
I think there are two workarounds: (1) using $ref (2) using custom element. Please let me show.
(1) Using $ref
class Square {
constructor(len) {
const $div = document.createElement('div');
$div.style.width = `${len}px`;
$div.style.height = `${len}px`;
$div.style.border = '1px solid black';
this.$ref = $div;
}
}
const square = new Square(5); // returns Square object
document.body.appendChild(square.$ref);
This way, I can make a square, but it is inconvenient since I can't access to the div(=$ref) directly. I need to go via 'square' object first. (What I want to say is the difference between appendChild(square) and appendChild(square.$ref)) Because new HTMLDivElement() is not possible, I have no choice but to use an object which acts like a wrapper.
(2) Using (autonomous) custom element -- also uses $ref too.
class Square extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const $div = document.createElement('div');
this.shadowRoot.appendChild($div);
this.shadowRoot.$ref = $div;
}
connectedCallback() {
this.shadowRoot.$ref.style.width = `${this.dataset.len}px`;
this.shadowRoot.$ref.style.height = `${this.dataset.len}px`;
this.shadowRoot.$ref.style.border = '1px solid black';
}
}
customElements.define('custom-square', Square);
const square = document.createElement('custom-square');
square.setAttribute('data-len', 5);
document.body.appendChild(square);
In this case, new Square(5) cannot be done because of the illegal constructor error, so I must use data-len custom attribute and connectedCallback() in order to apply data-len to $ref.style when square is appended to DOM.
Of course I can use both ways, but still I wonder why new HTMLElement() is prohibited. Are there any historical reasons for that?
Also, what makes me wonder is that even though new HTMLElement() throws illegal constructor error, how document.createElement() can make a new instance of HTMLElement (internally)? According to the first answer of this question, HTMLElement.constructor() throws an exception, not creating an object. Then how HTMLDivElement is instantiated and returned to me when I call document.createElement('div')?
In addition, I saw an answer that uses Object.create() method to create an instance of HTMLElement. It works and returns HTMLElement instance, but seems the HTMLElement instance doesn't work correctly. For example,
const test = Object.create(HTMLDivElement.prototype, {});
test.textContent = 'test'; // illegal invocation
For some reason I can get illegal invocation error. Why is that?
CodePudding user response:
The simplest answer is that HTMLDivElement and HTMLElement are interfaces and not all interfaces are constructible.
The reason why these interfaces are not constructible can be found by looking at the inheritance chain for HTMLElement on MDN:
EventTarget ← Node ← Element ← HTMLElement
Since these are DOM interfaces we can find more information about them in the DOM Living Standard. Looking at Node in particular, the DOM standard has these notes:
Node is an abstract interface that is used by all nodes. You cannot get a direct instance of it.
So Node is explicitly not constructible.
Each node has an associated node document, set upon creation, that is a document.
This explains why we need to use document.createElement() to create a HTML element; Node and objects that inherit from it must be associated with a document.
Could the Node, Element, and HTMLElement constructors be changed to take a document parameter? Well, yes, but how would that be any better than document.createElement()?
It’s also interesting to note that going a step further back in the inheritance chain, EventTarget recently was changed to be constructible since it does not have the same document association requirement as Node.
