1

I have created the following pattern to initialize native web components with JSON information (props). Naming things is hard, so the advanatage of this pattern is that one does not have to add ID's to the script tags, or the web component, to be able to apply the props to the web component.

How it works is that the web component has a slot named initprops. If this slot is slotted with a <script> tag, then parse it as JSON and apply the result as props to the web component.

The following example shows how it works (will discuss the event listener for slotchange below):

class Component extends HTMLElement {
    constructor() {
        super().attachShadow({ mode: "open" });
        const template = document.getElementById("TEMPLATE");
        this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
    connectedCallback() {
        this.initProps();
    }
    set props(json) {
        console.log("Props set to", json);
        for (const value of json) {
            const el = document.createElement("div");
            el.textContent = `Created ${value}`;
            this.appendChild(el);
        }
    }
    // Init props _____________________________________________________________
    getInitPropsSlot() {
        return this.shadowRoot.querySelector('slot[name="initprops"]');
    }
    getInitPropsScriptEl() {
        this.initPropsScriptEl = this.initPropsScriptEl || this.querySelector('[slot="initprops"]');
        return this.initPropsScriptEl;
    }
    applyInitProps() {
        const initPropsScriptEl = this.getInitPropsScriptEl();
        let props = JSON.parse(initPropsScriptEl.textContent);
        this.props = props;
    }
    initProps() {
        if (!this.getInitPropsSlot()) return;
        if (this.getInitPropsScriptEl()) {
            this.applyInitProps();
        } else {
            this.getInitPropsSlot().addEventListener("slotchange", this.applyInitProps.bind(this));
            alert("slot changed saved the day");
        }
    }
}
window.customElements.define("wc-foo", Component);
<template id="TEMPLATE">
  <slot></slot>
  <slot name="initprops"></slot>
</template>

<wc-foo>
   <script slot="initprops" type="application/json">[1, 2, 3]</script>  
</wc-foo>

The problem I had, is that it worked fine on my machine, but in deployment, for some people, not me, sometimes the props would not be applied, and I have seen a screen shot of it. I could reproduce the issue. My thoery is that before the script tag was added to the web component, connectedCallback was executed, meaning it did not see the script tag.

According to MDN:

connectedCallback: Invoked each time the custom element is appended into a document-connected element. This will happen each time the node is moved, and may happen before the element's contents have been fully parsed.

Emphasis mine, on that connectedCallback may happen before the element's contents have been fully parsed. So to solve this issue I added the event listener for slotchange.

Now my question is will the web component always detect and apply the props within the <script> tag now?

I ask this because I was wondering about the following: Does the browser populate the tree asynchronously to running the JavaScript (sounds like it from the MDN quote above)? Could there be a race condition that happens like this:

  1. connectedCallback fired, but <script> tag not parsed and inserted into the DOM yet, hence the props are not applied.
  2. While the JavaScript creates the event listener for slotchange, the browser parsers the <script> tag and appends it to the DOM.
  3. Now the event listener never fires because in the short time between when it did not see the slotchange and adding the slotchange event listener, the <script> was added to the DOM.
run_the_race
  • 1,344
  • 2
  • 36
  • 62
  • 1
    Not **may happen** but **will happen**. The ``connectedCallback`` fires on the **opening** tag. So you have to wait till all Custom Element DOM is parsed (closing tag) See: https://stackoverflow.com/questions/61971919/wait-for-element-upgrade-in-connectedcallback-firefox-and-chromium-differences – Danny '365CSI' Engelman Nov 18 '22 at 16:34
  • @Danny'365CSI'Engelman The answer in the linked SO post says "This means connectedCallback will generally be invoked when the element has zero children", strangely enough connected callback for me was seeing the child script element almost every time, sound like the reverse should be true, using Chrome 107. – run_the_race Nov 19 '22 at 06:40
  • 1
    That happens when you **define** your WC **after** it was **parsed** in the DOM. That is why some blogs tell you to load your `` – Danny '365CSI' Engelman Nov 19 '22 at 07:44
  • Interesting. For each custom component, I have a link in the `` like this: ` – run_the_race Nov 19 '22 at 08:09
  • In the long github issue from your linked post (https://github.com/WICG/webcomponents/issues/551), I see https://github.com/WebReflection/html-parsed-element/blob/master/esm/index.js was updated in 2021, why are they using such a complicated solution is they just need a `setTimeout`? From what I unserstand is they are trying to provide a callback for when all the child nodes are ready, which is what your code below does. – run_the_race Nov 19 '22 at 08:10
  • I have never gotten an answer to that question. Mind you, documentation (and all blogs) also says _always use ``super()`` first_. Which is plain wrong: [Web Components Lesson 102](https://dev.to/dannyengelman/web-component-102-the-5-lessons-after-learning-web-components-101-h9p) – Danny '365CSI' Engelman Nov 19 '22 at 08:55
  • 1
    (very) strictly speaking _provide a callback when child nodes are ready_ is **not** what ``setTimeout`` does. setTimeout kicks in when the Event Loop is empty; it does not keep an eye on child nodes. But the Event Loop is "busy" while parsing... Subtle.. in my answer I say **==** DOM is parsed; not **===** DOM is parsed – Danny '365CSI' Engelman Nov 19 '22 at 09:09
  • I did some playing with HTMLParsedElement. You are getting access to innerHTML earlier because it overloads some stuff. ``requestAnimationFrame`` is second, ``setTimeout`` is third. But you are talking 1 or 2 microseconds. And occasionally the order is different. So its like this years Formula1 racing. Max Verstappen wins.. but not in every race. Its up to the developer if adding a dependency (which also takes app .3 microseconds to execute (not counting loading those bytes)) is worth the potential win. All 3 methods deliver what you want: You cross the finish line. – Danny '365CSI' Engelman Nov 20 '22 at 14:44
  • @Danny'365CSI'Engelman Thank you! I definately am looking for the Toyota corolla solution, less code maintenance, less complexity. I don't understand why others are willing to introduce so much complexity for a few microsends. I have gone with the `setTimeout` solution, thank you! – run_the_race Nov 21 '22 at 07:04

1 Answers1

2

Per comments; you have to wait till DOM is parsed:

customElements.define("wc-foo", class extends HTMLElement {
    connectedCallback() { // fires on the *opening* tag
       setTimeout(()=>{ // so wait till the Event Loop is done (==DOM is parsed)
         this.props = JSON.parse(this.innerHTML);
       });
    }
    set props(json) {
        console.log("Props set to", json);
        for (const value of json) {
            this
             .appendChild(document.createElement("div"))
             .textContent = `Created ${value}`;
        }
    }
});
<wc-foo>
   [1, 2, 3]
</wc-foo>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49