Skip to content

Instantly share code, notes, and snippets.

@treshugart
Last active December 20, 2022 20:19
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save treshugart/3b8ee09c16efcbb43453f3f78e59d575 to your computer and use it in GitHub Desktop.
Save treshugart/3b8ee09c16efcbb43453f3f78e59d575 to your computer and use it in GitHub Desktop.
HMR for generic web components

Generic web component HMR

I'm messing around with a generic way to do hot-module-replacement with generic web components whether they be vanilla, or done in a framework like Polymer or Skate.

The idea is that you call hmr(customElementConstructor) in your module files and it will set up the proper hooks.

The const filename should be inserted at build time so that it can remember the original localName of the component for the module.

WTF is "x-layout", "Layout" and "filename"

  • filename represents the filename at build time. This should be replaced with that.
  • x-layout just a dummy filename I was using for testing.
  • Layout a constructor that I was testing.

Why not replace the node?

When you replace the node, you have to reconstruct it as it was before. If the original node was being interacted with, you'd lose UX state, even if it was restored back to its original internal state.

Why breadth-first traversal?

No reason yet. It doesn't appear, at this point, that it would make a difference at all since all replacements need to happen at some point.

// This will be applied at build time.
const filename = 'x-layout';
// Rudimentary, but it works.
function walk(root, call) {
call(root);
if (root.shadowRoot) {
walk(root.shadowRoot, call);
}
Array.from(root.children).forEach(child => {
walk(child, call);
});
}
function hmr(ctor) {
if (!module.hot) {
return;
}
module.hot.accept(function() {
walk(document.body, node => {
if (node.localName === window.__hot[filename]) {
// This ensures any stuff being used here as side-effects when the
// elemnt is defined (it's called and stored at that time) happens.
ctor.observedAttributes;
// We grab the descriptors we care about and apply then to the existing
// node.
const descriptorsS = Object.getOwnPropertyDescriptors(ctor);
const descriptorsI = Object.getOwnPropertyDescriptors(ctor.prototype);
// Static.
for (const name in descriptorsS) {
if (name !== 'length' && name !== 'name' && name !== 'prototype') {
Object.defineProperty(node.constructor, name, descriptorsS[name]);
}
}
// Instance.
for (const name in descriptorsI) {
Object.defineProperty(node, name, descriptorsI[name]);
}
// This ensures the lifecycle can begin as the new element.
if (node.connectedCallback) {
node.connectedCallback();
}
}
});
});
module.hot.dispose(function() {
if (!window.__hot) {
window.__hot = {};
}
if (!window.__hot[filename]) {
window.__hot[filename] = new ctor().localName;
}
});
}
hmr(Layout);
@vegarringdal
Copy link

very nice, thank you @treshugart

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment