diff --git a/change/@microsoft-fast-test-harness-2473cf9b-b505-47b0-900d-60dbeda05163.json b/change/@microsoft-fast-test-harness-2473cf9b-b505-47b0-900d-60dbeda05163.json new file mode 100644 index 00000000000..5461bffec97 --- /dev/null +++ b/change/@microsoft-fast-test-harness-2473cf9b-b505-47b0-900d-60dbeda05163.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "add support for repeat and when directives", + "packageName": "@microsoft/fast-test-harness", + "email": "863023+radium-v@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/fast-test-harness/src/build/generate-templates.test.ts b/packages/fast-test-harness/src/build/generate-templates.test.ts index 7924f36ea23..00e5386611d 100644 --- a/packages/fast-test-harness/src/build/generate-templates.test.ts +++ b/packages/fast-test-harness/src/build/generate-templates.test.ts @@ -10,6 +10,28 @@ import { } from "@microsoft/fast-test-harness/build/generate-templates.js"; import { generateWebuiTemplates } from "@microsoft/fast-test-harness/build/generate-webui-templates.js"; +interface RepeatHost { + items: RepeatItem[]; +} + +interface RepeatChild { + name: string; +} + +interface RepeatItem { + children: RepeatChild[]; + label: string; + name: string; + visible: boolean; +} + +interface WhenHost { + items: RepeatItem[]; + label: string; + ready: boolean; + visible: boolean; +} + test.describe("convertTemplate", async () => { // Install the DOM shim before any tests — convertTemplate needs declarative // syntax constants which require a DOM environment, and FAST Element needs @@ -18,7 +40,9 @@ test.describe("convertTemplate", async () => { // Dynamic import after the DOM shim is installed so FAST Element can // access `document`, `CSSStyleSheet`, etc. - const { html, ref, slotted, children } = await import("@microsoft/fast-element"); + const { html, ref, slotted, children, elements, repeat, when } = await import( + "@microsoft/fast-element" + ); test("should wrap a static template in f-template tags", () => { const template = html``; const result = convertTemplate(template, "fast-test"); @@ -94,6 +118,34 @@ test.describe("convertTemplate", async () => { assert.ok(result.includes("slottedItems"), `got: ${result}`); }); + test("should convert SlottedDirective factories with elements filter", () => { + const template = html``; + const result = convertTemplate(template, "fast-slotted-elements"); + + assert.ok(result); + assert.ok( + result.includes('f-slotted="{slottedItems filter elements()}"'), + `got: ${result}`, + ); + }); + + test("should convert SlottedDirective factories with selector elements filter", () => { + const template = html``; + const result = convertTemplate(template, "fast-slotted-elements-selector"); + + assert.ok(result); + assert.ok( + result.includes('f-slotted="{slottedItems filter elements(p, ol)}"'), + `got: ${result}`, + ); + }); + test("should convert value bindings to {{expression}}", () => { const template = html``; const result = convertTemplate(template, "fast-binding"); @@ -136,6 +188,16 @@ test.describe("convertTemplate", async () => { assert.ok(result.includes("handleClick"), `got: ${result}`); }); + test("should convert custom event bindings", () => { + const template = html``; + const result = convertTemplate(template, "fast-custom-event"); + + assert.ok(result); + assert.ok(result.includes("@value-change="), `got: ${result}`); + assert.ok(result.includes("handleValueChange"), `got: ${result}`); + assert.ok(result.includes("$e"), `got: ${result}`); + }); + test("should convert property bindings to :prop expressions", () => { const template = html``; const result = convertTemplate(template, "fast-prop"); @@ -145,6 +207,37 @@ test.describe("convertTemplate", async () => { assert.ok(result.includes("currentValue"), `got: ${result}`); }); + test("should convert plain innerHTML wrappers to unescaped content bindings", () => { + const template = html``; + const result = convertTemplate(template, "fast-inner-html"); + + assert.ok(result); + assert.ok(result.includes("{{{html}}}"), `got: ${result}`); + assert.ok(!result.includes(":innerHTML"), `got: ${result}`); + }); + + test("should preserve innerHTML property bindings on divs with static attributes", () => { + const template = html``; + const result = convertTemplate(template, "fast-inner-html-attrs"); + + assert.ok(result); + assert.ok( + result.includes('
'), + `got: ${result}`, + ); + assert.ok(!result.includes("{{{html}}}"), `got: ${result}`); + }); + + test("should preserve innerHTML property bindings on divs with other bindings", () => { + const template = html``; + const result = convertTemplate(template, "fast-inner-html-bindings"); + + assert.ok(result); + assert.ok(result.includes(':innerHTML="{{html}}"'), `got: ${result}`); + assert.ok(result.includes('?hidden="{{hidden}}"'), `got: ${result}`); + assert.ok(!result.includes("{{{html}}}"), `got: ${result}`); + }); + test("should handle multiple factories in a single template", () => { const template = html``; const result = convertTemplate(template, "fast-multi"); @@ -162,6 +255,140 @@ test.describe("convertTemplate", async () => { assert.ok(result); assert.ok(result.includes("icon"), `got: ${result}`); }); + + test("should convert RepeatDirective factories to f-repeat directives", () => { + const itemTemplate = html`
  • ${x => x.label}
  • `; + const template = html``; + const result = convertTemplate(template, "fast-repeat"); + + assert.ok(result); + assert.ok( + result.includes( + '', + ), + `got: ${result}`, + ); + assert.ok(result.includes("
  • {{item.label}}
  • "), `got: ${result}`); + assert.ok(result.includes("
    "), `got: ${result}`); + assert.ok(!result.includes("`, + "g", + ); + fContent = fContent.replace(repeatBindingRe, (match: string, factoryId: string) => { + const factory = factoryMap.get(factoryId); + if (!factory) { + return match; + } + + return convertRepeatDirective(factory, options) ?? match; + }); + // Attribute-position markers → f-ref / f-slotted / f-children const attrBindingRe = new RegExp( `${prefix}-\\d+="${prefix}\\{(${prefix}-\\d+)\\}${prefix}"`, @@ -289,15 +535,14 @@ export function convertTemplate( typeof factory.options === "string" ? factory.options : factory.options?.property; - const filterStr = extractSlottedFilter(factory); - return attributeDirective("children", `${prop}${filterStr}`); + return attributeDirective("children", prop); } return match; }); // Event bindings → @event="{handler(e)}" const eventBindingRe = new RegExp( - `(@[a-z]+)="${prefix}\\{(${prefix}-\\d+)\\}${prefix}"`, + `(@[^\\s=]+)="${prefix}\\{(${prefix}-\\d+)\\}${prefix}"`, "g", ); fContent = fContent.replace( @@ -307,7 +552,7 @@ export function convertTemplate( if (!factory) { return match; } - const evalStr = extractBindingExpression(factory); + const evalStr = extractBindingExpression(factory, options); return `${aspect}="${wrapClientExpression(evalStr)}"`; }, ); @@ -324,11 +569,13 @@ export function convertTemplate( if (!factory) { return match; } - const evalStr = extractBindingExpression(factory); + const evalStr = extractBindingExpression(factory, options); return `${aspect}="${wrapDefaultExpression(evalStr)}"`; }, ); + fContent = convertInnerHTMLWrappers(fContent); + // Attribute-value and content bindings → attr="{{propName}}" or inline HTML const valBindingRe = new RegExp(`${prefix}\\{(${prefix}-\\d+)\\}${prefix}`, "g"); fContent = fContent.replace(valBindingRe, (match: string, factoryId: string) => { @@ -337,15 +584,46 @@ export function convertTemplate( return match; } + const whenDirective = convertWhenDirective(factory, options); + if (whenDirective !== null) { + return whenDirective; + } + const inlined = tryInlineStaticTemplate(factory); if (inlined !== null) { return inlined; } - const evalStr = extractBindingExpression(factory); + const evalStr = extractBindingExpression(factory, options); return wrapDefaultExpression(evalStr); }); + return fContent; +} + +/** + * Convert a ViewTemplate's html string and factories into an + * f-template HTML string. + */ +export function convertTemplate( + viewTemplate: ViewTemplate, + componentName: string, +): string | null { + const { factories } = viewTemplate; + const html = + typeof viewTemplate.html === "string" + ? viewTemplate.html + : viewTemplate.html.innerHTML; + + const factoryEntries = Object.entries(factories); + if (factoryEntries.length === 0) { + // No factories — pure static template. + const content = html.replace(templateWrapperTagPattern, "").trim(); + return `\n`; + } + + const fContent = convertTemplateHTML(viewTemplate); + let fInner = fContent.trim(); if (!templateOpenTagPattern.test(fInner)) { fInner = ``; diff --git a/packages/fast-test-harness/src/build/generate-webui-templates.test.ts b/packages/fast-test-harness/src/build/generate-webui-templates.test.ts index 3fee85bea9a..e1e43409675 100644 --- a/packages/fast-test-harness/src/build/generate-webui-templates.test.ts +++ b/packages/fast-test-harness/src/build/generate-webui-templates.test.ts @@ -153,6 +153,80 @@ test.describe("generateWebuiTemplates", () => { assert.ok(!html.includes("shadowrootdelegatesfocus")); }); + test("should convert f-when directives to webui if directives", async () => { + const distDir = join(tempDir, "dist"); + await mkdir(distDir, { recursive: true }); + + await writeFile( + join(distDir, "conditional.template.js"), + `export const template = { + html: '', + factories: {} + };`, + ); + + await generateWebuiTemplates({ cwd: tempDir, tagPrefix: "fast" }); + + const html = await readFile( + join(distDir, "conditional.template-webui.html"), + "utf8", + ); + assert.ok( + html.includes('{{label}}'), + `got: ${html}`, + ); + assert.ok(!html.includes("f-when"), `got: ${html}`); + }); + + test("should convert f-repeat directives to webui for directives", async () => { + const distDir = join(tempDir, "dist"); + await mkdir(distDir, { recursive: true }); + + await writeFile( + join(distDir, "list.template.js"), + `export const template = { + html: '', + factories: {} + };`, + ); + + await generateWebuiTemplates({ cwd: tempDir, tagPrefix: "fast" }); + + const html = await readFile(join(distDir, "list.template-webui.html"), "utf8"); + assert.ok( + html.includes('
  • {{item.name}}
  • '), + `got: ${html}`, + ); + assert.ok(!html.includes("f-repeat"), `got: ${html}`); + assert.ok(!html.includes("positioning"), `got: ${html}`); + assert.ok(!html.includes("recycle"), `got: ${html}`); + }); + + test("should convert nested f-when and f-repeat directives", async () => { + const distDir = join(tempDir, "dist"); + await mkdir(distDir, { recursive: true }); + + await writeFile( + join(distDir, "nested.template.js"), + `export const template = { + html: '', + factories: {} + };`, + ); + + await generateWebuiTemplates({ cwd: tempDir, tagPrefix: "fast" }); + + const html = await readFile(join(distDir, "nested.template-webui.html"), "utf8"); + assert.ok( + html.includes( + '

    {{item}}

    ', + ), + `got: ${html}`, + ); + assert.ok(!html.includes("f-when"), `got: ${html}`); + assert.ok(!html.includes("f-repeat"), `got: ${html}`); + }); + test("should strip the {{styles}} marker from output", async () => { const distDir = join(tempDir, "dist"); await mkdir(distDir, { recursive: true }); diff --git a/packages/fast-test-harness/src/build/generate-webui-templates.ts b/packages/fast-test-harness/src/build/generate-webui-templates.ts index e8b088ac11f..f8e0677bb35 100644 --- a/packages/fast-test-harness/src/build/generate-webui-templates.ts +++ b/packages/fast-test-harness/src/build/generate-webui-templates.ts @@ -7,6 +7,7 @@ * different wrapper: * - `