Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/core/src/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import {
resolveDynamicValue,
getByPath,
findFormValue,
resolveRepeatStatePath,
resolveRepeatItemStatePath,
setByPath,
Expand Down Expand Up @@ -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");
Expand Down
17 changes: 14 additions & 3 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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;
}
}
Expand All @@ -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;
}
}
Expand Down