DOMContentLoaded never fires inside a module

Viewed 1120

I'm confused regarding events not firing in a particular ES6 module loading scenario. Here is a Codepen project to support my examples: https://codepen.io/fchristant/project/editor/AYQkGJ

Consider this very simple module app.js:

console.log('in module');
document.addEventListener('DOMContentLoaded', (event) => {
  console.log('DCL:2');
});

I'm using dynamic ES6 imports from script tags in the head of the document:

<script>
  (async () => {
    const module = await import("./js/app.js");
    console.log('async done');
   })();
</script>

So, this calling code is a non-module (sync script) dynamically loading a module. Because it is in the head of the document and not async or defer, I expect it to run immediately, and to do so in a blocking way.

That's my expectation, yet not what is happening. In particular, the problem is that the event in the module is never fired. And it looks to be because it's too late to listen to it, the event has already happened. How can this be, given the blocking script in the head of the document? It looks to be non-blocking, yet I don't understand why.

To be clear, the discussion isn't whether this is a good idea or not, I'm above all interested in the conceptual answer of why this doesn't work. Or to rephrase the question: how can a non-module script dynamically load a module in a synchronous way?

2 Answers

Wait for the domcontentloaded event to happen in your index.html file. In the callback of the event import the module that you want to run whenever the event has fired.

// app.js
export function start() {
  console.log('in module');
  console.log('DCL:2');
}
<!-- index.html -->
<script>
  (async () => {
    document.addEventListener('DOMContentLoaded', (event) => {
      const module = await import("./js/app.js");
      module.start();
      console.log('async done');
    });
  })();
</script>

Or if you don't need any modules and you simply want everything to load one after another before the DOM has been rendered, just use the traditional render-blocking <script> tags.

<script src="./js/app.js"></script>
<script>
  console.log('module loaded');
</script>

You could use a helper function to check document.readyState and then either addEventListener or run code immediately.

  • onDCL1() can be used with a callback fn (A.1)
  • onDCL2() is Promise based
    • … traditional .then() chain (B.1)
    • … or await for it (C.1)

We can "fake" and dispatch our own DOMContentLoaded event and verify that else block works (A.2. , B.2. , C.2.)

class App {
  static onDCL1(callback) {
    if (document.readyState !== 'loading')
      callback(`onDCL1 ➜ readyState: '${document.readyState}'`);
    else
      document.addEventListener('DOMContentLoaded',
        e => callback(`onDCL1 ➜ ... waited readyState: '${document.readyState}'`));
  }

  static onDCL2() {
    return new Promise(resolve => {
      if (document.readyState !== 'loading')
        resolve(`onDCL2 ➜ readyState: '${document.readyState}'`);
      else
        document.addEventListener('DOMContentLoaded',
          e => resolve(`onDCL2 ➜ ... waited readyState: '${document.readyState}'`));
    });
  }
}

(async() => {
  console.log(`readyState: '${document.readyState}'`);

  // A.1
  App.onDCL1(payload => console.log(`A.1.| ${payload}`)); // run callback
  // B.1
  App.onDCL2().then(payload => console.log(`B.1.| ${payload}`)); // Promise chain
  // C.1
  let payload = await App.onDCL2(); // await  for Promise. Hint: no 'async' required, since we already return a Promise
  console.log(`C.1.| ${payload}`);


  // have to wait for C.1. (`await`) ....
  // ... comment 2 lines of "C.1" will show 'END' bevor Promise B.1. gets resolved async (~1ms)
  console.log('END 1');

  setTimeout(async() => {
    // dispatch "fake" event in 100ms, (we need to addEventListeners first
    // ... since C.2 has `await` we do this here
    setTimeout(() => {
      window.document.dispatchEvent(new Event("DOMContentLoaded", {
        bubbles: true,
        cancelable: true
      }));
    }, 100);

    // A.2
    App.onDCL1(payload => console.log(`A.2.| ${payload}`)); // run callback
    // B.2
    App.onDCL2().then(payload => console.log(`B.2.| ${payload}`)); // Promise chain
    // C.2
    let payload = await App.onDCL2(); // await  for Promise
    console.log(`C.2.| ${payload}`);

    console.log('END 2');
  }, 300);
})();

Related