diff --git a/.changeset/tangy-lights-tickle.md b/.changeset/tangy-lights-tickle.md new file mode 100644 index 0000000000..fe0bdad333 --- /dev/null +++ b/.changeset/tangy-lights-tickle.md @@ -0,0 +1,5 @@ +--- +"@effect-app/vue-components": patch +--- + +Adds support to nested constructors in OmegaForm defaultsValue with schema diff --git a/packages/vue-components/__tests__/OmegaForm/WithDefaultConstructorPersistency.test.ts b/packages/vue-components/__tests__/OmegaForm/WithDefaultConstructorPersistency.test.ts index 6b0ddd20e4..0a9237c331 100644 --- a/packages/vue-components/__tests__/OmegaForm/WithDefaultConstructorPersistency.test.ts +++ b/packages/vue-components/__tests__/OmegaForm/WithDefaultConstructorPersistency.test.ts @@ -17,13 +17,12 @@ describe("OmegaForm withDefaultConstructor with persistency", () => { third: S.NullOr(S.String).withDefault, fourth: S .Struct({ - addForm: S.NullOr(S.String), - b: S.PositiveNumber - }) - .pipe(S.withDefaultConstructor(() => ({ - addForm: null, - b: S.PositiveNumber(100) - }))), + addForm: S.NullOr(S.String).withDefault, + b: S.PositiveNumber.pipe(S.withDefaultConstructor(() => S.PositiveNumber(100))), + c: S.Struct({ + d: S.Number.pipe(S.withDefaultConstructor(() => 10)) + }) + }), fifth: S.Email, sixth: S.NumberFromString.pipe(S.withDefaultConstructor(() => 1000)) }) @@ -33,7 +32,7 @@ describe("OmegaForm withDefaultConstructor with persistency", () => { // Format: pathname-key1-key2-key3... // Keys from meta will be flattened with dot notation for nested fields const pathname = "/test" - const keys = ["first", "second", "third", "fourth.addForm", "fourth.b", "fifth", "sixth"] + const keys = ["first", "second", "third", "fourth.addForm", "fourth.b", "fourth.c.d", "fifth", "sixth"] const persistencyKey = `${pathname}-${keys.join("-")}` const queryValue = JSON.stringify({ first: 1234 }) @@ -95,7 +94,10 @@ describe("OmegaForm withDefaultConstructor with persistency", () => { third: null, // Default from NullOr withDefault fourth: { addForm: null, - b: 100 // Default from withDefaultConstructor + b: 100, // Default from withDefaultConstructor + c: { + d: 10 // Default from withDefaultConstructor + } }, sixth: "1000" }) diff --git a/packages/vue-components/src/components/OmegaForm/useOmegaForm.ts b/packages/vue-components/src/components/OmegaForm/useOmegaForm.ts index f3bc868ab1..19a0e7f20f 100644 --- a/packages/vue-components/src/components/OmegaForm/useOmegaForm.ts +++ b/packages/vue-components/src/components/OmegaForm/useOmegaForm.ts @@ -706,17 +706,66 @@ export const useOmegaForm = < return normalized } + // Helper function to recursively extract default values from schema AST + const extractDefaultsFromAST = (schemaObj: any): any => { + const result: Record = {} + + // Check if this schema has fields (struct) + if (schemaObj?.fields && typeof schemaObj.fields === "object") { + for (const [key, fieldSchema] of Object.entries(schemaObj.fields)) { + // Check if this field has a default value in its AST + if ((fieldSchema as any)?.ast?.defaultValue) { + try { + const defaultValue = (fieldSchema as any).ast.defaultValue() + if (defaultValue !== undefined) { + result[key] = defaultValue + } + } catch { + // Silently ignore if defaultValue() throws + } + } + + // Recursively check nested fields for structs + const nestedDefaults = extractDefaultsFromAST(fieldSchema as any) + if (Object.keys(nestedDefaults).length > 0) { + // If we already have a default value for this key, merge with nested + if (result[key] && typeof result[key] === "object") { + Object.assign(result[key], nestedDefaults) + } else if (!result[key]) { + // Only set nested defaults if we don't have a default value + result[key] = nestedDefaults + } + } + } + } + + return result + } + // Extract default values from schema constructors (e.g., withDefaultConstructor) const extractSchemaDefaults = (defaultValues: Partial = {}) => { try { + // First try to use schema.make() if available // Note: Partial schemas don't have .make() method yet (https://github.com/Effect-TS/effect/issues/4222) if ("make" in schema && typeof (schema as any).make === "function") { - const decoded = (schema as any).make(defaultValues, { disableValidation: true }) + const decoded = (schema as any).make(defaultValues) return S.encodeSync(schema.pipe(S.partial))(decoded) } } catch (error) { - console.warn("Could not extract schema constructor defaults:", error) - return {} + // If make() fails, try to extract defaults from AST + if (window.location.hostname === "localhost") { + console.warn("schema.make() failed, extracting defaults from AST:", error) + } + try { + const astDefaults = extractDefaultsFromAST(schema) + + return S.encodeSync(schema.pipe(S.partial))(astDefaults) + } catch (astError) { + if (window.location.hostname === "localhost") { + console.warn("Could not extract defaults from AST:", astError) + } + return {} + } } } diff --git a/packages/vue-components/stories/OmegaForm/WithDefaultConstructor.vue b/packages/vue-components/stories/OmegaForm/WithDefaultConstructor.vue index de102f7599..88afb31482 100644 --- a/packages/vue-components/stories/OmegaForm/WithDefaultConstructor.vue +++ b/packages/vue-components/stories/OmegaForm/WithDefaultConstructor.vue @@ -23,13 +23,12 @@ const AddSchema = S.Struct({ third: S.NullOr(S.String).withDefault, fourth: S .Struct({ - addForm: S.NullOr(S.String), - b: S.PositiveNumber - }) - .pipe(S.withDefaultConstructor(() => ({ - addForm: null, - b: S.PositiveNumber(100) - }))), + addForm: S.NullOr(S.String).withDefault, + b: S.PositiveNumber.pipe(S.withDefaultConstructor(() => S.PositiveNumber(100))), + c: S.Struct({ + d: S.Number.pipe(S.withDefaultConstructor(() => 10)) + }) + }), fifth: S.Email, sixth: S.NumberFromString.pipe(S.withDefaultConstructor(() => 1000)) })