Skip to content

Commit

Permalink
Experimental client+server-renderable directives (repeat & cache) (#50)
Browse files Browse the repository at this point in the history
* Initial client+server-renderable directives (repeat & cache)

* Add guard directive test

* Add support for directives in AttributeParts. Add classMap test.

* Add styleMap tests (& classMap statics test)

* Add `isServerRendering` render option

* Respect `noChange` returned from attribute committer

* Add until directive tests

* Enable noChange tests in AttributeParts

* Re-enable attribute part tests

* Re-enable AttributePart array test now that committer.getValue is used

* Add asyncAppend/Repeat directive tests.
Includes test support for async check and closures in test definition, to allow for stateful arguments to render. Updated until tests to take advantage of this.

* Add ifDefined directive tests.

* Add "manual" test configuration

* Add live directive test.

* Add guard directive test (for AttributePart)

* Add PropertyPart directive tests.

* Add BooleanPart directive support & tests.

* Add EventPart directive tests.

* Add "reflected" PropertyPart tests

* Add support for directives in reflected PropertyParts

* Pass isServerRendering option to BooleanAttributePart

* Thread good error message through from reader

* Combine debug/manual back into one launch config

* Cleanup comments.

* Add SSR subclasses of Parts/Committers.
- Throws on access to DOM
- Sets isServerRendering flag

* Update to use NodePart:getPendingValue

* Name improvement.

* Use resolvePendingDirective().

* Add test for all part types at various depths. Fixes #37

* Add basic LitElement tests (#60)

* Add LitElement tests.
* Adds deep mutation obseving
* Add ability for deep html expectations
* Adds setup() callback that runs before render
* Changes check() callback to run before the html expectation (to allow awaiting a promise before checking)

* Fix example.

* Add issue link
  • Loading branch information
kevinpschaaf authored Jun 23, 2020
1 parent 199cd65 commit a0a120f
Show file tree
Hide file tree
Showing 12 changed files with 2,629 additions and 257 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"type": "node",
"runtimeVersion": "13.12.0",
"request": "launch",
"name": "Test: Integration",
"name": "Test: Integration (debug)",
"skipFiles": [
"<node_internals>/**"
],
Expand Down
30 changes: 0 additions & 30 deletions src/lib/directives/class-map.ts

This file was deleted.

43 changes: 0 additions & 43 deletions src/lib/directives/repeat.ts

This file was deleted.

13 changes: 3 additions & 10 deletions src/lib/import-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,9 @@ const resolveSpecifier = (specifier: string, referrer: string): URL => {
}

if (specifier.startsWith('lit-html')) {
if (specifier.match(/lit-html\/directives\/repeat\.js$/)) {
// Swap directives when requested.
return new URL(`file:${path.resolve('lib/directives/repeat.js')}`);
} else if (specifier.match(/lit-html\/directives\/class-map\.js$/)) {
return new URL(`file:${path.resolve('lib/directives/class-map.js')}`);
} else {
// Override where we resolve lit-html from so that we always resolve to
// a single version of lit-html.
referrer = import.meta.url;
}
// Override where we resolve lit-html from so that we always resolve to
// a single version of lit-html.
referrer = import.meta.url;
}
const referencingModulePath = new URL(referrer).pathname;
const modulePath = resolve.sync(specifier, {
Expand Down
213 changes: 154 additions & 59 deletions src/lib/render-lit-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@
* http://polymer.github.io/PATENTS.txt
*/

import {TemplateResult, nothing, noChange} from 'lit-html';
import {
TemplateResult,
nothing,
noChange,
NodePart,
RenderOptions,
AttributeCommitter,
BooleanAttributePart,
PropertyCommitter,
AttributePart,
PropertyPart,
} from 'lit-html';
import {
marker,
markerRegex,
Expand All @@ -34,14 +45,11 @@ import {

import {CSSResult} from 'lit-element';
import StyleTransformer from '@webcomponents/shadycss/src/style-transformer.js';
import {isRepeatDirective, RepeatPreRenderer} from './directives/repeat.js';
import {
isClassMapDirective,
ClassMapPreRenderer,
} from './directives/class-map.js';
import {isDirective} from 'lit-html/lib/directive.js';
import {isRenderLightDirective} from 'lit-element/lib/render-light.js';
import {LitElementRenderer} from './lit-element-renderer.js';
import {reflectedAttributeName} from './reflected-attributes.js';
import { TemplateFactory } from 'lit-html/lib/template-factory';

declare module 'parse5' {
interface DefaultTreeElement {
Expand All @@ -56,6 +64,87 @@ const templateCache = new Map<
}
>();

const directiveSSRError = (dom: string) =>
`Directives must not access ${dom} during SSR; ` +
`directives must only call setValue() during initial render.`

class SSRNodePart extends NodePart {
constructor(options: RenderOptions) {
super(options);
this.isServerRendering = true;
}
get startNode(): Element {
throw new Error(directiveSSRError('NodePart:startNode'));
}
set startNode(_v) {}
get endNode(): Element {
throw new Error(directiveSSRError('NodePart:endNode'));
}
set endNode(_v) {}
}

class SSRAttributeCommitter extends AttributeCommitter {
constructor(name: string, strings: ReadonlyArray<string>) {
super(undefined as any as Element, name, strings);
this.isServerRendering = true;
}
protected _createPart(): SSRAttributePart {
return new SSRAttributePart(this);
}
get element(): Element {
throw new Error(directiveSSRError('AttributeCommitter:element'));
}
set element(_v) {}
}

class SSRAttributePart extends AttributePart {
constructor(committer: AttributeCommitter) {
super(committer);
this.isServerRendering = true;
}
}

class SSRPropertyCommitter extends PropertyCommitter {
constructor(name: string, strings: ReadonlyArray<string>) {
super(undefined as any as Element, name, strings);
this.isServerRendering = true;
}
protected _createPart(): SSRPropertyPart {
return new SSRPropertyPart(this);
}
get element(): Element {
throw new Error(directiveSSRError('PropertyCommitter:element'));
}
set element(_v) {}
}

class SSRPropertyPart extends PropertyPart {
constructor(committer: PropertyCommitter) {
super(committer);
this.isServerRendering = true;
}
}

class SSRBooleanAttributePart extends BooleanAttributePart {
constructor(name: string, strings: readonly string[]) {
super(undefined as any as Element, name, strings);
this.isServerRendering = true;
}
get element(): Element {
throw new Error(directiveSSRError('BooleanAttributePart:element'));
}
set element(_v) {}
}

const ssrRenderOptions: RenderOptions = {
get templateFactory(): TemplateFactory {
throw new Error(directiveSSRError('RenderOptions:templateFactory'));
},
get eventContext(): EventTarget {
throw new Error(directiveSSRError('RenderOptions:eventContext'));
}
};

/**
* Operation to output static text
*/
Expand Down Expand Up @@ -330,23 +419,26 @@ export function* renderValue(
value: unknown,
renderInfo: RenderInfo
): IterableIterator<string> {
if (isRenderLightDirective(value)) {
// If a value was produced with renderLight(), we want to call and render
// the renderLight() method.
const instance = getLast(renderInfo.customElementInstanceStack);
// TODO, move out of here into something LitElement specific
if (instance !== undefined) {
yield* instance.renderLight(renderInfo);
}
value = null;
} else if (isDirective(value)) {
const part = new SSRNodePart(ssrRenderOptions);
part.setValue(value);
value = part.resolvePendingDirective();
}
if (value instanceof TemplateResult) {
yield `<!--lit-part ${value.digest}-->`;
yield* renderTemplateResult(value, renderInfo);
} else {
yield `<!--lit-part-->`;
if (value === undefined || value === null) {
// do nothing
} else if (isRepeatDirective(value)) {
yield* (value as RepeatPreRenderer)(renderInfo);
} else if (isRenderLightDirective(value)) {
// If a value was produced with renderLight(), we want to call and render
// the renderLight() method.
const instance = getLast(renderInfo.customElementInstanceStack);
if (instance !== undefined) {
yield* instance.renderLight(renderInfo);
}
} else if (value === nothing || value === noChange) {
if (value === undefined || value === null || value === nothing || value === noChange) {
// yield nothing
} else if (Array.isArray(value)) {
for (const item of value) {
Expand Down Expand Up @@ -405,45 +497,66 @@ export function* renderTemplateResult(
: undefined;
if (prefix === '.') {
const propertyName = name.substring(1);
const value = result.values[partIndex];
if (instance !== undefined) {
instance.setProperty(propertyName, value);
}
// Property should be reflected to attribute
const reflectedName = reflectedAttributeName(
op.tagName,
propertyName
);
if (reflectedName !== undefined) {
yield `${reflectedName}="${value}"`;
// Property should be set to custom element instance
const instance = op.useCustomElementInstance
? getLast(renderInfo.customElementInstanceStack)
: undefined;
if (instance || reflectedName !== undefined) {
const committer = new SSRPropertyCommitter(
attributeName,
statics);
committer.parts.forEach((part, i) => {
part.setValue(result.values[partIndex + i]);
part.resolvePendingDirective();
});
if (committer.dirty) {
const value = committer.getValue();
if (value !== noChange) {
if (instance !== undefined) {
instance.setProperty(propertyName, value);
}
if (reflectedName !== undefined) {
// TODO: escape the attribute string
yield `${reflectedName}="${value}"`;
}

}
}
}
} else if (prefix === '@') {
// Event binding, do nothing with values
} else if (prefix === '?') {
// Boolean attribute binding
attributeName = attributeName.substring(1);
if (statics.length !== 2 || statics[0] !== '' || statics[1] !== '') {
throw new Error(
'Boolean attributes can only contain a single expression'
);
const part = new SSRBooleanAttributePart(
attributeName,
statics);
part.setValue(result.values[partIndex]);
const value = part.resolvePendingDirective();
if (value && value !== noChange) {
yield attributeName;
}
const value = result.values[partIndex];
if (value) {
} else {
const committer = new SSRAttributeCommitter(
attributeName,
statics);
committer.parts.forEach((part, i) => {
part.setValue(result.values[partIndex + i]);
part.resolvePendingDirective();
});
// TODO: escape the attribute string
const value = committer.getValue();
if (value !== noChange) {
if (instance !== undefined) {
instance.setAttribute(attributeName, value as string);
}
yield attributeName;
}
} else {
const attributeString = `${attributeName}="${getAttrValue(
statics,
result,
partIndex
)}"`;
if (instance !== undefined) {
instance.setAttribute(attributeName, attributeString as string);
yield `${attributeName}="${value}"`;
}
yield attributeString;
}
partIndex += statics.length - 1;
break;
Expand Down Expand Up @@ -484,22 +597,4 @@ export function* renderTemplateResult(
}
}

const getAttrValue = (
strings: ReadonlyArray<string>,
result: TemplateResult,
startIndex: number
) => {
let s = strings[0];
for (let i = 0; i < strings.length - 1; i++) {
const value = result.values[startIndex + i];
if (isClassMapDirective(value)) {
s += (value as ClassMapPreRenderer)();
} else {
s += String(value);
}
s += strings[i + 1];
}
return s;
};

const getLast = <T>(a: Array<T>) => a[a.length - 1];
5 changes: 4 additions & 1 deletion src/lib/util/iterator-readable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export class IterableReader<T> extends Readable {
const r = this._iterator.next();
this.push(r.done ? null : r.value);
} catch (e) {
this.emit('error', e);
// Because the error may be thrown across realms, it won't pass an
// `e instanceof Error` check in Koa's default error handling; instead
// propagate the error string so we can get some context at least
this.emit('error', e.stack.toString());
}
}
}
Loading

0 comments on commit a0a120f

Please sign in to comment.