【问题标题】:How to wait for Custom Element reference to be "upgraded"?如何等待自定义元素引用“升级”?
【发布时间】:2016-08-28 22:51:40
【问题描述】:

我有一个元素的引用,该元素将在某个时候升级为自定义元素。如何等待它升级?

例如,假设el 是引用。如果假设它为此目的附加了一个承诺,则代码可能类似于

await el.upgradePromise
// do something after it has been upgraded.

这当然不存在,但描述了我想要做什么。也许没有轮询就没有办法做到这一点?如果我使用轮询,我会轮询什么(假设我没有对应该升级到的类构造函数的引用)。也许我可以轮询el.constructor 并等待它不是HTMLElement,或者等待它不是HTMLUnknownElement

编辑:对于背景,我有一些类似以下的代码,其中使用 setTimeout 是为了让代码正常工作。第一个console.log 输出假,而超时的输出真。

import OtherElement from './OtherElement'

class SomeElement extends HTMLElement {
    attachedCallback() {
        console.log(this.children[0] instanceof OtherElement) // false

        setTimeout(() => {
            console.log(this.children[0] instanceof OtherElement) // true
        }, 0)
    }
}

其中OtherElement 是对将在某个时候注册的自定义元素类的引用。请注意,在我的情况下,我使用的是 Chrome v0 document.registerElement。之所以需要超时,是因为如果SomeElement 像下面的代码一样首先注册,那么OtherElement 还不会被注册,因此如果SomeElement 元素的子元素应该是OtherElement 的一个实例,那么在接下来升级这些元素之前,情况将不会如此。

document.registerElement('some-el', SomeElement)
document.registerElement('other-el', OtherElement)

理想情况下,这样的超时是不可取的,因为如果升级恰好需要更长的时间(由于某些可能取决于浏览器实现的未知原因),那么超时黑客也会失败。

我想要一种绝对的方式来等待升级而不会出现故障,并且尽可能不进行轮询。也许它也需要在一段时间后取消?

编辑:理想的解决方案是让我们等待任何第三方自定义元素的升级,而无需在运行前修改这些元素,也无需在运行时进行猴子补丁。

编辑:从观察 Chrome 的 v0 行为来看,似乎第一次调用 document.registerElement('some-el', SomeElement) 会导致这些元素被升级,并且它们的 attachedCallback 方法在OtherElement 注册之前被触发 ,所以孩子不会是正确的类型。然后,通过延迟逻辑,我可以在孩子也升级为OtherElement 类型之后运行逻辑。

编辑:这是一个显示 the problem 的 jsfiddle,这是一个显示 the timeout hack solution 的 jsfiddle。两者都是用 Chrome Canary 中的 Custom Elements v1 API 编写的,在其他浏览器中不起作用,但是使用 Chrome Stable 的 Custom Elements v0 API 和 document.registerElementattachedCallback 而不是 customElements.define 和 @987654344 时问题是一样的@。 (请参阅两个小提琴中的控制台输出。)

【问题讨论】:

  • 您可以调度自定义事件或从 connectedCallback 设置 Promise。
  • @Supersharp 如果我等待的元素是我的,那将起作用。但是,如果我们使用第三方元素并且不想派生这些元素本身(假设它们是作为 NPM 的包安装的)。理想情况下,该解决方案允许我们等待任何第三方自定义元素的升级,而无需在运行前修改这些元素,也无需在运行时进行猴子补丁。
  • 我不确定你如何在 V0 中做到这一点,但在 V1 中,customElement 对象提供了whenDefined 方法,该方法采用tagName,并返回一个 Promise,当定义了带有 tagName 的 customElement。你可以做window.customElement.whenDefined(e.tagName).then(() => console.log('defined'))
  • @arkanciscan 这行得通,只是我不知道我的自定义元素类的最终用户将定义什么标签名称。我鼓励最终用户使用customElements.define 来定义和使用自定义元素。所以,在那种情况下这行不通。
  • @trusktr 请查看我刚刚添加的答案 - 它显示了在定义/未定义/升级元素时如何轻松获得,而无需提前亲自了解这些自定义元素的 localName :)

