diff --git a/change/@microsoft-fast-element-5027f6e6-0686-40b1-965f-adc905136502.json b/change/@microsoft-fast-element-5027f6e6-0686-40b1-965f-adc905136502.json new file mode 100644 index 00000000000..2def93343e3 --- /dev/null +++ b/change/@microsoft-fast-element-5027f6e6-0686-40b1-965f-adc905136502.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "refactor: enable pluggable template compiler for SSR scenarios", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 2fb991b5652..61d4c91a073 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -140,7 +140,7 @@ export class ChildrenDirective extends NodeObservationDirective = ChildListDirectiveOptions | SubtreeDirectiveOptions; // @public -export function compileTemplate(template: HTMLTemplateElement, directives: ReadonlyArray): HTMLTemplateCompilationResult; +export function compileTemplate(html: string | HTMLTemplateElement, directives: ReadonlyArray): HTMLTemplateCompilationResult; // @public export type ComposableStyles = string | ElementStyles | CSSStyleSheet; @@ -347,11 +347,14 @@ export abstract class HTMLDirective implements ViewBehaviorFactory { // @public export interface HTMLTemplateCompilationResult { - createTargets(root: Node, host?: Node): ViewBehaviorTargets; - readonly factories: ReadonlyArray; - readonly fragment: DocumentFragment; + createView(hostBindingTarget?: Element): HTMLView; } +// @public +export type HTMLTemplateCompiler = ( +html: string | HTMLTemplateElement, +directives: readonly HTMLDirective[]) => HTMLTemplateCompilationResult; + // @public export class HTMLView implements ElementView, SyntheticView { constructor(fragment: DocumentFragment, factories: ReadonlyArray, targets: ViewBehaviorTargets); @@ -644,7 +647,8 @@ export class ViewTemplate impl readonly directives: ReadonlyArray; readonly html: string | HTMLTemplateElement; render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView; - } + static setDefaultCompiler(compiler: HTMLTemplateCompiler): void; +} // @public export function volatile(target: {}, name: string | Accessor, descriptor: PropertyDescriptor): PropertyDescriptor; diff --git a/packages/web-components/fast-element/src/styles/element-styles.ts b/packages/web-components/fast-element/src/styles/element-styles.ts index 9743105fb72..7bccb8377c2 100644 --- a/packages/web-components/fast-element/src/styles/element-styles.ts +++ b/packages/web-components/fast-element/src/styles/element-styles.ts @@ -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; } } diff --git a/packages/web-components/fast-element/src/templating/compiler.spec.ts b/packages/web-components/fast-element/src/templating/compiler.spec.ts index 4b0599d72a1..cb7b9a6f1c5 100644 --- a/packages/web-components/fast-element/src/templating/compiler.spec.ts +++ b/packages/web-components/fast-element/src/templating/compiler.spec.ts @@ -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) { diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 6c832dad180..abbfdf8efe9 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -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}`; @@ -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; - - /** - * 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(); @@ -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, @@ -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); } @@ -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; @@ -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 @@ -277,9 +256,24 @@ function compileNode( * @public */ export function compileTemplate( - template: HTMLTemplateElement, + html: string | HTMLTemplateElement, directives: ReadonlyArray ): 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); diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts index 44c9f3237e2..3f466c9f423 100644 --- a/packages/web-components/fast-element/src/templating/repeat.ts +++ b/packages/web-components/fast-element/src/templating/repeat.ts @@ -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"; @@ -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"; diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 4d9da0160c1..cedf6b000b3 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -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. @@ -43,6 +41,36 @@ export interface SyntheticViewTemplate; } +/** + * 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 @@ -83,33 +111,10 @@ export class ViewTemplate */ public create(hostBindingTarget?: Element): HTMLView { 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( - fragment, - result.factories, - result.createTargets(fragment, hostBindingTarget) - ); + return this.result!.createView(hostBindingTarget); } /** @@ -129,8 +134,19 @@ export class ViewTemplate 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 */ @@ -154,7 +170,7 @@ export type TemplateValue = | CaptureType; /** - * 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