Custom-element could be initialised asynchronously, for instance after lazy loading its definition class.
Lets take a look at this example, we have an element sitting in the DOM that is not initialised yet:
<component-with-async-init></component-with-async-init>
Before initialisation we set a value
property of this element:
querySelector('component-with-async-init').value = "GETTERS SETTERS ARE KILLED"
Then we init this element by defining it as a custom element. As you can see its definition class has getter and setter for value
property.
class ComponentWithAsyncInit extends HTMLElement{
static get is() {
return 'component-with-async-init'
}
get value(){
console.log("Getting value")
return "VALUE FROM GETTER"
}
set value(v){
console.log("Setting value")
}
}
window.customElements.define(ComponentWithAsyncInit.is, ComponentWithAsyncInit);
Getters and setters(or any method) are now killed, as you can see:
querySelector('component-with-async-init').value //"GETTERS SETTERS ARE KILLED"
I observe this behaviour in latest Safari, Firefox and Chrome, others not tested. This problem affects Polymer and Lit-Element libraries as well as pure custom elements. Seems like it works the same way in every browser.
Question: Is it really an expected behaviour? Getters and Setters and any methods get wiped out if a property was set before element definition. If yes, what will be a workaround since we usually can't control when element value is set
Snippet:
document.querySelector('component-with-async-init').value = "SETTERS GETTERS KILLED"
document.querySelector('component-with-async-init').getSomeValue = "GET SOME VALUE KILLED"
class ComponentWithAsyncInit extends HTMLElement{
static get is() {
return 'component-with-async-init'
}
get value(){
console.log("Getting value")
return "VALUE FROM GETTER"
}
set value(v){
console.log("Setting value")
}
getSomeValue(){
return "SOME VALUE"
}
}
window.customElements.define(ComponentWithAsyncInit.is, ComponentWithAsyncInit);
console.log("getSomeValue method:")
console.log(document.querySelector('component-with-async-init').getSomeValue)
console.log("Reading value:")
console.log(document.querySelector('component-with-async-init').value)
<component-with-async-init></component-with-async-init>
That's indeed expected. The HTML specs ask that when a CustomElement is created UAs
- Perform
element.[[SetPrototypeOf]](prototype)
.
Which is the one from ECMAScript standards and which only sets the [[Prototype]]
from which element
will inherit.
Doing this does not override own properties values.
const elem = { a: "my own a" };
Object.setPrototypeOf(elem, { a: "proto's a", b: "proto's b" });
console.log(elem.a) // "my own a"
console.log(elem.b) // "proto's b"
You could try to workaround that by explicitly setting again the properties that have been overridden, and then set back the values so that the setters get called,
document.querySelector('component-with-async-init').value = "SETTERS GETTERS KILLED";
document.querySelector('component-with-async-init').getSomeValue = "GET SOME VALUE KILLED";
class ComponentWithAsyncInit extends HTMLElement {
constructor() {
super();
// first grab the properties that might already have been set
const ownProps = Object.entries(this);
// our class's prototype
const proto = ComponentWithAsyncInit.prototype;
// get all properties descriptors
const proto_desc = Object.getOwnPropertyDescriptors(proto);
ownProps.forEach(([key, value]) => {
// got overriden
if (key in proto_desc) {
// apply the one from our class
Object.defineProperty(this, key, proto_desc[key]);
// set again the own property (call setters)
this[key] = value;
}
});
}
static get is() {
return 'component-with-async-init'
}
get value() {
console.log("Getting value")
return "VALUE FROM GETTER"
}
set value(v) {
console.log("Setting value")
}
getSomeValue() {
return "SOME VALUE"
}
}
window.customElements.define(ComponentWithAsyncInit.is, ComponentWithAsyncInit);
customElements.upgrade(document.querySelector('component-with-async-init'))
console.log("getSomeValue method:") // note that this will get overidden anyway
console.log(document.querySelector('component-with-async-init').getSomeValue)
console.log("Reading value:")
console.log(document.querySelector('component-with-async-init').value)
<component-with-async-init></component-with-async-init>
But that sounds really ugly, and wouldn't prevent things like method calls before they get initialized.
So the best is probably to wait for your elements' registration before using these.
customElements.whenDefined("component-with-async-init")
.then(() => {
document.querySelector('component-with-async-init').value = "SETTERS GETTERS KILLED"
document.querySelector('component-with-async-init').getSomeValue = "GET SOME VALUE KILLED"
console.log("getSomeValue method:") // note that this will get overidden anyway
console.log(document.querySelector('component-with-async-init').getSomeValue)
console.log("Reading value:")
console.log(document.querySelector('component-with-async-init').value)
});
class ComponentWithAsyncInit extends HTMLElement{
static get is() {
return 'component-with-async-init'
}
get value(){
console.log("Getting value")
return "VALUE FROM GETTER"
}
set value(v){
console.log("Setting value")
}
getSomeValue(){
return "SOME VALUE"
}
}
window.customElements.define(ComponentWithAsyncInit.is, ComponentWithAsyncInit);
<component-with-async-init></component-with-async-init>