Re: [w3c/webcomponents] Custom Element - untrackable upgrade (#671)

### More context

Just to explain what is the real issue here, consider the following class

```js
customElements.define('direct-case', class extends HTMLElement {
  connectedCallback() { console.log('connected'); }
  get direct() { return this._direct; }
  set direct(value) {
    console.log('this log might never happen');
    this._direct = value;
  }
});
```

Now, assuming I have two `<direct-case>` elements, one already on the body, and one in a template.

```js
document.body.innerHTML = '<direct-case>live</direct-case>';
const tp = document.createElement('template');
tp.innerHTML = '<direct-case>limbo</direct-case>';
```

Now, using the same function, I'd like to set the `direct` property if the element is known.
```js
function setDirectValue(node, value) {
  const name = node.nodeName.toLowerCase();
  if (name === 'direct-case') {
    customElements.whenDefined(name).then(() => {
      node.direct = value;
    });
  }
}

// perform exact same operation
var rand = Math.random();
setDirectValue(document.body.firstChild, rand);
setDirectValue(tp.content.firstChild, rand);
```

You might notice that the log `this log might never happen` happens actually only once, and it's for the live `<direct-case>` node.

What's going on with the node in a limbo?

Well, accessing `tp.content.firstChild.direct` will show the random value ... it seems that after all everything is fine, right?

It's not. That is an own property (expando) on the node and it will shadow its prototype behavior once the node gets promoted.

```js
// so now we have the node on the document
document.body.appendChild(tp.content);

// and indeed it's now the same of the first node we had (true)
document.body.lastChild instanceof document.body.firstChild.constructor;

// but what happens if we set the property again?
document.body.lastChild.direct = Math.random();

// no console log whatsoever ... let's try again with the other node
document.body.firstChild.direct = Math.random();

// "this log might never happen"
// Yeah, the log is shown, everything is fine !!!
```

As summary, without a mechanism to intercept upgrades Custom Elements are potentially doomed for third parts libraries.

We can retrieve their class through their node name but we cannot understand if these are fully functional and, in case these are not, using an `instanceof` operation or any other alternative will not provide a mechanism to interact with elements once upgraded.

However, using `DOMNodeInsterted` seems to trigger right before the upgrade mechanism, so that the following hack seems to be the only way to deal with the current situation.

```js
function setDirectValue(node, value) {
  const name = node.nodeName.toLowerCase();
  if (name === 'direct-case') {
    customElements.whenDefined(name).then(() => {
      const Class = customElements.get(name);
      if (node instanceof Class) {
        node.direct = value;
      } else {
        document.addEventListener(
          'DOMNodeInserted',
          function upgrade(e) {
            if (e.target === node) {
              document.removeEventListener(e.type, upgrade);
              // here node is still not instanceof Class
              // however, a then() / tick will be enough
              // let's use the logic already in place
              setDirectValue(node, value);
            }
          }
        );
      }
    });
  }
}
```

Using above function instead of the previous one, will ensure a correct functionality for the Custom Element and its future prototype definition.

I wouldn't even mind going this way, but using `DOMNodeInserted` produces the following warning in console which is very developer "_un_friendly"

![screenshot from 2017-09-28 15-26-42](https://user-images.githubusercontent.com/85749/30973534-43d71b2c-a465-11e7-95b4-2bc9ec5a977a.png)

### Outstanding issues

  * even if `DOMNodeInserted` triggers before the upgrade, it's impossible to interact with the node, as Custom Element, before the connected or attributechangedcallback happens
  * since previous point is true, we could use `MutationObserver` ignoring its delayed execution 'cause it's going to be too late in any case
  * there is regardless no way third parts libraries can reliably deal with an upgrade from the DOM. Having this exposed through Custom Elements as event or callback would be the ideal solution.

Thanks for your help and patience in reading this through, I hope somebody will chime in at some point and provide some feedback.

Best Regards

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/w3c/webcomponents/issues/671#issuecomment-332867127

Received on Thursday, 28 September 2017 15:09:46 UTC