The issue you're facing is essentially the same our team has run into in our current project:
connectedCallback
in Chrome does not guarantee children are parsed. Specifically, relying on children works in the upgrade case, but does not work if the element is known upfront when the browser parses it. So if you place your webcomponents.js
bundle at the end of the body
, it at least reliably works for the static document you have up until then (but will still fail if you create the element programmatically after DOMContentLoaded
using document.write(which you shouldn't anyway)). This is basically what you have posted as your solution.
To make matters worse, there is no lifecycle hook that does guarantee child element access in Custom Elements spec v1.
So if your custom element relies on children to setup (and a simple textNode like your textContent
is a child node), this is what we were able to extract after a week of excessive research and testing (which is what the Google AMP team does as well):
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});
}
}
}
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 to child
// elements once using this as a guard
}
}
customElements.define('my-component', MyComponent)
<my-component>textNode here</my-component>
Update: Already quite a while ago Andrea Giammarchi (@webreflection), the author of the custom elements polyfill document-register-element
(which e.g. is being used by Google AMP), who is a strong advocate of introducing such a parsedCallback
to the custom elements' API, has taken the above code and created a package html-parsed-element
from it, which might help you:
https://github.com/WebReflection/html-parsed-element
You simply derive your elements from the HTMLParsedElement
base class that package provides (instead of HTMLElement
). That base class, in turn, inherits from HTMLElement
.