Skip to content

Commit

Permalink
refactor: enable pluggable template compiler for SSR scenarios (#5722)
Browse files Browse the repository at this point in the history
* refactor: enable pluggable template compiler for SSR scenarios

* Change files

Co-authored-by: EisenbergEffect <[email protected]>
  • Loading branch information
2 people authored and nicholasrice committed May 3, 2022
1 parent fc8da6f commit 58b4c9d
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "refactor: enable pluggable template compiler for SSR scenarios",
"packageName": "@microsoft/fast-element",
"email": "[email protected]",
"dependentChangeType": "patch"
}
14 changes: 9 additions & 5 deletions packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export class ChildrenDirective extends NodeObservationDirective<ChildrenDirectiv
export type ChildrenDirectiveOptions<T = any> = ChildListDirectiveOptions<T> | SubtreeDirectiveOptions<T>;

// @public
export function compileTemplate(template: HTMLTemplateElement, directives: ReadonlyArray<HTMLDirective>): HTMLTemplateCompilationResult;
export function compileTemplate(html: string | HTMLTemplateElement, directives: ReadonlyArray<HTMLDirective>): HTMLTemplateCompilationResult;

// @public
export type ComposableStyles = string | ElementStyles | CSSStyleSheet;
Expand Down Expand Up @@ -347,11 +347,14 @@ export abstract class HTMLDirective implements ViewBehaviorFactory {

// @public
export interface HTMLTemplateCompilationResult {
createTargets(root: Node, host?: Node): ViewBehaviorTargets;
readonly factories: ReadonlyArray<ViewBehaviorFactory>;
readonly fragment: DocumentFragment;
createView(hostBindingTarget?: Element): HTMLView;
}

// @public
export type HTMLTemplateCompiler = (
html: string | HTMLTemplateElement,
directives: readonly HTMLDirective[]) => HTMLTemplateCompilationResult;

// @public
export class HTMLView<TSource = any, TParent = any, TGrandparent = any> implements ElementView<TSource, TParent, TGrandparent>, SyntheticView<TSource, TParent, TGrandparent> {
constructor(fragment: DocumentFragment, factories: ReadonlyArray<ViewBehaviorFactory>, targets: ViewBehaviorTargets);
Expand Down Expand Up @@ -644,7 +647,8 @@ export class ViewTemplate<TSource = any, TParent = any, TGrandparent = any> impl
readonly directives: ReadonlyArray<HTMLDirective>;
readonly html: string | HTMLTemplateElement;
render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView<TSource, TParent, TGrandparent>;
}
static setDefaultCompiler(compiler: HTMLTemplateCompiler): void;
}

