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());
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>
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());
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?
Advertisement
Answer
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>