uwc part 4: Dynamic element hydration
Tags: haxtheweb, webcomponents, unbundle, webdev
Published: 2020-07-16 01:18:49

{% github elmsln/unbundled-webcomponents no-readme %}

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"

```js
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](https://github.com/elmsln/unbundled-webcomponents/blob/master/gulpfile.cjs#L39-L106). 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](https://github.com/elmsln/unbundled-webcomponents/blob/master/app/package.json#L14) 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
```js
  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](https://github.com/elmsln/lrnwebcomponents/blob/master/elements/wc-autoload/wc-autoload.js#L89) 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
```js
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](https://github.com/elmsln/lrnwebcomponents/blob/master/elements/dynamic-import-registry/dynamic-import-registry.js) 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.