Skip to content
Advertisement

Why does customElements.upgrade appear to not upgrade this custom element?

I have a situation similar to the example below: from a template element, I clone a node tree containing custom elements. One custom element is passed data during initialization, represented here by the line infobox.setData(getData()). The function I use to pass the data (setData) is added by my custom class, so I make sure the custom element is upgraded before calling it, by passing the node tree to customElements.upgrade. (I have also tried passing infobox itself.)

Unfortunately, when I try running my code or the example below I receive the error infobox.setData is not a function. I have confirmed infobox instanceof InfoBox is false and the element has no custom properties or methods prior to being connected to the document, so it seems customElements.upgrade is not upgrading the elements in the tree. What might be preventing it from doing so?

document.getElementById('addWidgetErr').onclick = addWidgetErr
document.getElementById('addWidgetWorkaround').onclick = addWidgetWorkaround

class InfoBox extends HTMLElement {
  _data = ""

  connectedCallback() {
    this.render()
  }

  setData(val) {
    this._data = val
    this.render()
  }

  render() {
    if (!this?.isConnected) {
      return
    }

    this.replaceChildren(...this._data.split(' ').map(datum => {
      const el = document.createElement('span')
      el.innerText = datum
      return el
    }))
  }
}
customElements.define('info-box', InfoBox)

function addWidgetErr() {
  const widget = document.getElementById('widgetTemplate').content.cloneNode(true)
  const infobox = widget.querySelector('info-box')

  // From my understanding, this should convert all unknown elements in `widget`
  //   into custom elements.
  customElements.upgrade(widget)

  console.assert(!(infobox instanceof InfoBox))
  console.assert(!('setData' in infobox))
  // TypeError: infobox.setData is not a function
  infobox.setData(getData())

  document.getElementById('container').append(widget)
}

function addWidgetWorkaround() {
  const widget = document.getElementById('widgetTemplate').content.cloneNode(true)
  const infobox = widget.querySelector('info-box')

  document.getElementById('container').append(widget)

  // works because infobox has been upgraded after being added to the document
  infobox.setData(getData())
}

function getData() {
  return ('lorem ipsum dolor sit amet consectetur adipiscing elit proin at ' +
    'vestibulum enim vestibulum ante ipsum primis in faucibus orci luctus')
}
#container {
  background: lightgrey;
  padding: 2em;
}

info-box {
  display: flex;
  flex-flow: row wrap;
  gap: .5em;
  padding: .5em;
  background: darkgrey;
}

info-box>span {
  background: lightblue;
  border-radius: .5em;
  padding: .5em;
}
<template id="widgetTemplate">
    <details>
        <info-box></info-box>
    </details>
</template>

<button id="addWidgetErr">Add via `upgrade`</button>
<button id="addWidgetWorkaround">Add post-hoc</button>
<div id="container"></div>

Advertisement

Answer

I narrowed it down to <template> / cloneNode

Submit it here: https://github.com/WICG/webcomponents/issues
(Your credits if it is a bug, in other words I don’t have a clue 🙂

class myEl extends HTMLElement {}

let el1 = document.createElement("my-el");
let el2 = Object.assign(document.createElement("template"), {
            innerHTML: "<my-el></my-el>"
          }).content.cloneNode(true).querySelector("my-el");

customElements.define("my-el", myEl);

customElements.upgrade(el1); // works as documented
customElements.upgrade(el2); // doesn't upgrade!

console.assert(el1 instanceof myEl, "element not upgraded");
console.assert(el2 instanceof myEl, "template not upgraded");

Update: submitted by OP as issue: https://github.com/WICG/webcomponents/issues/946#issuecomment-1200377464

Advertisement