Iterate Over Htmlcollection In Custom Element
Solution 1:
The reason will be that connectedCallback()
of a custom element in certain cases will be called as soon as the browser meets the opening tag of the custom element, with children not being parsed, and thus, unavailable. This does e.g. happen in Chrome if you define the elements up front and the browser then parses the HTML.
That is why let inputs = this.getElementsByTagName('spk-input')
in your update()
method of the outer <spk-root>
cannot find any elements. Don't let yourself be fooled by misleading console.log output there.
I've just recently taken a deep dive into this topic, and suggested a solution using a HTMLBaseElement
class:
https://gist.github.com/franktopel/5d760330a936e32644660774ccba58a7
Andrea Giammarchi (the author of document-register-element
polyfill for custom elements in non-supporting browsers) has taken on that solution suggestion and created an npm package from it:
As long as you don't need dynamic creation of your custom elements, the easiest and most reliable fix is to create the upgrade scenario by putting your element defining scripts at the end of the body
.
If you're interested in the discussion on the topic (long read!):
Here's the full gist:
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 https://github.com/w3c/webcomponents/issues/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.
HTMLBaseElement
classHTMLBaseElementextendsHTMLElement{
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 triggeredif ([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:
classMyComponentextendsHTMLBaseElement {
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 accessconsole.log(this.innerHTML)
// when setup is done, make this information accessible to the elementthis.parsed = true// this is useful e.g. to only ever attach event listeners once// to child element nodes using this as a guard
}
}
Solution 2:
The HTMLCollection inputs
does have a length property, and if you log it inside the update function you will see it's value is 2. You can also iterate through the inputs collection in a for loop so long as it's inside the update() function.
If you want to access the values in a loop outside of the update function, you can store the HTMLCollection in a variable declared outside of the scope of the SpektacularInput class.
I suppose there are other ways to store the values depending on what you're trying to accomplish, but hopefully this answers your initial question "How can I iterate over the spk-input elements within spk-root from the update() method?"
classSpektacularInputextendsHTMLElement {
constructor() {
super();
}
}
window.customElements.define('spk-input', SpektacularInput);
let template = document.createElement('template');
template.innerHTML = `
<canvas id='spektacular'></canvas>
<slot></slot>
`;
// declare outside variablelet inputsObj = {};
classSpektacularRootextendsHTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(template.content.cloneNode(true));
}
update() {
// store on outside variable
inputsObj = this.getElementsByTagName('spk-input');
// use in the functionlet inputs = this.getElementsByTagName('spk-input');
console.log("inside length: " + inputs.length)
for(let i = 0; i < inputs.length; i++){
console.log("inside input " + i + ": " + inputs[i]);
}
}
connectedCallback() {
this.update();
}
}
window.customElements.define('spk-root', SpektacularRoot);
console.log("outside length: " + inputsObj.length);
for(let i = 0; i < inputsObj.length; i++){
console.log("outside input " + i + ": " + inputsObj[i]);
}
<spk-root><spk-input></spk-input><spk-input></spk-input></spk-root>
Hope it helps, Cheers!
Post a Comment for "Iterate Over Htmlcollection In Custom Element"