Search code examples
javascripthtmlecmascript-6web-componentnative-web-component

Late "manual" upgrading of an element towards a customized-built-in web component


I have a jQuery plugin (which I don't want to modify) that is dynamically creating a div. Aside from that, I have a webcomponent scrollable-div, which is a customized built-in extended from HTMLDivElement. As I have no control over how that div is created by the jQuery plugin, I need to upgrade it after creation and after it has already been added to the DOM.

class myDiv extends HTMLDivElement {
  constructor(...args) {
    const self = super(...args);
    self.addEventListener('click', (e) => {
      e.target.textContent = 'clicked'
    })
    return self;
  }
}

customElements.define('my-div', myDiv, { extends: 'div' });

document.addEventListener('DOMContentLoaded', () => { 
  // this is where I'm trying to turn the div#upgradeMe into a my-div
  upgradeMe.setAttribute('is', 'my-div');
});
<div id="upgradeMe">Click me</div>

Simply adding the is="my-div" attribute obviously does not do the trick, the div simply stays a regular HTMLDivElement. How can I programmatically upgrade a native element that is already in the DOM to a customized built-in web component?


Solution

  • It's not possible because the element is already created as a standard <div> element and not identified when parsed as upgradable (extendable) due to the lack of the is attribute.

    If the custom element is already defined, the only possible workaround is to replace the existing by a clone (as suggested in the comments by @barbsan).

    The short way:

    1. create a <template> element
    2. copy the div's outerHTML into its innerHTML property
    3. replace the orginal element with the template's content with replaceChild()

    class myDiv extends HTMLDivElement {
      constructor(...args) {
        const self = super(...args);
        self.addEventListener('click', (e) => {
          e.target.textContent = 'clicked'
        })
        return self;
      }
    }
    
    customElements.define('my-div', myDiv, { extends: 'div' });
    
    document.addEventListener('DOMContentLoaded', () => { 
      // this is where I'm trying to turn the div#upgradeMe into a my-div
      upgradeMe.setAttribute('is', 'my-div');
      var t = document.createElement( 'template' )
      t.innerHTML = upgradeMe.outerHTML
      upgradeMe.parentElement.replaceChild( t.content, upgradeMe )
    });
    <div id="upgradeMe">Click me</div>

    Précisions

    When an element is parsed, an is value is affected according to the DOM spec:

    Elements have an associated namespace, namespace prefix, local name, custom element state, custom element definition, is value. When an element is created, all of these values are initialized.

    Only elements with a valid is attribute are identified as customizable:

    An element’s custom element state is one of "undefined", "failed", "uncustomized", or "custom". An element whose custom element state is "uncustomized" or "custom" is said to be defined. An element whose custom element state is "custom" is said to be custom.

    Therefore if the element has no is attribute at parse time, it will not be customizable. That's why you cannot add the is attribute afterward.

    Also in the HTML specs:

    After a custom element is created, changing the value of the is attribute does not change the element's behavior, as it is saved on the element as its is value.

    The is attribute is used only at element creation (at parse time) to initialize the is value and has no effect if changed when the element is already created. In that sense is value is read-only.