// @public
export function volatile(target: {}, name: string | Accessor, descriptor: PropertyDescriptor): PropertyDescriptor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export class ElementStyles {
* Sets the default strategy type to use when creating style strategies.
* @param Strategy - The strategy type to construct.
*/
public static setDefaultStrategy(Strategy: ConstructibleStyleStrategy) {
public static setDefaultStrategy(Strategy: ConstructibleStyleStrategy): void {
DefaultStyleStrategy = Strategy;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ import type { StyleTarget } from "../styles/element-styles";
import { toHTML, uniqueElementName } from "../__test__/helpers";
import { bind, HTMLBindingDirective } from "./binding";
import { compileTemplate } from "./compiler";
import type { HTMLDirective } from "./html-directive";
import type { HTMLDirective, ViewBehaviorFactory } from "./html-directive";
import { html } from "./template";

/**
* Used to satisfy TS by exposing some internal properties of the
* compilation result that we want to make assertions against.
*/
interface CompilationResultInternals {
readonly fragment: DocumentFragment;
readonly factories: ViewBehaviorFactory[];
}

describe("The template compiler", () => {
function compile(html: string, directives: HTMLDirective[]) {
const template = document.createElement("template");
template.innerHTML = html;
return compileTemplate(template, directives);
return compileTemplate(html, directives) as any as CompilationResultInternals;
}

function inline(index: number) {
Expand Down
84 changes: 39 additions & 45 deletions packages/web-components/fast-element/src/templating/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Markup, Parser } from "./markup.js";
import { isString } from "../interfaces.js";
import { DOM } from "../dom.js";
import { Markup, Parser } from "./markup.js";
import { bind, oneTime } from "./binding.js";
import type {
AspectedHTMLDirective,
HTMLDirective,
ViewBehaviorFactory,
ViewBehaviorTargets,
} from "./html-directive.js";
import type { HTMLTemplateCompilationResult } from "./template.js";
import { HTMLView } from "./view.js";

const targetIdFrom = (parentId: string, nodeIndex: number): string =>
`${parentId}.${nodeIndex}`;
Expand All @@ -23,30 +25,6 @@ const next: NextNode = {
node: null as ChildNode | null,
};

/**
* The result of compiling a template and its directives.
* @public
*/
export interface HTMLTemplateCompilationResult {
/**
* A cloneable DocumentFragment representing the compiled HTML.
*/
readonly fragment: DocumentFragment;

/**
* The behaviors that should be applied to the template's HTML.
*/
readonly factories: ReadonlyArray<ViewBehaviorFactory>;

/**
* Creates a behavior target lookup object.
* @param host - The host element.
* @param root - The root element.
* @returns A lookup object for behavior targets.
*/
createTargets(root: Node, host?: Node): ViewBehaviorTargets;
}

class CompilationContext implements HTMLTemplateCompilationResult {
private proto: any = null;
private targetIds = new Set<string>();
Expand Down Expand Up @@ -78,18 +56,6 @@ class CompilationContext implements HTMLTemplateCompilationResult {
return this;
}

public createTargets(root: Node, host?: Node): ViewBehaviorTargets {
const targets = Object.create(this.proto);
targets.r = root;
targets.h = host ?? root;

for (const id of this.targetIds) {
targets[id]; // trigger locator
}

return targets;
}

private addTargetDescriptor(
parentId: string,
targetId: string,
Expand All @@ -107,8 +73,8 @@ class CompilationContext implements HTMLTemplateCompilationResult {

if (!descriptors[parentId]) {
const index = parentId.lastIndexOf(".");
const grandparentId = parentId.substr(0, index);
const childIndex = parseInt(parentId.substr(index + 1));
const grandparentId = parentId.substring(0, index);
const childIndex = parseInt(parentId.substring(index + 1));
this.addTargetDescriptor(grandparentId, parentId, childIndex);
}

Expand All @@ -129,6 +95,20 @@ class CompilationContext implements HTMLTemplateCompilationResult {

descriptors[targetId] = descriptor;
}

public createView(hostBindingTarget?: Element): HTMLView {
const fragment = this.fragment.cloneNode(true) as DocumentFragment;
const targets = Object.create(this.proto);

targets.r = fragment;
targets.h = hostBindingTarget ?? fragment;

for (const id of this.targetIds) {
targets[id]; // trigger locator
}

return new HTMLView(fragment, this.factories, targets);
}
}

const marker = Markup.marker;
Expand Down Expand Up @@ -265,10 +245,9 @@ function compileNode(
}

/**
* Compiles a template and associated directives into a raw compilation
* result which include a cloneable DocumentFragment and factories capable
* of attaching runtime behavior to nodes within the fragment.
* @param template - The template to compile.
* Compiles a template and associated directives into a compilation
* result which can be used to create views.
* @param html - The html string or template element to compile.
* @param directives - The directives referenced by the template.
* @remarks
* The template that is provided for compilation is altered in-place
Expand All @@ -277,9 +256,24 @@ function compileNode(
* @public
*/
export function compileTemplate(
template: HTMLTemplateElement,
html: string | HTMLTemplateElement,
directives: ReadonlyArray<HTMLDirective>
): HTMLTemplateCompilationResult {
let template: HTMLTemplateElement;

if (isString(html)) {
template = document.createElement("template");
template.innerHTML = DOM.createHTML(html);

const fec = template.content.firstElementChild;

if (fec !== null && fec.tagName === "TEMPLATE") {
template = fec as HTMLTemplateElement;
}
} else {
template = html;
}

// https://bugs.chromium.org/p/chromium/issues/detail?id=1111864
const fragment = document.adoptNode(template.content);
const context = new CompilationContext(fragment, directives);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Markup } from "./markup.js";
import { isFunction } from "../interfaces.js";
import type { Splice } from "../observation/array-change-records.js";
import { enableArrayObservation } from "../observation/array-observer.js";
Expand All @@ -11,6 +10,7 @@ import {
Observable,
} from "../observation/observable.js";
import { emptyArray } from "../platform.js";
import { Markup } from "./markup.js";
import { HTMLDirective, ViewBehaviorTargets } from "./html-directive.js";
import type { CaptureType, SyntheticViewTemplate } from "./template.js";
import { HTMLView, SyntheticView } from "./view.js";
Expand Down
76 changes: 46 additions & 30 deletions packages/web-components/fast-element/src/templating/template.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { DOM } from "../dom.js";
import { isFunction, isString } from "../interfaces.js";
import { Binding, defaultExecutionContext } from "../observation/observable.js";
import { bind, oneTime } from "./binding.js";
import { compileTemplate } from "./compiler.js";
import type { HTMLTemplateCompilationResult } from "./compiler.js";
import { compileTemplate as compileFASTTemplate } from "./compiler.js";
import { AspectedHTMLDirective, HTMLDirective } from "./html-directive.js";
import { ElementView, HTMLView, SyntheticView } from "./view.js";
import type { ElementView, HTMLView, SyntheticView } from "./view.js";

/**
* A template capable of creating views specifically for rendering custom elements.
Expand Down Expand Up @@ -43,6 +41,36 @@ export interface SyntheticViewTemplate<TSource = any, TParent = any, TGrandparen
create(): SyntheticView<TSource, TParent, TGrandparent>;
}

/**
* The result of a template compilation operation.
* @public
*/
export interface HTMLTemplateCompilationResult {
/**
* Creates a view instance.
* @param hostBindingTarget - The host binding target for the view.
*/
createView(hostBindingTarget?: Element): HTMLView;
}

/**
* A function capable of compiling a template from the preprocessed form produced
* by the html template function into a result that can instantiate views.
* @public
*/
export type HTMLTemplateCompiler = (
/**
* The preprocessed HTML string or template to compile.
*/
html: string | HTMLTemplateElement,
/**
* The directives used within the html that is being compiled.
*/
directives: readonly HTMLDirective[]
) => HTMLTemplateCompilationResult;

let compileTemplate: HTMLTemplateCompiler;

/**
* A template capable of creating HTMLView instances or rendering directly to DOM.
* @public
Expand Down Expand Up @@ -83,33 +111,10 @@ export class ViewTemplate<TSource = any, TParent = any, TGrandparent = any>
*/
public create(hostBindingTarget?: Element): HTMLView<TSource, TParent, TGrandparent> {
if (this.result === null) {
let template: HTMLTemplateElement;
const html = this.html;

if (isString(html)) {
template = document.createElement("template");
template.innerHTML = DOM.createHTML(html);

const fec = template.content.firstElementChild;

if (fec !== null && fec.tagName === "TEMPLATE") {
template = fec as HTMLTemplateElement;
}
} else {
template = html;
}

this.result = compileTemplate(template, this.directives);
this.result = compileTemplate(this.html, this.directives);
}

const result = this.result;
const fragment = result.fragment.cloneNode(true) as DocumentFragment;

return new HTMLView<TSource, TParent, TGrandparent>(
fragment,
result.factories,
result.createTargets(fragment, hostBindingTarget)
);
return this.result!.createView(hostBindingTarget);
}

/**
Expand All @@ -129,8 +134,19 @@ export class ViewTemplate<TSource = any, TParent = any, TGrandparent = any>
view.appendTo(host);
return view;
}

/**
* Sets the default compiler that will be used by the ViewTemplate whenever
* it needs to compile a view preprocessed with the html template function.
* @param compiler - The compiler to use when compiling templates.
*/
public static setDefaultCompiler(compiler: HTMLTemplateCompiler): void {
compileTemplate = compiler;
}
}

ViewTemplate.setDefaultCompiler(compileFASTTemplate);

// Much thanks to LitHTML for working this out!
const lastAttributeNameRegex =
/* eslint-disable-next-line no-control-regex */
Expand All @@ -154,7 +170,7 @@ export type TemplateValue<TSource, TParent = any> =
| CaptureType<TSource>;

/**
* Transforms a template literal string into a renderable ViewTemplate.
* Transforms a template literal string into a ViewTemplate.
* @param strings - The string fragments that are interpolated with the values.
* @param values - The values that are interpolated with the string fragments.
* @remarks
Expand Down

0 comments on commit 58b4c9d

Please sign in to comment.