Search code examples
javascripthtmlweb-component

Why I can not call method of WebComponent?


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?


Solution

  • 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>