最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to wait for Custom Element reference to be "upgraded"? - Stack Overflow

programmeradmin3浏览0评论

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

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

Share Improve this question edited Aug 29, 2016 at 23:28 trusktr asked Aug 28, 2016 at 22:51 trusktrtrusktr 45.5k58 gold badges209 silver badges287 bronze badges 5
  • you could dispatch a custom event or set a Promise from the connectedCallback. – Supersharp Commented Aug 29, 2016 at 6:43
  • 1 @Supersharp That would work if the elements I'm waiting for were mine. But, what if we are using third-party elements and don't want to fork those elements perse (suppose they are installed as a package from NPM). The solution would ideally allow us to wait for the upgrade of any third-party custom elements without having modified those elements before runtime, and without having to monkey patch then at runtime. – trusktr Commented Aug 29, 2016 at 20:36
  • 2 I'm not sure how you could do this in V0, but in V1 the customElement object provides the whenDefined method, which takes a tagName, and returns a Promise that resolves when a customElement with that tagName is defined. You could do window.customElement.whenDefined(e.tagName).then(() => console.log('defined')) – Jesse Hattabaugh Commented Aug 29, 2016 at 23:42
  • @arkanciscan That works, except that I don't know what tag name the end user of my custom element class will define. I am encouraging end users to use customElements.define to define and use the custom elements. So, that won't work in that case. – trusktr Commented Sep 25, 2016 at 7:37
  • 1 @trusktr please see the answer I just added—it shows how you can easily get when elements are defined/undefined/upgraded without personally knowing the localName of these custom elements ahead of time :) – james_womack Commented Dec 14, 2017 at 1:18
Add a comment  | 

4 Answers 4

Reset to default 10

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

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.

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

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
发布评论

评论列表(0)

  1. 暂无评论