Skip to content Skip to sidebar Skip to footer

Iterate Over Htmlcollection In Custom Element

How can I iterate over instances of one custom element within the shadow dom of another custom element? HTMLCollections don't seem to behave as expected. (I'm a jQuerian and a novi

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:

https://github.com/WebReflection/html-parsed-element

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!):

https://github.com/w3c/webcomponents/issues/551

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 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.

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"