I have standard handlers for the click
, input
and change
events of my customized HtmlInputElement
.
I want to define these handlers like this:
class MyInputElement extends HTMLInputElement {
oninput = (...args) => console.log("input", ...args);
onchange = (...args) => console.log("change", ...args);
onclick = (...args) => console.log("click", ...args);
}
window.customElements.define("my-input", MyInputElement , {
extends: "input"
})
document.body.insertAdjacentHTML("afterbegin", `<input is="my-input" type="text">`)
But this does not work - none of the event handlers is called.
I experimented a bit created this version:
class MyInputElement extends HTMLInputElement {
oninput = (...args) => console.log("input", ...args);
onchange;
constructor() {
super();
this.onchange = (...args) => console.log("change", ...args);
this.onclick = (...args) => console.log("click", ...args);
}
}
window.customElements.define("my-input", MyInputElement , {
extends: "input"
})
document.body.insertAdjacentHTML("afterbegin", `<input is="my-input" type="text">`)
oninput
is the same as in the first example.
onchange
is defined upfront, but initialized in the constructor.
onclick
is only defined in the constructor.
Only the onclick
handler is executed.
Why is that?
Note: I transpile my code from TypeScript with swc through rollup. I found this issue when I switched from @rollup/plugin-swc to rollup-plugin-swc3 - both basically the same configuration.
As it turns out, the MDN Documentation has all the infos (if you scroll down far enough).
Because class fields are added using the
[[DefineOwnProperty]]
semantic (which is essentiallyObject.defineProperty()
), field declarations in derived classes do not invoke setters in the base class. This behavior differs from usingthis.field = …
in the constructor.
class Base {
_foo = null;
get foo() {return this._foo;};
set foo(v) {return this._foo = v;};
trigger() {
if(this._foo) this._foo();
else console.log("_foo is", this._foo);
}
triggerByGetter() {
if(this.foo) this.foo();
else console.log("foo is", this.foo);
}
}
class DerivedBad extends Base {
foo = () => console.log("foo"); // creates a new property basically overriding get/set foo in Base
}
class DerivedGood extends Base {
constructor() {
super();
this.foo = () => console.log("foo"); // this calls set foo in Base
}
}
const good = new DerivedGood();
good.trigger(); // console: "foo"
good.triggerByGetter(); // console: "foo"
const bad = new DerivedBad();
bad.trigger(); // console: "foo is null" <-- this is how browsers seem to do it
bad.triggerByGetter(); // console: "foo"
It seems like the browser executes the event handlers it stores internally - it does not use the getter to retrieve it.
And MDN also knows why different transpilers/versions/configurations create different output:
Note: Before the class fields specification was finalized with the
[[DefineOwnProperty]]
semantic, most transpilers, including Babel and tsc, transformed class fields to theDerivedWithConstructor
form, which has caused subtle bugs after class fields were standardized.
In other words: babel, typescript and swc move(d) the declaration of these fields to the constructor, secretly making it work as I expected. But this is not how these fields were finally specified, so my codebase stopped working as I expected.
So the solution is:
class MyInputElement extends HTMLInputElement {
constructor() {
super();
this.oninput = (...args) => console.log("input", ...args);
this.onchange = (...args) => console.log("change", ...args);
this.onclick = (...args) => console.log("click", ...args);
}
}
window.customElements.define("my-input", MyInputElement , {
extends: "input"
})
document.body.insertAdjacentHTML("afterbegin", `<input is="my-input" type="text">`)