How to wait for Custom Element reference to be “upgraded”?

[亡魂溺海] 提交于 2020-07-19 01:53:07

问题


I have a reference to an element that will at some point be upgraded to a custom element. How do I wait for it to be upgraded?

For example, suppose el is the reference. If it hypothetically had a promise attached to it for this purpose, code could be similar to

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

That of course doesn't exist, but describes what I want to do. Maybe there is no way to do it without polling? If I use polling, what would I poll for (suppose I don't have a reference to the class constructor that it should upgrade to). Maybe I can poll el.constructor and wait for it not to be HTMLElement, or wait for it not to be HTMLUnknownElement?

EDIT: for background, I have some code like the following, where using setTimeout is a hack in order for the code to work. The first console.log outputs false, while the one in the timeout outputs true.

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)
    }
}

where OtherElement is a reference to a Custom Element class that will at some point be registered. Note, I'm using Chrome v0 document.registerElement in my case. The timeout is needed because if SomeElement is registered first as in the following code, then OtherElement will not yet be registered, so therefore if the child of the SomeElement element is expected to be an instance of OtherElement, then that will not be the case until those elements are upgraded next.

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

Ideally, a timeout like that is undesirable, because if upgrade happens to take a longer time (for some unknown reason that could depend on the browser implementation) then the timeout hack will also fail.

I'd like an absolute way to wait for something to be upgraded without possible failure, and without polling if possible. Maybe it needs to be canceled after some time too?

EDIT: The ideal solution would allow us to wait for the upgrade of any third-party custom elements without needing to modify those elements before runtime, and without having to monkey patch then at runtime.

EDIT: From observing Chrome's v0 behavior, it seems like the first call to document.registerElement('some-el', SomeElement) causes those elements to be upgraded and their attachedCallback methods to be fired before the registration of OtherElement, so the children will not be of the correct type. Then, by deferring logic, I can run logic after the children have also been upgraded to be of type OtherElement.

EDIT: Here's a jsfiddle that shows the problem, and here's a jsfiddle that shows the timeout hack solution. Both are written with Custom Elements v1 API in Chrome Canary, and won't work in other browsers, but the problem is the same using Chrome Stable's Custom Elements v0 API with document.registerElement and attachedCallback instead of customElements.define and connectedCallback. (See console output in both fiddles.)


回答1:


You can use window.customElements.whenDefined("my-element") which returns a Promise that you can use to determine when an element has been upgraded.

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



回答2:


Because of sync order of html and javascript parsing, you just have to wait for the elements to be defined and inserted.

1st Test Case - HTML inserted then element defined:

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

2nd Test Case - element defined then inserted:

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

3rd Test Case - unknown element names

If you don't know what are the name of the inner elements, you could parse the content of the outer element and use whenDefined() on every discovered custom element.

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

Note If you have to wait for different custom elements to be upgraded you'll to to way for a Promise.all() resolution. You may also want to perform a more elaborated (recursive) parsing.




回答3:


@trusktr if I understand your question properly, this is absolutely possible through creative use of the :defined pseudo-selector, MutationObserver & the custom elements methods you've already mentioned

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'))



回答4:


Raising it again, since the original problem has not been answered and there probably a case for a spec enhancement here.

I, basically, have the same problem as an OP: in a framework code working on DOM elements I need to detect an undefined yet one and postpone its processing till when defined.

element.matches(':defined') as proposed by james_womack (and elsewhere) is a good start, since it eliminates the need of determining if the given element is custom/customized, which is not interesting in itself.

This approach solves the issue for custom elements:customElements.whenDefined(element.localName).

This is insufficient for customized built-in elements, since local name will be just a standard node name.

Preserving is attribute's value on the element would solve this issue, but it is not required by the spec today.

Therefore when performing the code below: let element = document.createElement('input', {is: 'custom-input'}); the is attribute gets lost.

BTW, when doing effectively the same thing as following, the is attribute is preserved: document.body.innerHTML += '<input is="custom-input"/>'.

IMHO, is attribute should be preserved also in the programmatic creation flow, thus giving a consistent APIs behavior and providing an ability of waiting for definition of customized built-in elements.

Appendix:

  • relevant Chromium issue
  • relevant whatwg/html spec issue


来源:https://stackoverflow.com/questions/39196503/how-to-wait-for-custom-element-reference-to-be-upgraded

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!