JS Custom Element get inner HTML
Asked Answered
E

4

7

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.

Earnestineearnings answered 18/3, 2019 at 6:4 Comment(3)
Possible duplicate of Cannot access attributes of a custom element from its constructorDielle
Here is a lifecycle diagram: andyogo.github.io/custom-element-reactions-diagramCarolanncarole
You can't access your custom element's DOM in the constructor because it has no DOM until it is connected.Ceyx
S
3

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.

Stound answered 26/3, 2019 at 13:59 Comment(1)
Previous link to rules broke; new link: html.spec.whatwg.org/multipage/… (and the post seems to have a full "suggested edit" queue, so I can't update it)Lullaby
H
2

There're 2 ways.

  1. You can define custom element after window.onload and everything should work fine.
window.addEventListener('load', () => {
    customElement.define('my-button', MyButton);
});
  1. set defer attribute in <script>
<script defer src="my-button.js"></script>
Hotbed answered 21/8, 2021 at 10:3 Comment(2)
In 99% of cases you don't need to wait for load. You can almost always do it earlier on DOMContentLoaded.Amine
works perfect, this is the right answer. Just to add, you can call connectedCallback func wrapped in window.requestAnimationFrame(func) as well. connectedCallback() { let func = () { ... }; window.requestAnimationFrame(func) }Johnsson
A
0

If you need to reliably access the children of your custom element, no matter how your custom element script is placed in the document, this is for you: https://github.com/WebReflection/html-parsed-element

It's surprisingly difficult to detect when the parser has parsed the children of a custom element; the HTMLParsedElement does this by repeatedly checking whether there is a nextSibling (which would mean parsing of the current element is done). The current implementation replaced this check with a mutation observer that checks for childList and subtree changes in the custom element's parent element.

Amine answered 21/8, 2021 at 10:14 Comment(0)
O
0

I've found out that when 'connectedCallback()' is being called, the '.innerHTML' is not yet set. BUT it'll be in the 'next tick..

In my custom component(s) it was enough to use 'setTimeout()', just with 0 als 2nd argument (after a function which can access the '.innerHTML').

connectedCallback(... _args)
{
    // obviously, you could also use 'setImmediate()', if available in your browser(!)
    window.setTimeout(() => {
        this._innerHTML = this.innerHTML;
    }, 0);
}

Just for your info. The solution above, with 'requestAnimationFrame', is maybe not that optimal, even if it's 'ok'.. our both's solution is not synchronous, but async should do it in most cases, too. If it's not even better.

Orometer answered 9/10, 2022 at 20:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.