Web Components accessing innerHTML in connectedCallBack
Asked Answered
P

3

4
class Form extends HTMLElement {
    constructor() {
        super()

    }
    connectedCallback() {
        console.log(this)
        console.log(this.innerHTML)

    }
}

customElements.define("my-form", Form);

I'm trying to access the innerHTML now for console.log(this) if I expand it in the console innerHTML is set but when I try console.log(this.innerHTML) it logs nothing.

how come even in the connectedCallback i cant access anything inside my tags.

ultimately what I'm trying to do is

class Form extends HTMLElement {
    constructor() {
        super()
        
    }
    connectedCallback() {
        let inputCounter = 0
        for (let i of this.querySelectorAll("input")) {
            this[inputCounter] = i
            inputCounter++
        }
    }
}

customElements.define("my-form", Form);

but I cant do it because I cant access anything inside the element.

Plashy answered 2/2, 2022 at 0:3 Comment(3)
"it logs nothing" - what do you expect to see in this case?Welloff
Im sorry the html code is <my-form> <input type="text"> <input type="text"> <input type="text"> <input type="text"> <input type="text"> <input type="submit"> </my-form>Plashy
innerHTML: "\n <input type=\"text\">\n <input type=\"text\">\n <input type=\"text\">\n <input type=\"text\">\n <input type=\"text\">\n <input type=\"submit\">\n " innerText: " " this is the log for innHTML in console.log(this)Plashy
E
5

WTF innerHTML is an empty string?!?

<script>
 customElements.define("my-component", class extends HTMLElement {
  connectedCallback(){
    console.log( this.innerHTML.length , " chars in innerHTML" );
  }
 });
</script>

<my-component>Hello Web Component World!</my-component>

HTML only example

Key is to understand when DOM was parsed

In this script below, everyone will agree the first console logs 0, because it executed before the remaining DOM was parsed.

<script>
  console.log(document.querySelectorAll("div").length , "divs");
</script>

<div></div>
<div></div>
<div></div>

<script>
  console.log(document.querySelectorAll("div").length , "divs");
</script>

Logs:

0 divs
3 divs

Web Component connectedCallback

The same applies to the connectedCallback, it fires/fired on the opening tag

So like the first <script> tag in the first example,
all following DOM (3 DIVs in lightDOM) is NOT parsed yet

<script>
  customElements.define("my-component", class extends HTMLElement {
    connectedCallback() {
        const LOG = () => console.log(this.id, this.children.length, "divs");
        LOG();
        setTimeout( () => LOG() );
    }
  });
</script>

<my-component id="Foo">
  <div></div>
  <div></div>
  <div></div>
</my-component>

Logs:

Foo 0 divs
Foo 3 divs

I repeat: the connectedCallback fires on the opening tag.

That means all attributes are available ON the Web Component,
but NOT its three <div> child elements IN lightDOM.

Wait till lightDOM is parsed

Simplest method to get that lightDOM content is to delay execution till the Event Loop is empty again, and you know more (and most likely all) of your lightDOM was parsed.

Background knowledge:

Youtube: Jake Archibald on the web browser event loop, setTimeout, requestAnimationFrame

BUT!
Because the Event Loop can be(come) empty when the (large) DOM is still being parsed!

This gets you the next N elements parsed, not ALL elements in a (large) lightDOM!

Rough tests show around N=1000 (1000 lightDOM elements) are safe to work with.
but your mileage may vary for complex CPU consuming elements
Maybe just increase to 1 millisecond setTimeout delay

requestAnimationFrame (rAF)

requestAnimationFrame can also be used. Read!: setTimeout vs requestAnimationFrame
But because setTimeout triggers later you know more N elements were parsed.

requestAnimationFrame(()=>{
  // ... your code
});

Do watch Jakes video before using rAF!

Potential pitfall: the attributeChangedCallback

!!! The attributedChangedCallback fires BEFORE the connectedCallback for every attribute defined as an observed attribute in static get observedAttributes()

If none of those Observed attributes exist on the Element in the DOM, attributeChangedCallback will not execute.

get ALL children - parsedCallback()

For getting all Child nodes, there is parsedCallback() by WebReflection.
But LOC (Lines Of Code) now goes from 1 to 77 :
https://github.com/WebReflection/html-parsed-element/blob/master/index.js
Maybe good to add this to your own BaseClass. But for small components you are adding more overhead than a setTimeout or rAF takes.

Lifecycle methods in Lit, Stencil, FAST, Hybirds and 61 other tools

Almost all Tools add their own parsedCallback like lifecycle methods:

Saving unexperienced developers headaches

Biggest drawback; you learn a Tool, not the Technology.

Old Mozilla/FireFox bug

Closed bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=1673811

Up until Spring 2021 there where issues with connectedCallback in FireFox always firing late, so all above mentioned issues never happened in FireFox... but do now.

What the experts said

Experts discussion going back to 2016 is here:
https://github.com/WICG/webcomponents/issues/551

Escaping all issues

When Web Components are defined AFTER DOM was created you don't have any of these connectedCallback issues; because all DOM was parsed

So a <script defer src="yourelement.js"> does the job; but will run after all DOM is created,
your components are now created (very) late. So you now have to deal with (more) FOUCs.

Online IDEs

Note CodePen, JSFiddle and all those online IDEs run the JavaScript AFTER the DOM is created!
So you never experience any issues there.
Test your code outside of these online IDEs before you take it to production!

Elsie answered 2/2, 2022 at 7:59 Comment(0)
I
1

To add to @Danny '356CSI' Engelmans answer, there is an npm package available that adds this missing parsedCallback, and instead of relying on the event loop (which might be problematic in edge cases (none of which I could tell off the top of my head)) tries to make sure children are available by using some more complex heuristics like checking if the custom element has a nextSibling (in which case it must have been fully parsed): https://github.com/WebReflection/html-parsed-element

Using a timeout instead of those complex heuristics may be preferrable in case you need to support really old browsers like IE 6-10.

If you're using npm you can simply install it using

import HTMLParsedElement from 'html-parsed-element';

Usage goes like

customElements.define(
  'custom-element',
  class extends HTMLParsedElement {
    parsedCallback() {
      this.innerHTML = 'always <strong>safe</strong>!';
      console.log(this.parsed); // always true here
    }
  }
);

// or use as a mix-in
const {withParsedCallback} = HTMLParsedElement;
customElements.define(
  'other-element',
  withParsedCallback(class extends HTMLElement {
    parsedCallback() {
      this.innerHTML = 'always <strong>safe</strong>!';
      console.log(this.parsed); // always true here
    }
  })
);

This small repository was created following a discussion over on github in which I contributed the blue print of what Andrea Giammarchi (also the author of a well-known (and well-working) custom elements polyfill) has published as HTMLParsedElement.

Interleave answered 5/2, 2022 at 9:46 Comment(0)
M
0

I have spent many hours on this problem, and here is simple solution that works well.

All you need to do is wrap connectedCallback in window.requestAnimationFrame

class Form extends HTMLElement {
  constructor() {
    super()
  }
  connectedCallback() {
    window.requestAnimationFrame(()=>{
      let inputCounter = 0

      for (let i of this.querySelectorAll("input")) {
        this[inputCounter] = i
        inputCounter++
      }
    })
  }
}

customElements.define("my-form", Form);

Using this method, you will always have all child nodes available.

Myoglobin answered 9/2, 2023 at 18:21 Comment(1)
See my updated answer, I don't think rAF gets you all child nodes; since setTimeout fires later than rAF, setTimeout will actually get you more (not all) nodes.Musky

© 2022 - 2024 — McMap. All rights reserved.