Search code examples
javascripthtmlweb-componentshadow-dom

JS Custom Element get inner HTML


We have this custom element defined like so...

<my-button>
   Submit
</my-button>

and the standard customElements definitions

class MyButton extends HTMLElement{
   constructor(){
      super();
      // our custom code...
      this.innerHTML = ''; // ??? where did the 'Submit' go?
   }
}

...

customElements.define('my-button',MyButton);

The problem, when trying to get the innerHTML, we know we can do something like DOMContentLoaded or window.onload.

but sometimes we would like to create the 'my-button' dynamically, using code. and have it "render" upon being appended...

Is there a standard way to do this? does it have something to do with the connectedcallback() and the rest of the 'connected' features?

Thanks!

Please note - I have tried using connectedCallback() as a possible solution, and this does not solve the problem.


Solution

  • There are a set of rules on what you can and can not do in the constructor of a Web Component. They are below.

    But think of this:

    A component can be created one of three ways:

    1. Part of initial page/using innerHTML: When the browser loads the page or when using innerHTML you can add attributes and children to a component as part of the page load or part of innerHTML.
    parent.innerHTML = '<super-hero name="Thor"><super-weapon value="Mjolnir"></super-weapon></super-hero>'.
    
    1. You can call document.createELement to create an element. You can not add attributes or children until after the element is created.
    let superHero = document.createElement('super-hero');
    let superWeapon = document.createElement('super-weapon');
    superHero.setAttribute('name', 'Thor');
    superWeapon.setAttribute('value',  'Mjolnir');
    superHero.appendChild(superWeapon);
    parent.appendChild(superHero)
    
    1. You can instantiate an object using new. Just like document.createElement you must wait until after the element is created before adding attributes and children.
    let superHero = new SuperHero();
    let superWeapon = new SuperWeapon();
    superHero.setAttribute('name', 'Thor');
    superWeapon.setAttribute('value',  'Mjolnir');
    superHero.appendChild(superWeapon);
    parent.appendChild(superHero)
    

    Once the component is created it is added to the DOM and this is when your component's connectedCallback is called.

    And all three of these ways really come down to instantiating with new. document.createElement calls CustomElementRegistry.get to get the constructor for that element and then uses new to create the object.

    And innerHTML parses the HTML and then either calls document.createElement or uses new to create the object.

    But, in doing so, there are NO attributes or children when the constructor for your element is called. In fact, there may not be any children or attributes when connectedCallback is called. This is one reason that observedAttributes and attributeChangedCallback were added in the spec.

    The one thing missing from the spec is knowing is someone has added or changed children either before or after the component was added to the DOM. But, if you really want to know when the children change you can use MutationObserver.

    This is why no children or attributes exists in your constructor. They have not bee added yet.

    Now on to the rules:

    When authoring custom element constructors, authors are bound by the following conformance requirements:

    • A parameter-less call to super() must be the first statement in the constructor body, to establish the correct prototype chain and this value before any further code is run.

    • A return statement must not appear anywhere inside the constructor body, unless it is a simple early-return (return or return this).

    • The constructor must not use the document.write() or document.open() methods.

    • The element's attributes and children must not be inspected, as in the non-upgrade case none will be present, and relying on upgrades makes the element less usable.

    • The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.

    • In general, work should be deferred to connectedCallback as much as possible—especially work involving fetching resources or rendering. However, note that connectedCallback can be called more than once, so any initialization work that is truly one-time will need a guard to prevent it from running twice.

    • In general, the constructor should be used to set up initial state and default values, and to set up event listeners and possibly a shadow root.

    Several of these requirements are checked during element creation, either directly or indirectly, and failing to follow them will result in a custom element that cannot be instantiated by the parser or DOM APIs.