Skip to content

Instantly share code, notes, and snippets.

@franktopel
Last active September 27, 2024 18:22
Show Gist options
  • Save franktopel/5d760330a936e32644660774ccba58a7 to your computer and use it in GitHub Desktop.
Save franktopel/5d760330a936e32644660774ccba58a7 to your computer and use it in GitHub Desktop.
HTMLBaseElement class solving the problem of connectedCallback being called before children are parsed

There is a huge practical problem with web components spec v1:

In certain cases connectedCallback is being called when the element's child nodes are not yet available.

This makes web components dysfunctional in those cases where they rely on their children for setup.

See WICG/webcomponents#551 for reference.

To solve this, we have created a HTMLBaseElement class in our team which serves as the new class to extend autonomous custom elements from.

HTMLBaseElement in turn inherits from HTMLElement (which autonomous custom elements must derive from at some point in their prototype chain).

HTMLBaseElement adds two things:

  • a setup method that takes care of the correct timing (that is, makes sure child nodes are accessible) and then calls childrenAvailableCallback() on the component instance.
  • a parsed Boolean property which defaults to false and is meant to be set to true when the components initial setup is done. This is meant to serve as a guard to make sure e.g. child event listeners are never attached more than once.
class HTMLBaseElement extends HTMLElement {
  constructor(...args) {
    const self = super(...args)
    self.parsed = false // guard to make it easy to do certain stuff only once
    self.parentNodes = []
    return self
  }

  setup() {
    // collect the parentNodes
    let el = this;
    while (el.parentNode) {
      el = el.parentNode
      this.parentNodes.push(el)
    }
    // check if the parser has already passed the end tag of the component
    // in which case this element, or one of its parents, should have a nextSibling
    // if not (no whitespace at all between tags and no nextElementSiblings either)
    // resort to DOMContentLoaded or load having triggered
    if ([this, ...this.parentNodes].some(el=> el.nextSibling) || document.readyState !== 'loading') {
      this.childrenAvailableCallback();
    } else {
      this.mutationObserver = new MutationObserver(() => {
        if ([this, ...this.parentNodes].some(el=> el.nextSibling) || document.readyState !== 'loading') {
          this.childrenAvailableCallback()
          this.mutationObserver.disconnect()
        }
      });

      this.mutationObserver.observe(this, {childList: true});
    }
  }
}

Example component extending the above:

class MyComponent extends HTMLBaseElement {
  constructor(...args) {
    const self = super(...args)
    return self
  }

  connectedCallback() {
    // when connectedCallback has fired, call super.setup()
    // which will determine when it is safe to call childrenAvailableCallback()
    super.setup()
  }

  childrenAvailableCallback() {
    // this is where you do your setup that relies on child access
    console.log(this.innerHTML)
    
    // when setup is done, make this information accessible to the element
    this.parsed = true
    // this is useful e.g. to only ever attach event listeners once
    // to child element nodes using this as a guard
  }
}
@dux
Copy link

dux commented Feb 9, 2023

Just wrap connectedCallback in window.requestAnimationFrame for the same but faster result ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment