loading...
Cover image for uwc part 4: Dynamic element hydration

uwc part 4: Dynamic element hydration

btopro profile image Bryan Ollendyke ・4 min read

GitHub logo elmsln / unbundled-webcomponents

"The magic script" - Unbundled Web components for lazy-hydration routines hitting maximal browsers

How this repo works will be broken out into a few posts on the different pieces. First, let's start with how Dynamic Element Hydration works. Let's define that as follows.

Dynamic Element Hydration - the browser noticing an undefined, web component and then injecting the definition to "hydrate" the tag in the DOM.

Because the "Magic Script" (see last post) has 2 lines to integrate, let's look at what's inside that build.js file that's so "magic"

window.process = {env: {NODE_ENV: "production"}};
var cdn = "./";
var ancient=false;
if (window.__appCDN) {
  cdn = window.__appCDN;
}
window.WCAutoloadRegistryFile = cdn + "wc-registry.json";
try {
  var def = document.getElementsByTagName("script")[0];
  // if a dynamic import fails, we bail over to the compiled version
  new Function("import('');");
  // insert polyfill for web animations
  var ani = document.createElement("script");
  ani.src = cdn + "build/es6/node_modules/web-animations-js/web-animations-next-lite.min.js";
  def.parentNode.insertBefore(ani, def);
  var build = document.createElement("script");
  build.src = cdn + "build/es6/node_modules/@lrnwebcomponents/wc-autoload/wc-autoload.js";
  build.type = "module";
  def.parentNode.insertBefore(build, def);
} catch (err) {
  var legacy = document.createElement("script");
  legacy.src = cdn + "build-legacy.js";
  def.parentNode.insertBefore(legacy, def);
}

Breaking this file down into a few concepts

  • 1st we establish the location of the wc-registry.json file via window.WCAutoloadRegistryFile. For our standard integrations in this article, this points to https://cdn.webcomponents.psu.edu/cdn/wc-registry.json
  • 2nd we attempt establish if we need polyfills using a "hack" new Function("import('');");.
    • This evaluates if a dynamic import is possible. If the platform cannot do a dynamic import, we know that we have a super old, ES5 or partial ES6 browser, something we'll go into detail in another post
  • 3rd we add in an animation polyfill
  • 4th we inject a script type="module" tag that references the wc-autoload.js web component singleton

wc-registry.json

This JSON file contains an object that is tag-name => bare import location of that tag name. This file is generated by our gulp tooling in the repo seen here. Basically its a glob that finds web component definitions via customElements.define and uses this to form tag name => file location.

This means that anything in your package.json file (referenced here is accent-card as an example) will be available in your node_modules and get roped into the unbundled routine in the end.

wc-autoload Singleton

The heavy lifting and logic of the hydration all stems from a singleton element called wc-autoload. The injected script type="module" portion at the end of our steps is below

  var build = document.createElement("script");
  build.src = cdn + "build/es6/node_modules/@lrnwebcomponents/wc-autoload/wc-autoload.js";
  build.type = "module";
  def.parentNode.insertBefore(build, def);

The autoloader will process this json file and create the registry. You can see the code here but it follows the following logical loop:

  • Event listener for the webpage to load which then calls window.WCAutoload.requestAvailability()
  • This self-appends a a single wc-autoloader tag to the dom which is then in control of listening for changes
  • in its connectedCallback it drops in a mutation observer
connectedCallback() {
    // listen for changes and then process any new node that has a tag name
    this._mutationObserver = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
          this.processNewElement(node);
        });
      });
    });
    // support window target
    if (window.WCAutoloadOptions) {
      this.options = window.WCAutoloadOptions;
    }
    setTimeout(() => {
      // support window target
      if (window.WCAutoloadTarget) {
        this.target = window.WCAutoloadTarget;
      } else {
        this.target = document.body;
      }
      // listen on the body and deep children as well
      this._mutationObserver.observe(this.target, this.options);
    }, 0);
  }

Mutation Observer

This mutation observer defaults to monitoring document.body for changes. This is the critical piece that makes this all work (hence, "magic"): When a change is noticed in the body, it will run processNewElement against each addedNode.

Example

  • DOM loads
  • my-app is noticed being added into the dom.
  • wc-registry.json has { "my-app": "@yourorg/my-app/some-location/my-app.js" }
  • The observer notices the match in the registry (using another singleton called dynamic-import-registry and performs import("@yourorg/my-app/some-location/my-app.js"); to dynamically hydrate the element

There's also logic that runs on initial load to ensure that all nodes that are undefined at run time, yet live in the registry, are dynamically hydrated.

wheeeww

Wow, that was a lot. But we're not done with the unbundled build routine. This is how we get a common integration and default to evergreen browsers but some questions remain:

  • How do we handle polyfill'ed targets (ES5 / ES6-ish)?
  • How do we compile to the three targets in an unbundled manner?
  • How does HAX and other applications leverage this approach in a production environment?

Next up, I'll go into how we figure out which version to ship the user on the front end and then we'll dig into how Polymer CLI is used to compile the assets into ES5, ES6, and ES8 versions.

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