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;
}