From 8324a5b2ed055c56ed3aff5e9eb0967df9575660 Mon Sep 17 00:00:00 2001 From: Mikkili Sai Prasad Reddy Date: Sat, 11 Jul 2026 22:04:57 +0530 Subject: [PATCH] fix: parse filter elements(...) syntax in f-children directive --- .../declarative/template-parser.pw.spec.ts | 125 ++++++++++++++++++ .../src/declarative/template-parser.ts | 47 ++++--- 2 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 packages/fast-element/src/declarative/template-parser.pw.spec.ts diff --git a/packages/fast-element/src/declarative/template-parser.pw.spec.ts b/packages/fast-element/src/declarative/template-parser.pw.spec.ts new file mode 100644 index 00000000000..f05c146e564 --- /dev/null +++ b/packages/fast-element/src/declarative/template-parser.pw.spec.ts @@ -0,0 +1,125 @@ +import { expect, test } from "@playwright/test"; + +// Vite serves local filesystem files under the /@fs/ prefix. +// Using .pathname (not fileURLToPath) mirrors the template-bridge pattern and +// correctly produces a leading slash on Windows (e.g. /@fs/E:/fast/...). +const pureDeclarativeEntrypointUrl = `/@fs${ + new URL("../../test/pure-declarative-main.ts", import.meta.url).pathname +}`; + +test.describe("TemplateParser", () => { + test.describe("f-children directive", () => { + test("without filter produces a ChildrenDirective with the correct property", async ({ + page, + }) => { + await page.goto("/"); + + const result = await page.evaluate(async declarativeUrl => { + // @ts-expect-error: Client module. + const { Schema, TemplateParser } = await import(declarativeUrl); + // @ts-expect-error: Client module. + const { ChildrenDirective } = await import("/main.js"); + + const parser = new TemplateParser(); + const schema = new Schema("test-element"); + const { values } = parser.parse( + '', + schema, + ); + const directive = values[0]; + + return { + isChildrenDirective: directive instanceof ChildrenDirective, + property: directive.options.property, + hasFilter: "filter" in directive.options, + }; + }, pureDeclarativeEntrypointUrl); + + expect(result.isChildrenDirective).toBe(true); + expect(result.property).toBe("listItems"); + expect(result.hasFilter).toBe(false); + }); + + test("with filter elements() produces a ChildrenDirective with an element filter", async ({ + page, + }) => { + await page.goto("/"); + + const result = await page.evaluate(async declarativeUrl => { + // @ts-expect-error: Client module. + const { Schema, TemplateParser } = await import(declarativeUrl); + // @ts-expect-error: Client module. + const { ChildrenDirective } = await import("/main.js"); + + const parser = new TemplateParser(); + const schema = new Schema("test-element"); + const { values } = parser.parse( + '', + schema, + ); + const directive = values[0]; + const filter = directive.options.filter; + + // Use real DOM nodes so element.matches() is available. + const elementNode = document.createElement("div"); + const textNode = document.createTextNode("hello"); + + return { + isChildrenDirective: directive instanceof ChildrenDirective, + property: directive.options.property, + hasFilter: typeof filter === "function", + filterAcceptsElement: filter(elementNode), + filterRejectsText: filter(textNode), + }; + }, pureDeclarativeEntrypointUrl); + + expect(result.isChildrenDirective).toBe(true); + expect(result.property).toBe("childItems"); + expect(result.hasFilter).toBe(true); + expect(result.filterAcceptsElement).toBe(true); + expect(result.filterRejectsText).toBe(false); + }); + + test("with filter elements(li) produces a ChildrenDirective with a selector filter", async ({ + page, + }) => { + await page.goto("/"); + + const result = await page.evaluate(async declarativeUrl => { + // @ts-expect-error: Client module. + const { Schema, TemplateParser } = await import(declarativeUrl); + // @ts-expect-error: Client module. + const { ChildrenDirective } = await import("/main.js"); + + const parser = new TemplateParser(); + const schema = new Schema("test-element"); + const { values } = parser.parse( + '', + schema, + ); + const directive = values[0]; + const filter = directive.options.filter; + + const li = document.createElement("li"); + const div = document.createElement("div"); + const text = document.createTextNode("hello"); + + return { + isChildrenDirective: directive instanceof ChildrenDirective, + property: directive.options.property, + hasFilter: typeof filter === "function", + filterAcceptsLi: filter(li), + filterResultDiv: filter(div), + filterResultText: filter(text), + }; + }, pureDeclarativeEntrypointUrl); + + expect(result.isChildrenDirective).toBe(true); + expect(result.property).toBe("childItems"); + expect(result.hasFilter).toBe(true); + expect(result.filterAcceptsLi).toBe(true); + expect(result.filterResultDiv).toBe(false); + expect(result.filterResultText).toBe(false); + }); + }); +}); diff --git a/packages/fast-element/src/declarative/template-parser.ts b/packages/fast-element/src/declarative/template-parser.ts index 4df9c5e8993..530e0fdbdd7 100644 --- a/packages/fast-element/src/declarative/template-parser.ts +++ b/packages/fast-element/src/declarative/template-parser.ts @@ -219,6 +219,29 @@ export class TemplateParser { } } + /** + * Parses a directive attribute value that may include a " filter elements(...)" suffix. + * Returns an options object with `property` and, when a filter is specified, `filter`. + * Shared by the `children` and `slotted` directive cases. + */ + private parseNodeDirectiveOptions(propName: string): { + property: string; + filter?: ReturnType; + } { + const parts = propName.trim().split(" filter "); + const options: { property: string; filter?: ReturnType } = { + property: parts[0], + }; + + if (parts[1]?.startsWith("elements(")) { + const inner = parts[1].slice("elements(".length); + const params = inner.substring(0, inner.lastIndexOf(")")); + options.filter = elements(params || undefined); + } + + return options; + } + /** * Resolve an attribute directive (children/slotted/ref). * @param name - The name of the directive. @@ -232,27 +255,17 @@ export class TemplateParser { ): void { switch (name) { case "children": { - externalValues.push(children(propName)); + const options = this.parseNodeDirectiveOptions(propName); + externalValues.push( + options.filter !== undefined + ? children(options) + : children(options.property), + ); break; } case "slotted": { - const parts = propName.trim().split(" filter "); - const slottedOption = { - property: parts[0], - }; - - if (parts[1]) { - if (parts[1].startsWith("elements(")) { - let params = parts[1].replace("elements(", ""); - params = params.substring(0, params.lastIndexOf(")")); - Object.assign(slottedOption, { - filter: elements(params || undefined), - }); - } - } - - externalValues.push(slotted(slottedOption)); + externalValues.push(slotted(this.parseNodeDirectiveOptions(propName))); break; }