diff --git a/packages/core/src/types.test.ts b/packages/core/src/types.test.ts index 31701a95..b11ce780 100644 --- a/packages/core/src/types.test.ts +++ b/packages/core/src/types.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { resolveDynamicValue, getByPath, + findFormValue, resolveRepeatStatePath, resolveRepeatItemStatePath, setByPath, @@ -60,6 +61,38 @@ describe("getByPath", () => { }); }); +describe("findFormValue", () => { + it("returns a direct param value", () => { + expect(findFormValue("name", { name: "Alice" })).toBe("Alice"); + }); + + it("returns emails and other dotted literals from params", () => { + expect(findFormValue("email", { email: "john.doe@example.com" })).toBe( + "john.doe@example.com", + ); + expect(findFormValue("site", { site: "https://example.com" })).toBe( + "https://example.com", + ); + expect(findFormValue("version", { version: "1.2.3" })).toBe("1.2.3"); + }); + + it("still resolves dotted path references against state", () => { + expect( + findFormValue( + "email", + { email: "form.email" }, + { "form.email": "a@b.c" }, + ), + ).toBe("a@b.c"); + }); + + it("returns a dotted literal on a nested param key", () => { + expect( + findFormValue("email", { "form.email": "john.doe@example.com" }), + ).toBe("john.doe@example.com"); + }); +}); + describe("resolveRepeatStatePath", () => { it("preserves string paths exactly", () => { expect(resolveRepeatStatePath("/items")).toBe("/items"); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c051449a..c3757a7c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -514,6 +514,14 @@ function deepEqual(a: unknown, b: unknown): boolean { return aKeys.every((key) => deepEqual(aObj[key], bObj[key])); } +/** + * True for values that look like dotted state paths (`form.email`) rather + * than literals that happen to contain a dot (emails, URLs, versions). + */ +function isDotPathReference(val: string): boolean { + return /^[A-Za-z_][\w$]*(\.[A-Za-z_][\w$]*)+$/.test(val); +} + /** * Find a form value from params and/or state. * Useful in action handlers to locate form input values regardless of path format. @@ -524,6 +532,10 @@ function deepEqual(a: unknown, b: unknown): boolean { * 3. State keys ending with the field name (dot notation) * 4. State path using getByPath (slash notation) * + * A string is treated as a path reference only when it looks like + * `form.email` (identifier.identifier). Emails, URLs, and versions are + * returned as literals. + * * @example * // Find "name" from params or state * const name = findFormValue("name", params, state); @@ -538,8 +550,7 @@ export function findFormValue( // Check params first (but not if it looks like a state path reference) if (params?.[fieldName] !== undefined) { const val = params[fieldName]; - // If the value looks like a path reference (contains dots), skip it - if (typeof val !== "string" || !val.includes(".")) { + if (typeof val !== "string" || !isDotPathReference(val)) { return val; } } @@ -549,7 +560,7 @@ export function findFormValue( for (const key of Object.keys(params)) { if (key.endsWith(`.${fieldName}`)) { const val = params[key]; - if (typeof val !== "string" || !val.includes(".")) { + if (typeof val !== "string" || !isDotPathReference(val)) { return val; } }