-
Notifications
You must be signed in to change notification settings - Fork 632
fix: parse filter elements(...) syntax in f-children directive #7638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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( | ||
| '<ul f-children="{listItems}"></ul>', | ||
| 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( | ||
| '<ul f-children="{childItems filter elements()}"></ul>', | ||
| 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( | ||
| '<ul f-children="{childItems filter elements(li)}"></ul>', | ||
| 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); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<typeof elements>; | ||
| } { | ||
| const parts = propName.trim().split(" filter "); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const options: { property: string; filter?: ReturnType<typeof elements> } = { | ||
| 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; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We test in browser for declarative format, please check the fixture files for examples of how to do this. In this case you should see
fast-element/test/declarative/fixtures/directives/children.