Skip to content
Advertisement

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?

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