loading...
Cover image for Bringing old JavaScript to the type="module" world

Bringing old JavaScript to the type="module" world

btopro profile image Bryan Ollendyke ・4 min read

ES Modules is a new form of JavaScript that is processed after all assets are discovered in an execution chain. Normal <script> tags are processed as follows:

<script> /* part 1 starts */ </script>
<script> /* part 2 after 1 is parsed */ </script>
<script src="3.js">/* file handled after part 2 */ </script>

script type="module" allow us to import assets as individual parts, while the browser won't start processing them until ALL have been discovered and loaded.

example

<script type="module">
  import "assets/some-asset.js";
  import { aClassOfSomeKind } from "assets/something-exporting-a-class.js";
  import "asset/and-another.js";
</script>

dynamic import() caveat

There's another type of import you can use now called a dynamic import which looks like import('something.js');. Importing assets in this manner requires that it be art of a type="module" execution chain however, it will not require that asset be loaded in order to begin processing execution of the chain of JS files being imported.

So what's the problem?

The world isn't entirely ready for ES modules everywhere. We have wide browser support but we've had people making awesome libraries for a decade which most likely were not in ESM. How do I know that something isn't in ESM format? Here's some common examples:

  • jQuery references hanging around
  • uses or assumes that window.anything is available
  • has assumptions of require() being utilized.

Because JS modules require all files to be there to start executing, we can't assume that all window.anything is available until after the page is all setup. But we have a lot of great resources that are too complicated to port outright. So what do we do?

There's a JS Module for that!

Our team made a bridge project just for this purpose. Let's look at it in the context of a EXIF code library we use; a feature that we want to add to images in HAX but not enough of a critical feature that we should be spending time completely rewriting (and maintaining) existing EXIF solutions to work with the way we're deploying JS code.

yarn add @lrnwebcomponents/es-global-bridge

The ES global bridge project code is as follows:

/**
 * Object to help load things in globally scoped and fire events when ready
 */
export class ESGlobalBridge {
  constructor() {
    /**
     * Load location and register it by name
     */
    this.load = (name, location, webpack = false) => {
      //don't try to load file if a story.js is already working on importing the packed version of the file
      let imported =
          window.ESGlobalBridge.imports && window.ESGlobalBridge.imports[name],
        importing =
          !webpack &&
          window.ESGlobalBridge.webpack &&
          window.ESGlobalBridge.webpack[name];
      if (!importing && !imported) {
        return new Promise((resolve, reject) => {
          const script = document.createElement("script");
          script.src = location;
          script.setAttribute("data-name", name);
          window.ESGlobalBridge.imports[name] = location;
          script.onload = () => {
            resolve(window.ESGlobalBridge.imports[name]);
            window.ESGlobalBridge.imports[name] = true;
            // delay firing the event just to be safe
            setTimeout(() => {
              const evt = new CustomEvent(`es-bridge-${name}-loaded`, {
                bubbles: true,
                cancelable: true,
                detail: {
                  name: name,
                  location: location
                }
              });
              document.dispatchEvent(evt);
            }, 100);
          };
          script.onerror = () => {
            reject(
              new Error(
                `Failed to load ${name} script with location ${location}.`
              )
            );
            delete window.ESGlobalBridge.imports[name];
            window.ESGlobalBridge.imports[name] = false;
          };
          document.documentElement.appendChild(script);
        });
      }
    };
  }
}
// register global bridge on window if needed
window.ESGlobalBridge = window.ESGlobalBridge || {};
window.ESGlobalBridge.imports = window.ESGlobalBridge.imports || {};

window.ESGlobalBridge.requestAvailability = () => {
  if (!window.ESGlobalBridge.instance) {
    window.ESGlobalBridge.instance = new ESGlobalBridge();
  }
};

ESGlobalBridge uses a Promise which creates a fake script tag, points the source to what you need to import, and then returns an event with a name you can listen for.

yarn add @lrnwebcomponents/exif-data

Let's take a look at the relevent portions of the exif-data element. (full source)


import "@lrnwebcomponents/es-global-bridge/es-global-bridge.js";

class ExifData extends HTMLElement {
  /
  /**
   * life cycle
   */
  constructor() {
    super();
    const basePath = this.pathFromUrl(decodeURIComponent(import.meta.url));
    window.ESGlobalBridge.requestAvailability();
    window.ESGlobalBridge.instance.load("exif-js", `${basePath}lib/exif-js.js`);
    window.addEventListener(
      "es-bridge-exif-js-loaded",
      this._onExifJsLoaded.bind(this)
    );
    this.template = document.createElement("template");
    this.attachShadow({ mode: "open" });
    this.render();
  }
// simple path from a url modifier
  pathFromUrl(url) {
    return url.substring(0, url.lastIndexOf("/") + 1);
  }
  /**
   * Library loaded
   */
  _onExifJsLoaded() {
    window.removeEventListener(
      "es-bridge-exif-js-loaded",
      this._onExifJsLoaded.bind(this)
    );
    this.__ready = true;
    this.updateExif();
  }
  /**
   * Load exifData
   */
  getExifData(node) {
    window.EXIF.getData(node, () => {
      let data = window.EXIF.getAllTags(node);
      // REALLY verbose field
      delete data.MakerNote;
      delete data.thumbnail;
      this.nodeData.push({
        node: node,
        data: data
      });
    });
  }
  updateExif(show = false) {
    this.nodeData = [];
    this.dataElement.innerHTML = "";
    this.childNodes.forEach(node => {
      if (this.__ready && node.tagName && node.tagName === "IMG") {
        this.getExifData(node);
      }
    });
    if (show && this.children.length === 1) {
      setTimeout(() => {
        this.showDetails(this.nodeData[0]);
      }, 250);
    }
  }
}
window.customElements.define(ExifData.tag, ExifData);
export { ExifData };

What we see here is that when this web component is defined (constructor) we call the global bridge function :

 window.ESGlobalBridge.requestAvailability();
    window.ESGlobalBridge.instance.load("exif-js", `${basePath}lib/exif-js.js`);
    window.addEventListener(
      "es-bridge-exif-js-loaded",
      this._onExifJsLoaded.bind(this)
    );

The requestAvailability() callback ensures there's a "Singleton" or one element no matter how many requests we have for it so that we're managing all import data in one place. Then we listen for a consistent function based on the name of our library and call _onExifJsLoaded local to our code once the import has been successful.

If you've never seen it, the .bind(this) ensures that even though we listen globally for this event, the context of this is binded to the current element.

This function looks at the children of the currently element and then runs getExifData for each of them which calls for the window.EXIF successfully. While requiring some setup, it is now possible to support globally scoped code without undefined global errors in ES module imported JS.

Without our bridge, you wouldn't be able to KNOW that the global scoped code was available when the exif-data element was wanting it.

Looking for other uses?

Our LRNWebComponents mono-repo has many of them for calenders, pdf tools, QR codes, image pan-zoom viewers and more. You can see the qr-code element (which uses es-global-bridge) live on HAXTheWeb.

Posted on by:

btopro profile

Bryan Ollendyke

@btopro

@elmsln @haxcamp @btopro #HAXTheWeb #drupal #webcomponents #edtech ✻ Full stack unicorn Adjunct professor teaching about webdev, ethics, and everything in between

Discussion

markdown guide