标签: javascript dom polymer web-component custom-element


【解决方案1】:

您可以使用window.customElements.whenDefined("my-element"),它返回一个Promise,您可以使用它来确定元素何时升级。

window.customElements.whenDefined('my-element').then(() => {
  // do something after element is upgraded
})

【讨论】:

  • 这个解决方案不起作用,因为作为库作者,我不知道最终用户选择的元素名称,所以我不知道将什么传递给whenDefined 调用。这仅对定义元素名称的人有用。我正在制作一个 WebComponent 基类,它不知道子类将被分配给什么名称。这是一个丑陋的问题,我想知道我是否做错了。也许我需要让子类作者在类属性或其他东西上定义元素(或实例)的名称。
  • @trusktr 查看更新后的编辑,使用通用包罗万象进行扩展
【解决方案2】:

由于sync的html和javascript解析顺序,你只需要等待元素被定义插入。

第一个测试用例 - 插入 HTML,然后定义元素:

<el-one id="E1">
  <el-two id="E2">
  </el-two>
</el-one>
<script>
  // TEST CASE 1: Register elements AFTER instances are already in DOM but not upgraded:
  customElements.define('el-one', ElementOne)
  customElements.define('el-two', ElementTwo)
  // END TEST CASE 1
  console.assert( E1 instanceof ElementOne )
  console.assert( E2 instanceof ElementTwo )
</script>

第二个测试用例 - 元素定义然后插入:

// TEST CASE 2: register elements THEN add new insances to DOM:
customElements.define('el-three', ElementThree)
customElements.define('el-four', ElementFour)
var four = document.createElement('el-four')
var three = document.createElement('el-three')
three.appendChild(four)
document.body.appendChild(three)
// END TEST CASE 2
console.assert( three instanceof ElementThree )
console.assert( four instanceof ElementFour )

class ElementZero extends HTMLElement {
    connectedCallback() {
        console.log( '%s connected', this.localName )
    }
}

class ElementOne extends ElementZero { }
class ElementTwo extends ElementZero { }

// TEST CASE 1: Register elements AFTER instances are already in DOM but not upgraded:
customElements.define('el-one', ElementOne)
customElements.define('el-two', ElementTwo)
// END TEST CASE 1
console.info( 'E1 and E2 upgraded:', E1 instanceof ElementOne && E2 instanceof ElementTwo )


class ElementThree extends ElementZero { }
class ElementFour extends ElementZero { }

// TEST CASE 2: register elements THEN add new insances to DOM:
customElements.define('el-three', ElementThree)
customElements.define('el-four', ElementFour)
const E4 = document.createElement('el-four')
const E3 = document.createElement('el-three')
E3.appendChild(E4)
document.body.appendChild(E3)
// END TEST CASE 2
console.info( 'E3 and E4 upgraded:', E3 instanceof ElementThree && E4 instanceof ElementFour )
<el-one id="E1">
  <el-two id="E2">
  </el-two>
</el-one>

第三个​​测试用例 - 未知元素名称

如果您不知道内部元素的名称是什么,您可以解析外部元素的内容并在每个发现的自定义元素上使用whenDefined()

// TEST CASE 3
class ElementOne extends HTMLElement {
  connectedCallback() {
    var customs = []
    for (var element of this.children) {
      if (!customs.find(name => name == element.localName) &&
        element.localName.indexOf('-') > -1)
        customs.push(element.localName)
    }
    customs.forEach(name => customElements.whenDefined(name).then(() => 
      console.log(name + ' expected to be true:', this.children[0] instanceof customElements.get(name))
    ))
  }
}

class ElementTwo extends HTMLElement {}
customElements.define('el-one', ElementOne)
customElements.define('el-two', ElementTwo)
<el-one>
  <el-two>
  </el-two>
</el-one>

注意如果您必须等待不同的自定义元素升级,您将获得Promise.all() 分辨率。您可能还希望执行更详细的(递归)解析。

