How does DOMContentLoaded relate to Web Components?
Asked Answered
V

3

11

It's age-old common sense to start manipulating the DOM only once it's ready and we can be certain all the elements are available, and in the post-jQuery days we're all using the DOMContentLoaded event for this.

Now web components (especially in the form of autonomous custom elements) tend to create their own HTML, usually in the connectedCallback() lifecycle method.

1st question:

How does DOMContentLoaded relate to (autonomous) custom elements? Will the event occur only after all component connectedCallbacks have finished? If not, how can I make sure certain code only executes after the web components are done initializing?

2nd question, totally related:

How do web components relate to the defer attribute of the script element?

Vesica answered 16/7, 2018 at 12:47 Comment(0)
U
3

I'm not into web-components, but I would say... not at all.

Your component is being defined by your script, but before it is, the browser will still parse the markup and execute all the synchronous scripts as usual, and fire the DOMContentLoaded when it's done.

So if you do define your CustomElements synchronously before the DOMContentLoaded event fired, then your elements connectedCallback will have fired (because it's not an Event, it's a callback, and is called synchronously).

if (window.customElements) {

  addEventListener('DOMContentLoaded', e => console.log('DOM loaded'));

  class MyCustom extends HTMLElement {
    connectedCallback() {
      console.log('Custom element added to page.');
    }
  }

  customElements.define('my-custom', MyCustom);
  console.log('Just defined my custom element')

} else {
  console.log("your browser doesn't have native support");
}
<my-custom></my-custom>

But if you do wait for the DOMContentLoaded event, then... the callbacks will fire after.

if (window.customElements) {

  addEventListener('DOMContentLoaded', e => console.log('DOM loaded'));

  class MyCustom extends HTMLElement {
    connectedCallback() {
      console.log('Custom element added to page.');
    }
  }

  setTimeout(()=> customElements.define('my-custom', MyCustom), 2000);

} else {
  console.log("your browser doesn't have native support");
}
<my-custom></my-custom>

But in no way is DOMContentLoaded waiting for anything else than the end of the synchronous execution of all the scripts, just like if you didn't had any CustomElement in there.


As to your last question, as said in the docs about the defer attribute, the scripts with such an attribute will get parsed before the DOMContentLoaded fires.

Unman answered 16/7, 2018 at 13:24 Comment(7)
Do note, the defer attribute, when set, is known to not work flawless cross browsers.Crescent
@LGSon web-components either ;-)Unman
LOL ... touche :)Crescent
So loading the library of web components with <script defer src="webcomponent-lib.js"></script>, would it make sure that if I include another <script src="wantsToWorkWithTheFullyRenderedWebComponents.js"></script> later in the page that I can in that file's code listen to the DOMContentLoaded event and reliably access the components' innerHTML (assuming no web component asynchronously changes its innerHMTL)?Vesica
@Vesica you would have to insure that your second script is executed after the one that defines the CE ran, just like... any other js global actually. So if the first one is deferred, you'll at least want to defer the second one too. Now how reliable this is... as said in the first comment, unfortunately not yet that much, even though per specs it should be reliable.Unman
The defer attribute tells the browser to only execute the script file once the HTML document has been fully parsed. bitsofco.de/async-vs-defer But how can it be fully parsed before knowing what to do with the custom elements (which the browser cannot know before having parsed webcomponents-lib.js)?Vesica
@Vesica isn't my second snippet exactly demonstrating this? The custom element gets define only 2 seconds after script execution. During this time, your markup has been parsed as a HTMLUnknownElement. So it doesn't have to do anything more with it, until you tell it it actually has.Unman
P
2

They are on different axes. DOMContentLoaded is about parsing the initial HTML document, so the raw "file" which is downloaded. Components are ready when they are defined.
I am not familiar with the topic either, so just modified the MDN example to completely decouple the two happenings via a button press. I assumed definition of new components can happen any time, and indeed that is the case.
MDN example is on GitHub, linked from CustomElementRegistry.define (and also from various other pages). They have a live variant too on GitHub IO, but of course that is just the original example.
Putting the complete example here feels hopeless, so this is just the modified HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Pop-up info box — web components</title>
  </head>
  <body id="body">
    <h1>Pop-up info widget - web components</h1>

    <form>
      <div>
        <label for="cvc">Enter your CVC <popup-info img="img/alt.png" text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers on the back of your card."></label>
        <input type="text" id="cvc">
      </div>
    </form>

    <!--<script src="main.js"></script>-->
    <script>
      function test(){
        var x=document.createElement("script");
        x.src="main.js";
        document.getElementById("body").appendChild(x);
      }
      customElements.whenDefined("popup-info").then(function(){alert("popup-info");});
      document.addEventListener("DOMContentLoaded",function(){alert("DOMContentLoaded")});
    </script>
    <button onclick="test()">Test</button>
  </body>
</html>

So main.js is not loaded automatically, only after pressing the button, and it still works. "DOMContentLoaded" popup has happened by a long time then. However, there is a CustomElementRegistry.whenDefined() which is patient, and fires only after the custom element gets defined. I think this is what you could use, perhaps subscribing to it from DOMContentLoaded, so your final event would be guaranteed to happen when both the DOM and the custom element is ready. Downside is that you have to know the name of the custom element(s) you are waiting for. (Untested assumption, but based on the scheduling diagrams on https://html.spec.whatwg.org/multipage/scripting.html, one could probably make whenDefined() to happen before DOMContentLoaded. By the way: the diagrams also show what defer does, so if you put whenDefined() into a deferred script, the callback will happen after DOMContentLoaded, or at least that is what I believe)

Pelvis answered 16/7, 2018 at 14:48 Comment(0)
H
1

For your first question: Don't worry about the outer page--just the components themselves. However, since there is no method available for when the nested content and shadowDOM of components are ready (see WICG Web Components Issue #809 and MDN connectedCallback), inside the connectedCallback you can put your code inside a "DOMContentLoaded" event listener like this

connectedCallback() {
  addEventListener('DOMContentLoaded', () => {
    // Your code, such as both `this.children` and `this.shadowRoot.querySelector()`
  });
}

or, you can use the very verbose MutationObserver with a debounce function; but the latter is excessive.

For your second question: Though differing still works, I would avoid it so that the browser can handle the component DOMs as soon as possible instead of waiting until the whole page document is ready.

Horan answered 18/5, 2023 at 0:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.