Search code examples
javascriptcustom-element

Understanding customElements processing order


There is a point in time when a parent custom element can access its children before they are blessed with their custom methods.

class CustomParent extends HTMLElement {
  connectedCallback() {
    // works
    this.children[0].textContent = "bar";

    // works
    setTimeout(() => this.children[0].test(), 0);

    // throws a Type error
    this.children[0].test();
  }
}

customElements.define("custom-parent", CustomParent);


class CustomChild extends HTMLElement {
  test() {
    this.textContent = "baz";
  }
}

customElements.define("custom-child", CustomChild);

document.body.innerHTML = `
<custom-parent>
  <custom-child>foo</custom-child>  
</custom-parent>
`;

How can this be possible and is it safe to defer this.children[0].test()?


Solution

  • It's due to the upgrading process of Custom Element.

    1st step: when you execute document.body.innerHTML = '<custom-parent><custom-child>foo</custom-child></custom-parent>' the 2 elements are inserted in the page as unknown elements.

    2nd step: the parent element is upgraded first. It can access its child (and then update its textContent property) as an unknown element. But it cannot access access the custom element test() method... because it is not a custom element yet!

    3rd step: the child element is upgraded immediately after, and now gets a test() method.

    4th step: the deferred test() call logically works :-)

    See the example below. It uses querySelectorAll( ':not(:defined)' ) to show that the child is upgraded after its parent.

    class CustomParent extends HTMLElement {
      constructor() { super() ; console.log( 'parent upgraded') }
      connectedCallback() {
        console.log( 'parent connected', this.children[0].outerHTML )
        // works
        this.children[0].textContent = 'bar'    
        // works
        setTimeout( () => this.children[0].test() )
        // throws a Type error
        try { 
           this.children[0].test() 
        } catch ( e ) { 
          //this shows the parent is upgraded, but not its child 
          var not_upgraded = document.querySelectorAll( ':not(:defined)' )
          console.log( 'not upgraded: ', ...not_upgraded )    
        }    
      }
    }
    
    customElements.define( 'custom-parent', CustomParent )
    
    class CustomChild extends HTMLElement {
      constructor() { super() ; console.log( 'child upgraded') }      
      test() { this.textContent = 'baz' }
    }
    
    customElements.define( 'custom-child', CustomChild ) 
    
    document.body.innerHTML = `
      <custom-parent>
        <custom-child>foo</custom-child>  
      </custom-parent>`