Search code examples
javascriptecmascript-6web-componentcircular-dependency

Error when importing web components in the "wrong" order


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.


Solution

  • 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