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 callschildrenAvailableCallback()
on the component instance. - a
parsed
Boolean property which defaults tofalse
and is meant to be set totrue
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
}
}
Just wrap
connectedCallback
inwindow.requestAnimationFrame
for the same but faster result ;)