【讨论】:

  • 感谢 Supersharp。然而,我的问题是我正在创建一个具有某些功能的基本 WebComponent 类,其中该功能需要在子级升级后运行逻辑。作为库作者,我不是在编写最终用户标记,也不是在控制执行顺序。我只需要一种方法来升级最终用户的子元素,而不知道它们的名称(例如,我不能使用customElements.whenDefined,因为我不知道子元素的名称。也许我需要最终用户以某种方式指定元素名称。
  • 解析是一个有趣的技巧,虽然很昂贵。感谢您分享这个想法!
  • 请注意,这种方法不能解决 customized 内置元素的情况,因为它们的 localname 只是标准元素的名称,不会被拾取通过破折号测试检测。
【解决方案3】:

@trusktr 如果我正确理解您的问题,这完全可以通过创造性地使用 :defined 伪选择器、MutationObserver 和您已经提到的自定义元素方法来实现

const o = new MutationObserver(mutationRecords => {
  const shouldCheck = mutationRecords.some(mutationRecord => mutationRecord.type === 'childList' && mutationRecord.addedNodes.length)

  const addedNodes = mutationRecords.reduce((aN, mutationRecord) => aN.concat(...mutationRecord.addedNodes), [])

  const undefinedNodes = document.querySelectorAll(':not(:defined)')

  if (shouldCheck) { 
    console.info(undefinedNodes, addedNodes);

    [...undefinedNodes].forEach(n => customElements.whenDefined(n.localName).then(() => console.info(`${n.localName} defined`)))
  }
})

o.observe(document.body, { attributes: true, childList: true })

class FooDoozzz extends HTMLElement { connectedCallback () { this.textContent = 'FUUUUUCK' }  }

// Will tell you that a "foo-doozzz" element that's undefined has been added
document.body.appendChild(document.createElement('foo-doozzz'))

// Will define "foo-doozzz", and you event fires telling you it was defined an there are no longer any undefined elements
customElements.define('foo-doozzz', FooDoozzz)

// You'll see an event fired telling you an element was added, but there are no undefined elements still
document.body.appendChild(document.createElement('foo-doozzz'))

【讨论】:

  • 这很有趣。您有没有机会在我的问题底部采取两个小提琴,并使其与您的解决方案一起使用而不是超时?
  • 注意,在类代码中,假设我们不知道我们正在检查的元素的名称(例如,我们只知道类是 ElementTwo,但我们不知道它会被定义为“el-two”,父ElementOne想在升级后检测到子是ElementTwo)
  • 基本上我的问题是,您如何将 setTimeout 替换为 MutationObserver 版本?
【解决方案4】:

再次提出它,因为原始问题尚未得到解决,并且可能需要在此处进行规范增强。

基本上,我和 OP 有同样的问题:在处理 DOM 元素的框架代码中,我需要检测一个未定义的尚未定义的元素,并将其处理推迟到定义时。

james_womack(和其他地方)提出的element.matches(':defined') 是一个好的开始,因为它消除了确定给定元素是否是自定义/自定义的需要,这本身并不有趣。

这种方法解决了 custom 元素的问题:customElements.whenDefined(element.localName)

这对于定制的内置元素是不够的,因为本地名称将只是一个标准节点名称。

在元素上保留is 属性的值可以解决这个问题,但今天的规范并不要求这样做。

因此在执行以下代码时: let element = document.createElement('input', {is: 'custom-input'}); is 属性丢失。

顺便说一句,当有效地执行以下相同的操作时,is 属性将被保留: document.body.innerHTML += '&lt;input is="custom-input"/&gt;'.

恕我直言,is 属性应该保留在程序化创建流程中,从而提供一致的 API 行为并提供等待自定义内置元素定义的能力。

附录:

【讨论】:

    猜你喜欢
    • 2021-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-10
    • 1970-01-01
    • 2020-05-20
    • 2018-04-01
    相关资源
    最近更新 更多