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.
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());
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
.
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>
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
.
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());
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?
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>";
}
connectedCallback(){
console.log("connectedCallback",this.nodeName);
}
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");
}
connectedCallback(){
console.log("connectedCallback",this.nodeName);
}
});
document.body.append(document.createElement("component-a"));
</script>