在自定义元素中迭代HTMLCollection

4

我该如何在一个自定义元素的影子DOM中迭代另一个自定义元素的实例?HTMLCollections似乎没有按预期工作。(作为一个jQuerian和vanilla js的新手,我肯定在某个地方犯了一个明显的错误)。

HTML

<spk-root>
  <spk-input></spk-input>
  <spk-input></spk-input>
</spk-root>

自定义元素定义

对于 spk-input

class SpektacularInput extends HTMLElement {
  constructor() {
    super();
  }
}
window.customElements.define('spk-input', SpektacularInput);

对于 spk-root

let template = document.createElement('template');
template.innerHTML = `
  <canvas id='spektacular'></canvas>
  <slot></slot>
`;

class SpektacularRoot extends HTMLElement {
  constructor() {
    super();
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
  update() {
    let inputs = this.getElementsByTagName('spk-input')
  }
  connectedCallback() {
    this.update();
  }
}
window.customElements.define('spk-root', SpektacularRoot);

这里有一部分我不太明白。在update()方法中:

console.log(inputs)返回的是一个HTMLCollection:

console.log(inputs)

// output
HTMLCollection []
  0: spk-input
  1: spk-input
  length: 2
  __proto__: HTMLCollection

然而,HTMLCollection无法使用for循环进行迭代,因为它没有长度。

console.log(inputs.length)

// output
0

搜索SO发现HTMLCollections类似于数组,但不是数组。尝试使用Array.from(inputs)或展开运算符将其转换为数组会导致返回一个空数组。

这是怎么回事?如何从update()方法中迭代spk-root中的spk-input元素?

我正在使用gulp-babel和gulp-concat,并在Chrome中使用。如果需要更多信息,请告诉我。提前致谢。


编辑:澄清一下,从update()内调用console.log(inputs.length)输出0而不是2

2个回答

4
原因在于自定义元素的connectedCallback()在某些情况下会在浏览器遇到自定义元素的开始标签时立即调用,而子元素尚未被解析,因此不可用。例如,在Chrome中,如果您预先定义了元素,然后浏览器解析HTML,则会发生这种情况。
这就是为什么您的外部<spk-root>update()方法中的let inputs = this.getElementsByTagName('spk-input')找不到任何元素的原因。不要被误导的控制台输出所欺骗。
我最近深入研究了这个主题,并提出了使用HTMLBaseElement类的解决方案:

https://gist.github.com/franktopel/5d760330a936e32644660774ccba58a7

Andrea Giammarchi(非支持浏览器中自定义元素的document-register-element polyfill的作者)采纳了该解决方案建议并从中创建了一个npm包:

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

只要您不需要动态创建自定义元素,最简单和最可靠的解决方法是通过将定义元素的脚本放置在body的末尾来创建升级方案。
如果您对此主题的讨论感兴趣(长篇阅读!):

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

以下是完整的代码片段:

HTMLBaseElement类解决了在解析子元素之前调用connectedCallback的问题

Web组件规范v1存在一个巨大的实际问题:
在某些情况下,当元素的子节点尚不可用时,会调用connectedCallback
这使得Web组件在它们依赖其子项进行设置的那些情况下无法正常工作。
有关参考,请参见https://github.com/w3c/webcomponents/issues/551
为解决此问题,我们在团队中创建了一个HTMLBaseElement类,该类用作自主自定义元素的扩展。 HTMLBaseElement反过来继承自HTMLElement(自主自定义元素必须在其原型链中的某个时刻派生自该元素)。

HTMLBaseElement 添加了两个内容:

  • 一个 setup 方法来处理正确的时机(即确保子节点可访问),然后在组件实例上调用 childrenAvailableCallback()
  • 一个名为 parsed 的布尔属性,默认值为 false,在组件初始设置完成后应将其设置为 true。这旨在作为一个防护措施,以确保例如子事件侦听器永远不会被多次附加。

HTMLBaseElement

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 once
    // to child element nodes using this as a guard
  }
}

-1

HTMLCollection inputs 具有长度属性,如果在 update 函数内记录它,您将看到其值为 2。只要在 update() 函数内部,您也可以通过 for 循环迭代 inputs 集合。

如果您想在 update 函数之外的循环中访问这些值,可以将 HTMLCollection 存储在在 SpektacularInput 类的作用域之外声明的变量中。

我想,根据您想要实现的目标,可能还有其他存储值的方法,但希望这回答了您最初的问题:“如何在 update() 方法中迭代 spk-root 中的 spk-input 元素?”

class SpektacularInput extends HTMLElement {
  constructor() {
    super();
  }
}
window.customElements.define('spk-input', SpektacularInput);
let template = document.createElement('template');
template.innerHTML = `
  <canvas id='spektacular'></canvas>
  <slot></slot>
`;
// declare outside variable
let inputsObj = {};
class SpektacularRoot extends HTMLElement {
  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 function
    let 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>

希望有所帮助, 干杯!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接