I wonder why I cannot call a method defined in web-component if I attached this component via .append
instead of using tag name inside the template. Below I am providing few examples. One is not working(throwing an error). I wonder why this first example is throwing this error.
Example 1
const templateB = document.createElement('template');
templateB.innerHTML = `
<h1>ComponentB</h1>
`
class ComponentB extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(templateB.content.cloneNode(true));
}
hello() {
console.log('Hello');
}
}
customElements.define('component-b', ComponentB);
const templateA = document.createElement('template');
templateA.innerHTML = `
<div>
<component-b></component-b>
</div>
`;
class ComponentA extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(templateA.content.cloneNode(true));
this.componentB = this.shadowRoot.querySelector('component-b');
console.log(this.componentB instanceof ComponentB);
this.componentB.hello();
}
}
customElements.define('component-a', ComponentA);
document.body.append(new ComponentA());
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
In this example, I am creating a web-component inside my js file and then directly appending it to document. In this case, I am getting an error that .hello
doesn't exist in my ComponentB
. What's more, the reference to my ComponentB
instance which I get using .querySelector
is NOT an instance of ComponentB
.
Example 2
const templateB = document.createElement('template');
templateB.innerHTML = `
<h1>ComponentB</h1>
`
class ComponentB extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(templateB.content.cloneNode(true));
}
hello() {
console.log('Hello');
}
}
customElements.define('component-b', ComponentB);
const templateA = document.createElement('template');
templateA.innerHTML = `
<div>
<component-b></component-b>
</div>
`;
class ComponentA extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.append(templateA.content.cloneNode(true));
this.componentB = this.shadowRoot.querySelector('component-b');
console.log(this.componentB instanceof ComponentB);
this.componentB.hello();
}
}
customElements.define('component-a', ComponentA);
<component-a></component-a>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
In this example, I am adding a web-component directly to html file. In this case, I am NOT getting an error and the reference to my ComponentB
instance which I get using .querySelector
is an instance of ComponentB
.
Example 3
const templateB = `
<h1>ComponentB</h1>
`;
class ComponentB extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = templateB;
}
hello() {
console.log('Hello');
}
}
customElements.define('component-b', ComponentB);
const templateA = `
<div>
<component-b></component-b>
</div>
`;
class ComponentA extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = templateA;
this.componentB = this.shadowRoot.querySelector('component-b');
console.log(this.componentB instanceof ComponentB);
this.componentB.hello();
}
}
customElements.define('component-a', ComponentA);
document.body.append(new ComponentA());
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
In this example, I am creating a web-component inside my js file and then directly appending it to document. In this case, I am NOT getting an error and the reference to my ComponentB
instance which I get using .querySelector
is an instance of ComponentB
. The only one difference between Example 1 and Example 3 is that here I am using .innerHTML
instead of deep cloned template.
From my point of view Example 1 is correct and should work. Can anybody explain to me why i am mistaken and why it is not working? Maybe you can also provide a solution how I can use <template>
.cloneNode
inside js files to be able to access methods of my web-components created in such a way?
CodePudding user response:
Simple explanation:
.innerHTML
is synchronous
Thus <div><component-b></component-b></div>
is immediately parsed when Component-A is constructed.
.append
with Templates is A-synchronous, it will create the HTML in Component A shadowDOM, but leaves the parsing to later
I cleaned up your code to only show the relevant parts, and added console.log
to show when Component-B is constructed
You can play with the append/append/innerHTML
lines in Component A
(complex explanation) In depth video: https://www.youtube.com/watch?v=8aGhZQkoFbQ
Note: You should actually try and avoid this.componentB.hello
style coding,
as it creates a tight coupling between components. Component A should work even if B doesn't exist yet. Yes, this requires more complex coding (Events, Promises, whatever). If you have tight-coupled components you should consider making them 1 component.
<script>
customElements.define('component-b', class extends HTMLElement {
constructor() {
console.log("constructor B");
super().attachShadow({mode: "open"}).innerHTML = "<h1>ComponentB</h1>";
}
hello() {
console.log('Hello');
}
});
const templateA = document.createElement('template');
templateA.innerHTML = `<div><component-b></component-b></div>`;
customElements.define('component-a', class extends HTMLElement {
constructor() {
console.log("constructor A");
super().attachShadow({mode: "open"})
.append(templateA.content.cloneNode(true));
//.append(document.createElement("component-b"));
//.innerHTML = "<div><component-b></component-b></div>";
this.componentB = this.shadowRoot.querySelector('component-b');
console.assert(this.componentB.hello,"component B not defined yet");
}
});
document.body.append(document.createElement("component-a"));
</script>
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
CodePudding user response:
https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/upgrade
I first noticed that example 1 worked OK if I just added a tiny setTimeout before trying to call the componentB method. With further fiddling I was able to get Example 1 to work mostly as-is, the only change being I called the method described in the documentation link above on the component B reference before trying to access any of its methods.
customElements.upgrade(this.componentB);
I would try to explain but honestly, the nuances and exact timings of Web Component lifecycle hooks are still somewhat befuddling to me. It was obviously an issue of the component not being fully registered in the custom element registry when the first attempt to access its method is being made, I'm just not sure why the other 2 examples work as expected whereas the first example requires an explicit 'upgrading'.