2

I have built a small library of several HTML web components for internal company use. Some components are mutually dependent on each other, so I also import them mutually. Until recently, I had no serious issues with this approach, but I am now encountering an error message when loading a HTML page that uses such mutually dependent components.

I have isolated the issue in a small example. Please review the following three files.

test-container.js

import { TestItem } from "./test-item";

export class TestContainer extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: "open" }).innerHTML = `
      <style>
        * {
          position: relative;
          box-sizing: border-box;
        }
        :host {
          contain: content;
          display: block;
        }
      </style>
      <div>
        <slot></slot>
      </div>
    `;
  }

  connectedCallback() {
    if (!this.isConnected) {
      return;
    }

    for (const node of this.childNodes) {
      if (node instanceof TestItem) {
        //...
      }
    }
  }
}

customElements.define("test-container", TestContainer);

test-item.js

import { TestContainer } from "./test-container";

export class TestItem extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: "open" }).innerHTML = `
      <style>
        * {
          position: relative;
          box-sizing: border-box;
        }
        :host {
          contain: content;
          display: block;
        }
      </style>
      <div>
        <slot></slot>
      </div>
    `;
  }
}

customElements.define("test-item", TestItem);

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Test</title>
  <script type="module" src="/test-container"></script>
  <script type="module" src="/test-item"></script>
  <style>
    test-container {
      width: 600px;
      height: 400px;
      background: lightblue;
      border: 1px solid;
    }
    test-item {
      width: 200px;
      height: 200px;
      background: lightgreen;
      border: 1px solid;
    }
  </style>
</head>
<body>
  <test-container>
    <test-item></test-item>
  </test-container>
</body>
</html>

This code seems to work fine.

However, if I switch the two <script> tags in the index.html file, the developer tools console shows the following error:

Uncaught ReferenceError: Cannot access 'TestItem' before initialization
    at HTMLElement.connectedCallback (test-container:30)
    at test-container:37

Since I import several modules in many of my components, I want to sort them alphabetically (for clarity). In my test example it's fine, but in my actual code it isn't...

So basically I want my modules to be completely independent of the order in which they will be imported by other modules. Is there any way to achieve that?

All suggestions are very welcome. However, I am not allowed to install and use any external/3rd party packages. Even the use of jQuery is not allowed. So a solution should consist of only plain vanilla JS, plain CSS, and plain HTML5, and it should at least work correctly in the latest Google Chrome and Mozilla Firefox web browsers.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Bart Hofland
  • 3,700
  • 1
  • 13
  • 22
  • 3
    You can use the [whenDefined (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined) Promise in an element that has a dependency – Danny '365CSI' Engelman Jun 21 '19 at 14:39
  • @Danny'365CSI'Engelman: That was the golden tip for me! It works perfectly now. :) If you provide your solution as an answer, I will be very happy to mark it as the accepted answer. Thank you very much. – Bart Hofland Jun 22 '19 at 12:41
  • Why does `test-item.js` import the `TestContainer` when it doesn't use it anywhere? Remove that dependency and you will break the circle. – Bergi Jun 23 '19 at 12:45
  • @Bergi: It's just an example to reproduce the issue. In reality, the component corresponding to my TestItem component has a reference to the container as well; the container provides some management functionality for its items, and if the items need to use that functionality, they should be aware of the container and its capabilities. In reality, the components are also written in plain TypeScript. I just wanted to eliminate all those unnecessary details. (I could update the example in my question, but the code would become too complex IMHO.) – Bart Hofland Jun 23 '19 at 15:41

2 Answers2

3

When you can't control the order in which Elements are loaded,
you have to handle the dependency in your Element

Use: https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined

whenDefined returns a Promise!

So your <test-container> code needs something like:

  customElements.whenDefined('test-item')
   .then( () => {
       //execute when already exist or became available
   });

https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined

has a more detailed example waiting for all undefined elements in a page


Dependencies

An Event driven approach might be better to get rid of dependencies.

Make <test-item> dispatch Event X in the connectedCallback

<test-container> listens for Event X and does something with the item

You can then add <another-item> to the mix without having to change <test-container>

Maybe the default slotchange Event can be of help:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/slotchange_event

.

Success met welke aanpak je ook kiest

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • Thanks for the tip about the `slotchange` event. I already use a `MutationObserver` for detecting child node changes, but using the `slotchange` event might be more elegant (and perhaps also more performant; I will analyze it), since I indeed only want to detect changes to the direct children of the container. – Bart Hofland Jun 24 '19 at 08:03
  • I am also thinking about a more general dependency resolver mechanism, because I prefer not to keep track of all dependencies explicitly. Your suggestion for using an event driven approach looks interesting, so I am investigating that further now. So thanks for that tip too. :) – Bart Hofland Jun 24 '19 at 08:05
  • At least I will go for your suggestion to use event handling between the container and its items to minimize direct dependencies from the items to the container. Since my items should be usable stand-alone as well, this seems like a more robust approach indeed. – Bart Hofland Jun 24 '19 at 08:44
  • users Supersharp and Intervalia are the brightest minds on SO: [search their recent Event answers](https://stackoverflow.com/search?tab=newest&q=%5bcustom-element%5d%20event%20user%3aIntervalia%20or%20%5bcustom-element%5d%20event%20supersharp) – Danny '365CSI' Engelman Jun 24 '19 at 10:16
  • After tweaking your [search link](https://stackoverflow.com/search?q=%5Bcustom-element%5D+event+user%3AIntervalia+or+%5Bcustom-element%5D+event+user%3Asupersharp) a little, I got about 57 results. Some questions/answers are rather old and target the old web component implementation, but I will work it out. I will go and study them. Thanks again! Your help is quite valuable! :) – Bart Hofland Jun 24 '19 at 11:22
0

it may help

<!-- This script will execute after… -->
<script type="module" src="1.mjs"></script>

<!-- …this script… -->
<script src="2.js"></script>

<!-- …but before this script. -->
<script defer src="3.js"></script>

The order should be 2.js, 1.mjs, 3.js.

The way scripts block the HTML parser during fetching is baaaad. With regular scripts you can use defer to prevent blocking, which also delays script execution until the document has finished parsing, and maintains execution order with other deferred scripts. Module scripts behave like defer by default – there's no way to make a module script block the HTML parser while it fetches.

Module scripts use the same execution queue as regular scripts using defer.

Source

Hadock
  • 796
  • 1
  • 12
  • 28
  • Yes, I know. I have already tried playing with the `defer` attribute of the `script` tag. But with no success. Applying the `type="module"` attribute on the `script` tag is required for using the ES6 import/export mechanism, so I cannot omit it. – Bart Hofland Jun 21 '19 at 13:28
  • And thanks for silently drawing attention to the new `.mjs` extension. I was not aware of it yet. :) – Bart Hofland Jun 21 '19 at 13:55
  • I found [this](https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload) – Hadock Jun 21 '19 at 14:08
  • 1
    Ah. Module preloading. Yes, that's interesting too. I will study it. I googled it and found this article about it: https://developers.google.com/web/updates/2017/12/modulepreload. Thanks for the tip. – Bart Hofland Jun 22 '19 at 12:47