Disclaimer: This blog post suggests using
setTimeout
This post was originally written as StackOverflow answer in February 2022:
WTF? this.innerHTML
is an empty string in the connectedCallback
Ping back: https://dbushell.com/2024/06/15/custom-elements-unconnected-callback/
When was DOM parsed?
Key is to understand when DOM was parsed
In the script below, everyone will agree the result tab will show:
because the first script executed before the remaining DOM was parsed.
the connectedCallback
fires on the opening tag!
Then how do you think the DOM Parser handles this code?
component-1
is defined before DOM is parsedcomponent-2
is defined after DOM is parsed
Read the code below, then click the Result Tab
If you understand why the answer is:
You can stop reading here.
component-1 connectedCallback()
Because the connectedCallback
fires on the opening tag! all following DOM (DIVs in lightDOM) is NOT parsed yet
That means all component-1
attributes (id in above code) 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.
With a setTimeout
Optional background knowledge:
Youtube: Jake Archibald on the web browser event loop, setTimeout, requestAnimationFrame
component-1
setTimeout
executes after DOM is parsed (the DIVs in lightDOM)
Also note (in the above Results Tab) that component-1
wrote its output after component-2
, because of the setTimeout
in component-1
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 10 milliseconds setTimeout delay
Sidenote: Should a Web Component with a 1000 Childnodes really be one Web Component?
requestAnimationFrame (rAF)
requestAnimationFrame
can also be used. Read!:
https://stackoverflow.com/questions/71523029/settimeout-vs-requestanimationframe
rAF
fires before or after setTimeout
. I my tests setTimeout
already could access that lightDOM
Do watch Jakes video before using rAF!
https://www.youtube.com/watch?v=cCOL7MC4Pl0
Potential pitfall: the attributeChangedCallback
!!! The attributedChangedCallback
fires BEFORE the connectedCallback
for every attribute
defined as an observed attribute in static get observedAttributes()
which is declared as initial attribute on your Custom Element.
If none of those Observed attributes exist on the Element in the DOM, attributeChangedCallback
will not execute.
setTimeout
gets you the next N elements
N can be scary for a developer only used to digital 0 and 1 states
If you can't deal with N
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:
- https://stenciljs.com/docs/component-lifecycle
- https://lit.dev/docs/components/lifecycle/#custom-element-lifecycle
Saving unexperienced developers headaches
Biggest drawback; you learn a Tool, not the Technology.
And your code now executes (way) later than that setTimeout
, so more (potential) FOUCs and Layout Shifts to deal with.
What the experts said
Experts discussion has been going on since 2016
That is nearly a decade now
The issue is obviously not that big it needs a solution
Not that everyone is aware of the issue...
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.
Escaping all issues
9 out 10 devs will not understand why they fixed the "bug"
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.
This also applies to <script type="module">
and other ways of importing modules.
Your script will most likely be executed after DOM was parsed.
Best advice is to just make your Web Components work for the before scenario.
Online IDEs
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!
Conclusion
All together now!
the connectedCallback
fires on the opening tag!
I use setTimeout
in Web Components where I really need that lightDOM
Never had an issue, since I started with Web Components in 2017
There are plenty of other workarounds using readystatechange
, MutationObserver
, Promisses
possible.
Use whatever you think suits you best. It is all about DOM having been parsed or not.
Took me 3 hours to format this blog-post. No need to buy me coffee, or send me money. I am happy enough if you just send some positive karma into this f*ing world.
Top comments (11)
So, the best thing to do is just putting all the JavaScript code on the bottom of the body tag, as always, let the browser create a myriad of
UnknownHTMLElement
instances, the let it upgrade all instances whencustomElement.define()
triggers. That opens the door to inability to interact and FOUC (en.wikipedia.org/wiki/Flash_of_uns...) for me, and yet it seems to be the best approach.When I first started to learn about web components, I already knew Angular so I thought "ok
connectedCallback()
is likengOnInit()
" and actually it is, because if you need to read the parsed children you need to callngAfterViewInit()
andngAfterContentInit()
which are Angular-specific methods to interact with inner HTML, notngOnInit()
. Still,connectedCallback()
seemed to be used by other developers more like "run this when the DOM is ready to be queried" more than "run this when the component is attached to the DOM, but maybe it's not ready yet". I guess it's just a little confusing all around and frameworks do a much better job than "standards" in giving simple flows and guarantees for me.React is different, because a
useEffect()
without dependencies actually triggers after any first render (I'd say almost like asetTimeout()
) and it actually works as intended.Wouldn't a much simpler
"readystatechange"
event listener get you like 98% of the way there?Like, I imagine something like (untested):
Edit: As I am working on a custom element right now, I decided to try this, and noticed two things:
EDIT: Added a
console.log
to both examples to make it clearer where the actual initialisation code goes.Depends how you do it, I guess?
I assume you're mostly considering using
innerHTML
here, which I tend to stay away from whenever possible, so to me that falls in the remaining 2%.Building a custom element in JS (including children) and then inserting it into the page will run the callback on a populated component. If you want to add elements afterwards, then you probably need a MutationObserver anyway, because you'll likely want to continue monitoring for changes for the entirety of the components life-cycle, not just for some initial "setup" phase where you continue adding children manually.
It has nothing to do with
innerHTML
It doesn't matter how you add DOM to your existing page,readystatechange
will never fire again after the first pageload.I don't think I understand what requirements you have towards your custom elements then.
I see two settings:
a) A custom element should consider its child elements when it is created and do some stuff with those, but can afterwards become inert because the contents of the element are static at that point.
b) The component should respond to insertions and deletions throughout its entire lifetime and update its internal state accordingly on every change.
In the first case, the site load is the only problematic part, because the element gets inserted into the DOM before its children have been parsed. When creating the element from javascript, the child elements are already there by the time the element is connected, so the
connectedCallback
will work just fine.In the latter case, inserting items initially is really just a not-so-special case of inserting items at any point during the object's lifecycle, so no special code is required here.
The dynamic case can usually be achieved relatively easily with a generic
MutationObserver
that dispatches an event or calls a method on its target.There is no requirement, there is a fact.
This blog is (an attempt) to explain why there is no innerHTML when the Web Component is defined before the DOM is parsed
Almost all Web Components posted on X in the last month, fall into this trap.. and don't work in 100% of cases.
Yes, you can solve part of the issue with a MutationObserver; that is like taking a sledgehammer to drive in a nail.
I think this conversation is going in circles.
To reiterate:
readystatechange
event listener can fix the problem where custom elements appear in the HTML sent from the serverMost of your post makes perfect sense, but towards the end you start to consider a weird case that I don't exactly get where you insert a custom element from client-side code, but you talk about it like DOM parsing is still a factor here, which I don't understand how that would be the case.
Going back to the code in my original comment:
and the example you provided
Maybe you could explain what would have to happen for this to fail? There's no child-elements being added to the component in your example, so I don't see where the problem is supposed to come from.
Yes, that is a a wrong answer in my comment, I re-read the blog post; looks fine to me.
I added a better reply to your
readystatechange
.Full working (and
readystatechange
failing) JSFiddle is: jsfiddle.net/WebComponents/d9sbzcex/Note: As I said in the blog, WebReflection wrote a
parsedCallback
that fires when all child Nodes are available.github.com/WebReflection/html-pars...
It uses the document readyState, MutationObserver and lost more MJ... and is 77 lines of code
The one liner
setTimeout
will get you the next N Child NodesN can be scary for developers who only believe in digital 0 and 1 values, and can't deal with quarks or Schrodingers cat when programming.
readystatechage
will not workHere is a full working JSFiddle: jsfiddle.net/WebComponents/d9sbzcex/
Extract of your solution:
Issues
(minor) You can not call
this.connectedCallback
in the handler, it will have the wrong lexical scope. so a.bind(this)
is requiredBut you can't just run
connectedCallback
again because it would run any other (initialization) code again also So I changed your code as aboveCode now runs fine
Although your workaround runs "late" after the
readystatechange
EventBut now another issue occurs
Because your code relies on an Event that happens only once, that code will never fire again.
So if you move DOM around, or append DOM, that Event will never fire.
Your component never did the "Access" again
That isn't my work-around though. Here's what that should look like:
When the component is loaded before the DOM is fully loaded, it will defer its
connectedCallback
to the nextreadystatechange
event by attaching an event listener and returning early, but when the DOM has already loaded, it will jump over the early return and do its initialisation as usual.This works in all three cases: