Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial hot elements implementation. [Not ready to merge] #802

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/lib/css-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ export class CSSResult {
toString(): string {
return this.cssText;
}

/**
* Used when generating files at build time that transform .css files
* into something like CSS Modules that export CSSResults.
*
* Does nothing in production, and doesn't replace existing references
* unless the current browser supports adopting stylesheets (at time of
* writing that Chrome only).
*
* We could support replacing styles cross-browser,
* but it would be very tricky to do so without leaking memory, becasue we'd
* need to keep track of every style element that `this` is written into.
* This actually seems like a legit use case for WeakRefs.
*/
notifyOnHotModuleReload(newVal: CSSResult) {
if (goog.DEBUG) {
const sheet = this.styleSheet;
// Only works with constructable stylesheets
if (sheet === null) {
return;
}
// tslint:disable-next-line:no-any hot module reload writes readonly prop.
(this as any).cssText = newVal.cssText;
sheet.replaceSync(newVal.cssText);
}
}
}

/**
Expand Down
66 changes: 66 additions & 0 deletions src/lib/hot-elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* @fileoverview Allow custom element classes to do hot module reloading.
*
* IMPORTANT:
* Hot module reloading is hot in the sense of fast, but also hot as
* in CAUTION WILL VAPORIZE FINGERS. When in doubt, reload the page, and never
* ever use when connected to a production store of data. It *will* violate the
* assumptions of otherwise-correct code, causing it to execute undefined
* behavior.
*
* An element class that defines the method notifyOnHotModuleReload can be
* defined on the custom elements registry multiple times. The first time,
* the element is registered normally. Each subsequent time, the original will
* receive a call to notifyOnHotModuleReload with the new class object, where
* it can patch prototypes, re-render existing instances, etc.
*
* See the implementation on LitElement for one way of hooking this up. It works
* well enough for elements that are pure functions of their state.
*
* All of the usual concerns about hot module reloading definitely apply.
* Concerns like that it's a "mad dream" and is "poorly conceived", or that it
* "violates most every reasonable assumption one might have about a body of
* code". All still true, still valid. But it can't be ignored that it also can
* make for a *much* nicer development experience for a lot of frontend dev.
*/

interface Constructor<T> {
new(): T;
}
interface HotReloadableElementClass extends Constructor<HTMLElement> {
notifyOnHotModuleReload(
tagname: string, updatedClass: HotReloadableElementClass): void;
}

function patchCustomElementsDefine() {
function isHotReloadableElementClass(maybe: Constructor<HTMLElement>):
maybe is HotReloadableElementClass {
// This isn't rename safe, but this is definitely debug code, it's
// not even compatible with bundling, let alone renaming, so that's fine.
return 'notifyOnHotModuleReload' in maybe;
}

const originalDefine = customElements.define;

const implMap = new Map<string, HotReloadableElementClass>();
function hotDefine(tagname: string, classObj: Constructor<HTMLElement>) {
if (!isHotReloadableElementClass(classObj)) {
return originalDefine.call(customElements, tagname, classObj);
}
const impl = implMap.get(tagname);
if (!impl) {
implMap.set(tagname, classObj);
originalDefine.call(customElements, tagname, classObj);
} else {
impl.notifyOnHotModuleReload(tagname, classObj);
}
}

customElements.define = hotDefine;
}
// Just to be extra sure this never ends up in production.
if (goog.DEBUG) {
patchCustomElementsDefine();
}

export {};
56 changes: 56 additions & 0 deletions src/lit-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,60 @@ export class LitElement extends UpdatingElement {
*/
protected render(): TemplateResult|void {
}

/** @nocollapse */
static notifyOnHotModuleReload(tagname: string, classObj: typeof LitElement) {
if (goog.DEBUG) {
// There's lots of things that this doesn't handle, but probably the
// biggest is updates to the constructor. That means that changes to event
// handlers won't go through when they're defined as arrow function
// property initializers. We could potentially hack that together, via
// some really wild tricks, but I'm inclined not to. Periodically
// reloading the page while developing with HMR is a good habit for people
// to get into.
//
// One thing I'd like to support is live updating of CSS defined in a
// css file with lit_css_module.
const existingProps = new Set(Object.getOwnPropertyNames(this.prototype));
const newProps = new Set(Object.getOwnPropertyNames(classObj.prototype));
for (const prop of Object.getOwnPropertyNames(classObj.prototype)) {
Object.defineProperty(
this.prototype, prop,
Object.getOwnPropertyDescriptor(classObj.prototype, prop)!);
}
for (const existingProp of existingProps) {
if (!newProps.has(existingProp)) {
// tslint:disable-next-line:no-any Also hacky
delete (this.prototype as any)[existingProp];
}
}

// This new class object has never been finalized before. Need to finalize
// so that instances can get any updated styles.
classObj.finalize();

for (const node of shadowPiercingWalk(document)) {
if (node instanceof HTMLElement &&
node.tagName.toLowerCase() === tagname.toLowerCase()) {
const myNode = node as LitElement;
// Get updated styling. Need to test that this works in all the
// different browser configs, only tested on recent Chrome so far,
// where overriding adopted stylesheets seems to just work.
myNode.adoptStyles();
if (!supportsAdoptingStyleSheets) {
const nodes = Array.from(myNode.renderRoot.children);
for (const node of nodes) {
// TODO(rictic): this is super hacky and doesn't always work,
// even for inline styles.
if ((node as HTMLElement).tagName.toLowerCase() === 'style') {
myNode.renderRoot.removeChild(node);
}
}
}
// Ask for a re-render.
myNode.requestUpdate();
}
}
}
}
}