迭代自定义元素中的 HTMLCollection [英] Iterate over HTMLCollection in custom element

查看:63
本文介绍了迭代自定义元素中的 HTMLCollection的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何在另一个自定义元素的 shadow dom 中迭代一个自定义元素的实例?HTMLCollections 似乎不像预期的那样表现.(我是一个 jQuerian 并且是一个新手,所以我确定我在某个地方犯了一个明显的错误.

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 novice when it comes to vanilla js, so I'm sure I'm making an obvious error somewhere).

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:


Here's the part I don't understand. Inside the update() method: console.log(inputs) returns an HTMLCollection:

console.log(inputs)

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

然而,HTMLCollection 不能使用 for 循环迭代,因为它没有长度.

However, the HTMLCollection is not iterable using a for loop, because it has no length.

console.log(inputs.length)

// output
0

搜索 SO 发现 HTMLCollections 类似于数组,但不是数组.尝试使用 Array.from(inputs) 或扩展运算符使其成为数组会导致空数组.

Searching SO revealed that HTMLCollections are array-like, but not arrays. Trying to make it an array using Array.from(inputs) or the spread operator results in an empty array.

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

What's going on here? How can I iterate over the spk-input elements within spk-root from the update() method?

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

I'm using gulp-babel and gulp-concat and using Chrome. Let me know if more info is needed. Thanks in advance.

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

Edit: To clarify, calling console.log(inputs.length) from within the update() outputs 0 instead of 2.

推荐答案

原因会是自定义元素的connectedCallback()在某些情况下会在浏览器遇到connectedCallback()strong>自定义元素的开始标记,子元素未被解析,因此不可用.这例如如果您预先定义元素,然后浏览器解析 HTML,就会发生在 Chrome 中.

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.

这就是为什么 letinputs = this.getElementsByTagName('spk-input') 在你的 update() 外部 方法中; 找不到任何元素.不要被那里误导性的 console.log 输出所愚弄.

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.

我最近刚刚深入研究了这个话题,并提出了一个使用 HTMLBaseElement 类的解决方案:

I've just recently taken a deep dive into this topic, and suggested a solution using a HTMLBaseElement class:

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

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

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

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

这里是完整的要点:

Web 组件规范 v1 存在一个巨大的实际问题:

There is a huge practical problem with web components spec v1:

在某些情况下,connectedCallback 在元素的子节点尚不可用时被调用.

In certain cases connectedCallback is being called when the element's child nodes are not yet available.

这会使 Web 组件在依赖其子组件进行设置的情况下无法正常工作.

This makes web components dysfunctional in those cases where they rely on their children for setup.

参见 https://github.com/w3c/webcomponents/issues/551 供参考.

为了解决这个问题,我们在我们的团队中创建了一个 HTMLBaseElement 类,作为扩展自主自定义元素的新类.

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 依次继承自 HTMLElement(自主自定义元素必须从其原型链中的某个点派生).

HTMLBaseElement in turn inherits from HTMLElement (which autonomous custom elements must derive from at some point in their prototype chain).

HTMLBaseElement 添加两件事:

  • 一个 setup 方法,负责处理正确的时间(即确保子节点可访问),然后在组件实例上调用 childrenAvailableCallback().
  • 一个 parsed 布尔属性,默认为 false,并在组件初始设置完成后设置为 true.这是为了充当警卫以确保例如子事件侦听器永远不会附加一次以上.
  • 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.
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
  }
}

这篇关于迭代自定义元素中的 HTMLCollection的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