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
setupmethod that takes care of the correct timing (that is, makes sure child nodes are accessible) and then callschildrenAvailableCallback()on the component instance. - a
parsedBoolean property which defaults tofalseand is meant to be set totruewhen 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
}
}
Just wrap
connectedCallbackinwindow.requestAnimationFramefor the same but faster result ;)