diff --git a/.changeset/chatty-seas-cross.md b/.changeset/chatty-seas-cross.md new file mode 100644 index 0000000..b8c5dd7 --- /dev/null +++ b/.changeset/chatty-seas-cross.md @@ -0,0 +1,6 @@ +--- +"@dynz/react-hook-form": minor +"dynz": minor +--- + +added discriminated union schema diff --git a/examples/example/src/index.ts b/examples/example/src/index.ts index 1b0594a..2e56cf6 100644 --- a/examples/example/src/index.ts +++ b/examples/example/src/index.ts @@ -1,7 +1,70 @@ import * as d from "dynz"; -import { runExample } from "./registration-form"; -runExample(); +// runExample(); + +async function oldExample() { + const schema = d.object({ + type: d.options(["aluminium-side-wall", "glass-sliding-door"]), + + aluminium_side_wall: d + .object({ + shiplap_color: d.options(["color_a", "color_b"]), + }) + .setIncluded(d.eq(d.ref("type"), "aluminium-side-wall")), + + glass_sliding_door: d + .object({ + rail_type: d.options(["2-rails", "3-rails", "4-rails", "5-rails", "6-rails"]), + }) + .setIncluded(d.eq(d.ref("type"), "glass-sliding-door")), + }); + + const result = await d.validate(schema, undefined, {}); + + if (result.success) { + const values = result.values; + // console.log(result.values.wall) + } +} + +async function runExample() { + const schema = d.object({ + wall: d.discriminatedUnion("type", [ + { + type: "aluminium-side-wall", + shiplap_color: d.options(["color_a", "color_b"]), + }, + { + type: "glass-sliding-door", + foo: d.string().setRequired(false), + rail_type: d.options(["2-rails", "3-rails", "4-rails", "5-rails", "6-rails"]).setIncluded(false), + }, + ]), + }); + + const result = await d.validate(schema, undefined, {}); + + if (result.success) { + const values = result.values; + + if (values.wall.type === "glass-sliding-door") { + values.wall.rail_type; + } + } +} + +// console.log(res) + +// if (res.success) { + +// // if (res.values.foo.type === 1) { +// // const a = res.values.foo.foo +// // } + +// } +// } + +// foo().then(() => console.log('damn..')) // console.log(`stringSchema result:`, d.validate(stringSchema, undefined, { // foo: [{ diff --git a/examples/next-example/messages/en.json b/examples/next-example/messages/en.json index 2adcc25..e119d72 100644 --- a/examples/next-example/messages/en.json +++ b/examples/next-example/messages/en.json @@ -3,6 +3,25 @@ "title": "Hello world!", "about": "Go to the about page" }, + "contactForm": { + "name": { + "label": "Naam", + "placeholder": "Bijv. Jan" + }, + "contactDetails": { + "type": { + "label": "Type" + }, + "email": { + "label": "Email", + "placeholder": "Bijv. Jan" + }, + "phone": { + "label": "Phone", + "placeholder": "Bijv. Jan" + } + } + }, "pensionPersonalInfo": { "firstName": { "label": "Voornaam", diff --git a/examples/next-example/package.json b/examples/next-example/package.json index 4881dda..093de86 100644 --- a/examples/next-example/package.json +++ b/examples/next-example/package.json @@ -19,7 +19,7 @@ "lucide-react": "^0.542.0", "next": "15.3.5", "next-intl": "^4.3.4", - "react-hook-form": "^7.62.0", + "react-hook-form": "^7.75.0", "tailwind-merge": "^3.3.1", "zod": "^3.25.76" }, diff --git a/examples/next-example/src/app/[locale]/discriminated-union/page.tsx b/examples/next-example/src/app/[locale]/discriminated-union/page.tsx new file mode 100644 index 0000000..1fba3ec --- /dev/null +++ b/examples/next-example/src/app/[locale]/discriminated-union/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { discriminatedUnion, literal, object, ref, string } from "dynz"; +import { DynzForm } from "@/components/dynz/form"; +import { DynzInput } from "@/components/dynz/input"; +import { DynzUnionKey } from "@/components/dynz/union-key"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; + +const schema = object({ + name: string(), + contactDetails: discriminatedUnion("type", [ + { + type: "email", + email: string(), + }, + { + type: "phone", + phone: string(), + }, + ]), +}); + +export default function Home() { + return ( + + + + + + + + + + + + ); +} diff --git a/examples/next-example/src/components/dynz/union-key.tsx b/examples/next-example/src/components/dynz/union-key.tsx new file mode 100644 index 0000000..17c7b65 --- /dev/null +++ b/examples/next-example/src/components/dynz/union-key.tsx @@ -0,0 +1,43 @@ +import { useDiscriminatedUnionKeyValues } from "@dynz/react-hook-form"; +import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "../ui/form"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; +import { DynzFormField } from "./dynz-form-field"; + +export type DynzSelectProps = { + name: string; +}; + +export function DynzUnionKey({ name }: DynzSelectProps) { + // Get options from schema if not provided via props + const options = useDiscriminatedUnionKeyValues(name); + + return ( + ( + + + {translations.label} + {required && " *"} + + + {translations.description && {translations.description}} + + + )} + /> + ); +} diff --git a/packages/dynz/src/conditions/get-condition-dependencies.ts b/packages/dynz/src/conditions/get-condition-dependencies.ts index 0fd91cd..ff69bb5 100644 --- a/packages/dynz/src/conditions/get-condition-dependencies.ts +++ b/packages/dynz/src/conditions/get-condition-dependencies.ts @@ -155,6 +155,30 @@ function _getRulesDependenciesMap(schema: Schema, path: string, root: Schema): R } break; } + case SchemaType.DISCRIMINATED_UNION: { + for (const member of schema.schemas) { + for (const [fieldKey, fieldSchema] of Object.entries(member)) { + if (typeof fieldSchema === "string" || typeof fieldSchema === "number" || typeof fieldSchema === "boolean") { + continue; + } + + // add discriminated union key as a dependency + addDependencies(`${path}.${schema.key}`, [`${path}.${fieldKey}`]); + + const childDependencies = _getRulesDependenciesMap(fieldSchema, `${path}.${fieldKey}`, root); + Object.assign(result.dependencies, childDependencies.dependencies); + for (const [dep, dependents] of Object.entries(childDependencies.reverse)) { + if (!result.reverse[dep]) { + result.reverse[dep] = new Set(); + } + for (const dependent of dependents) { + result.reverse[dep].add(dependent); + } + } + } + } + break; + } } return result; diff --git a/packages/dynz/src/conditions/resolve-property.ts b/packages/dynz/src/conditions/resolve-property.ts index a518c51..1328e85 100644 --- a/packages/dynz/src/conditions/resolve-property.ts +++ b/packages/dynz/src/conditions/resolve-property.ts @@ -18,9 +18,20 @@ export function resolveProperty( for (let i = 1; i <= segments.length; i++) { const currentPath = segments.slice(0, i).join("."); const nested = getNested(currentPath, context.schema, context.values); + + // getNested returns null when the path cannot be resolved — this happens when + // navigating through a discriminated union whose discriminator value does not + // match any member (e.g. the field is empty or the current value is an excluded + // member). Treat this as "not accessible" → behave as if not included. + if (nested === null) { + return false; + } + const ret = _resolveProperty(nested.schema, property, currentPath, defaultValue, context); - if (!ret) return false; + if (ret === false) { + return false; + } } return _resolveProperty(context.schema, property, path, defaultValue, context); diff --git a/packages/dynz/src/functions/resolve.ts b/packages/dynz/src/functions/resolve.ts index 87ac0cf..3024c78 100644 --- a/packages/dynz/src/functions/resolve.ts +++ b/packages/dynz/src/functions/resolve.ts @@ -16,7 +16,18 @@ export function unpackRef( ...expected: T[] ): ValueType | undefined { const absolutePath = ensureAbsolutePath(ref.path, path); - const { schema, value } = getNested(absolutePath, context.schema, context.values); + + // getNested returns null when the path cannot be resolved — this happens when + // navigating through a discriminated union whose discriminator value does not + // match any member (e.g. the field is empty or the current value is an excluded + // member). Treat this as "not accessible" → behave as if not included. + const ret = getNested(absolutePath, context.schema, context.values); + + if (ret === null) { + return undefined; + } + + const { schema, value } = ret; // only return when the schema is actually included if (!isIncluded(context.schema, absolutePath, context.values)) { diff --git a/packages/dynz/src/schemas/discriminated-union/fluent.ts b/packages/dynz/src/schemas/discriminated-union/fluent.ts new file mode 100644 index 0000000..be8da09 --- /dev/null +++ b/packages/dynz/src/schemas/discriminated-union/fluent.ts @@ -0,0 +1,55 @@ +import type { Predicate } from "../../functions"; +import type { JsonRecord, Schema } from "../../types"; +import { SchemaType } from "../../types"; +import type { CheckMember, DiscriminatedUnionSchema } from "./types"; + +type SchemaMember = Record; + +export type DiscriminatedUnionFluent< + TKey extends string, + TSchemas extends SchemaMember[], + TProps, +> = DiscriminatedUnionSchema & + TProps & { + setRequired:

( + value: P + ) => DiscriminatedUnionFluent; + optional: () => DiscriminatedUnionFluent; + setMutable:

( + value: P + ) => DiscriminatedUnionFluent; + setIncluded:

( + value: P + ) => DiscriminatedUnionFluent; + setPrivate:

(value: P) => DiscriminatedUnionFluent; + setUi: (config: TUI) => DiscriminatedUnionFluent; + }; + +function createFluent( + key: TKey, + schemas: TMembers, + props: TProps +): DiscriminatedUnionFluent { + const setProp = (k: K, v: V): DiscriminatedUnionFluent> => + createFluent(key, schemas, { ...props, [k]: v } as TProps & Record); + + return { + type: SchemaType.DISCRIMINATED_UNION, + key, + schemas, + ...props, + setRequired:

(v: P) => setProp("required", v), + optional: () => setProp("required", false as false), + setMutable:

(v: P) => setProp("mutable", v), + setIncluded:

(v: P) => setProp("included", v), + setPrivate:

(v: P) => setProp("private", v), + setUi: (config: TUI) => setProp("ui", config), + } as DiscriminatedUnionFluent; +} + +export function discriminatedUnion< + const TKey extends string, + const TSchemas extends { [I in keyof TSchemas]: CheckMember }, +>(key: TKey, schemas: TSchemas): DiscriminatedUnionFluent> { + return createFluent(key, schemas as TSchemas & SchemaMember[], {} as Record); +} diff --git a/packages/dynz/src/schemas/discriminated-union/index.ts b/packages/dynz/src/schemas/discriminated-union/index.ts new file mode 100644 index 0000000..12eb95b --- /dev/null +++ b/packages/dynz/src/schemas/discriminated-union/index.ts @@ -0,0 +1,2 @@ +export * from "./fluent"; +export * from "./types"; diff --git a/packages/dynz/src/schemas/discriminated-union/types.ts b/packages/dynz/src/schemas/discriminated-union/types.ts new file mode 100644 index 0000000..5e05ac8 --- /dev/null +++ b/packages/dynz/src/schemas/discriminated-union/types.ts @@ -0,0 +1,19 @@ +import type { BaseSchema, Schema, SchemaType } from "../../types"; + +// Per-element validation type: discriminator key accepts primitives, all other keys must be Schema. +// Used as a self-referential constraint in discriminatedUnion() so TypeScript checks each key +// individually rather than collapsing the condition into a single index signature. +export type CheckMember = { + [K in keyof T]: K extends TKey ? string | number | boolean : T[K] extends Schema ? T[K] : never; +}; + +export type DiscriminatedUnionSchema< + TKey extends string = string, + TSchemas extends Record[] = Record< + string, + Schema | string | number | boolean + >[], +> = BaseSchema & { + key: TKey; + schemas: [TSchemas] extends [never] ? Record[] : TSchemas; +}; diff --git a/packages/dynz/src/schemas/index.ts b/packages/dynz/src/schemas/index.ts index 84c46fe..a62da0a 100644 --- a/packages/dynz/src/schemas/index.ts +++ b/packages/dynz/src/schemas/index.ts @@ -9,4 +9,5 @@ export * from "./number"; export * from "./object"; export * from "./options"; export * from "./shared"; +export * from "./discriminated-union"; export * from "./string"; diff --git a/packages/dynz/src/types/schema.ts b/packages/dynz/src/types/schema.ts index 21d38e5..5f7371f 100644 --- a/packages/dynz/src/types/schema.ts +++ b/packages/dynz/src/types/schema.ts @@ -4,6 +4,7 @@ import type { ArraySchema, BooleanSchema, DateSchema, + DiscriminatedUnionSchema, DynamicOptionValue, EnumValue, EnumValues, @@ -13,6 +14,7 @@ import type { OptionsSchema, StringSchema, } from "../schemas"; + import type { EnumSchema } from "../schemas/enum"; import type { ExpressionSchema } from "../schemas/expression"; import type { LiteralSchema } from "../schemas/literal"; @@ -31,6 +33,7 @@ export const SchemaType = { FILE: "file", EXPRESSION: "expression", LITERAL: "literal", + DISCRIMINATED_UNION: "discriminated_union", } as const; export type SchemaType = EnumValues; @@ -61,7 +64,8 @@ export type Schema = | DateSchema | EnumSchema | ExpressionSchema - | LiteralSchema; + | LiteralSchema + | DiscriminatedUnionSchema; export type IsIncluded = T extends { included: true } ? true @@ -117,7 +121,9 @@ export type ValueType = T extends typeof Sche ? unknown : T extends typeof SchemaType.LITERAL ? string | number | boolean | null - : never; + : T extends typeof SchemaType.DISCRIMINATED_UNION + ? Record + : never; export type ValueTypeOrUndefined = ValueType | undefined | Array; @@ -135,6 +141,19 @@ type RequiredFields> = { export type ObjectValue> = OptionalFields & RequiredFields; +type DiscriminatedMemberValue< + TKey extends string, + TMember extends Record, +> = TMember extends Record + ? { + [K in keyof TMember as K extends TKey ? K : TMember[K] extends Schema ? K : never]: K extends TKey + ? TMember[K] + : TMember[K] extends Schema + ? SchemaValuesInternal + : never; + } + : never; + export type SchemaValuesInternal = T extends ObjectSchema ? Prettify> : T extends ArraySchema @@ -143,8 +162,10 @@ export type SchemaValuesInternal = T extends ObjectSchema> : T extends OptionsSchema ? MakeOptional>> - : T extends LiteralSchema - ? MakeOptional - : MakeOptional>; + : T extends DiscriminatedUnionSchema + ? MakeOptional> + : T extends LiteralSchema + ? MakeOptional + : MakeOptional>; export type SchemaValues = Prettify>>; diff --git a/packages/dynz/src/utils/find-schema-by-path.ts b/packages/dynz/src/utils/find-schema-by-path.ts index 7f20684..94f82db 100644 --- a/packages/dynz/src/utils/find-schema-by-path.ts +++ b/packages/dynz/src/utils/find-schema-by-path.ts @@ -27,6 +27,30 @@ export function findSchemaByPath(path: string, schema return childSchema; } + if (prev.type === SchemaType.DISCRIMINATED_UNION) { + for (const member of prev.schemas) { + const childSchema = member[cur]; + + if (childSchema !== undefined) { + /** + * When the childSchema is a primitive type then we need + * to return the union schema since the union type doesnt + * have an actual schema attached to it. + */ + if ( + typeof childSchema === "boolean" || + typeof childSchema === "number" || + typeof childSchema === "string" + ) { + return prev; + } + + return childSchema; + } + } + throw new Error(`No schema found for path ${path}`); + } + throw new Error(`Cannot find schema at path ${path}`); }, schema); diff --git a/packages/dynz/src/utils/get-nested.ts b/packages/dynz/src/utils/get-nested.ts index 141b92a..06d990c 100644 --- a/packages/dynz/src/utils/get-nested.ts +++ b/packages/dynz/src/utils/get-nested.ts @@ -6,13 +6,17 @@ export function getNested( path: string, schema: T, value: unknown -): { schema: Schema; value: unknown } { +): { schema: Schema; value: unknown } | null { const result = path .split(/[.[\]]/) .filter(Boolean) .splice(1) - .reduce<{ schema: Schema; value: unknown }>( + .reduce<{ schema: Schema; value: unknown } | null>( (acc, cur) => { + if (acc === null) { + return null; + } + if (acc.schema.type === SchemaType.ARRAY) { if (!Array.isArray(acc.value)) { throw new Error(`Expected an array at path ${path}, but got ${typeof acc.value}`); @@ -45,13 +49,59 @@ export function getNested( }; } + if (acc.schema.type === SchemaType.DISCRIMINATED_UNION) { + // if the key is referenced return the schema of the union type + if (cur === acc.schema.key) { + return { + value: isObject(acc.value) ? acc.value[acc.schema.key] : undefined, + schema: acc.schema, + }; + } + + if (acc.value === undefined || acc.value === null) { + return null; + } + + if (!isObject(acc.value)) { + throw new Error(`Expected an object at path ${path}, but got ${typeof acc.value}`); + } + const { key } = acc.schema; + + const discriminatorValue = acc.value[key]; + const matchingMember = acc.schema.schemas.find((s) => s[key] === discriminatorValue); + + if (matchingMember === undefined) { + return null; + } + + const childSchema = matchingMember[cur]; + + if (childSchema === undefined) { + return null; + } + + if (typeof childSchema === "string" || typeof childSchema === "number" || typeof childSchema === "boolean") { + return { + value: isObject(acc.value) ? acc.value[acc.schema.key] : undefined, + schema: acc.schema, + }; + } + + return { + value: acc.value, + schema: childSchema, + }; + } + throw new Error("Cannot get nested value on non array or non object"); }, { value, schema } ); - return { - schema: result.schema, - value: coerceSchema(result.schema, result.value), - }; + return result === null + ? null + : { + schema: result.schema, + value: coerceSchema(result.schema, result.value), + }; } diff --git a/packages/dynz/src/validate/validate-type.ts b/packages/dynz/src/validate/validate-type.ts index f3dc4b9..07ad195 100644 --- a/packages/dynz/src/validate/validate-type.ts +++ b/packages/dynz/src/validate/validate-type.ts @@ -38,6 +38,8 @@ export function validateType( return true; case SchemaType.LITERAL: return value === schema.value; + case SchemaType.DISCRIMINATED_UNION: + return isObject(value); } } @@ -74,6 +76,8 @@ export function validateShallowType(type: T, value: unknow return true; case SchemaType.LITERAL: return isString(value) || isNumber(value) || isBoolean(value) || value === null; + case SchemaType.DISCRIMINATED_UNION: + return true; } } diff --git a/packages/dynz/src/validate/validate.ts b/packages/dynz/src/validate/validate.ts index 71c5f0c..f80b8b3 100644 --- a/packages/dynz/src/validate/validate.ts +++ b/packages/dynz/src/validate/validate.ts @@ -1,4 +1,4 @@ -import { resolveProperty, resolveRules } from "../conditions"; +import { _resolveProperty, resolveProperty, resolveRules } from "../conditions"; import { resolve } from "../functions"; import { isPivateValue, isValueMasked, type PrivateValue } from "../private"; import { validateRule } from "../rules"; @@ -11,6 +11,7 @@ import { type ValidateOptions, type ValidateRuleContextUnion, type ValidationResult, + type ValidationSuccesResult, } from "../types"; import { coerceSchema } from "../utils"; import { isArray, isObject, validateType } from "./validate-type"; @@ -326,40 +327,79 @@ export async function _validate( return acc; } - // return newValue.reduce>( - // (acc, cur, index) => { - // const result = _validate( - // schema.schema, - // { - // current: currentValue?.[index], - // new: cur, - // }, - // `${path}.[${index}]`, - // newContext - // ); - - // if (acc.success) { - // if (result.success) { - // acc.values.push(result.values); - // return acc; - // } - - // return { - // success: false, - // errors: result.errors, - // }; - // } - - // if (result.success) { - // return acc; - // } - - // acc.errors.push(...result.errors); - // return acc; - // }, - // { success: true, values: [] } - // ); - // } + /** + * Validate discriminated union - find the matching member by the discriminator key value. + */ + if (schema.type === SchemaType.DISCRIMINATED_UNION) { + if (!isObject(newValue)) { + throw new Error(`new value is not an object: ${newValue}`); + } + + if (isDefined(currentValue) && !isObject(currentValue)) { + throw new Error(`current value is not an object: ${currentValue}`); + } + + const discriminatorValue = newValue[schema.key]; + const matchingMember = schema.schemas.find((s) => s[schema.key] === discriminatorValue); + + if (matchingMember === undefined) { + return { + success: false, + errors: [ + { + path, + schema, + value: newValue, + current: currentValue, + customCode: ErrorCode.TYPE, + code: ErrorCode.TYPE, + expectedType: schema.type, + message: `The value for schema ${path} does not match any member of the discriminated union (key: ${schema.key}=${String(discriminatorValue)})`, + }, + ], + }; + } + + const entries = await Promise.all( + Object.entries(matchingMember).map(async ([key, innerSchema]) => { + if (typeof innerSchema === "string" || typeof innerSchema === "boolean" || typeof innerSchema === "number") { + return { + key, + result: { + success: true, + values: innerSchema, + } as ValidationSuccesResult, + }; + } + + return { + key, + result: await _validate( + innerSchema, + { current: currentValue?.[key], new: newValue[key] }, + `${path}.${key}`, + context + ), + }; + }) + ); + + let acc = { success: true, values: {} } as ValidationResult>; + + for (const { key, result } of entries) { + if (acc.success) { + if (result.success) { + acc.values[key] = result.values; + } else { + acc = { success: false, errors: result.errors }; + } + } else if (!result.success) { + acc.errors.push(...result.errors); + } + } + + return acc; + } return { success: true, diff --git a/packages/react-hook-form/src/hooks/index.ts b/packages/react-hook-form/src/hooks/index.ts index 1244194..15a2c45 100644 --- a/packages/react-hook-form/src/hooks/index.ts +++ b/packages/react-hook-form/src/hooks/index.ts @@ -1,3 +1,4 @@ +export * from "./use-discriminated-union-key-values"; export * from "./use-dynz-form"; export * from "./use-dynz-form-context"; export * from "./use-is-included"; diff --git a/packages/react-hook-form/src/hooks/use-conditional-property.ts b/packages/react-hook-form/src/hooks/use-conditional-property.ts index 72ecb0d..3eb49f7 100644 --- a/packages/react-hook-form/src/hooks/use-conditional-property.ts +++ b/packages/react-hook-form/src/hooks/use-conditional-property.ts @@ -1,4 +1,4 @@ -import { findSchemaByPath, getConditionDependencies, resolveProperty } from "dynz"; +import { findSchemaByPath, getConditionDependencies, resolveProperty, SchemaType } from "dynz"; import { useMemo } from "react"; import { useWatch } from "react-hook-form"; import { useDynzFormContext } from "./use-dynz-form-context"; @@ -13,11 +13,23 @@ function getPropertyDependencies( const segments = path.split(/[.[\]]/).filter(Boolean); return segments.flatMap((_, i) => { const ancestorPath = segments.slice(0, i + 1).join("."); - const ancestorPropertyValue = findSchemaByPath(ancestorPath, schema)[property]; + const ancestor = findSchemaByPath(ancestorPath, schema); + + // When ancestor is a discriminuted union the 'key' of the union + // must always be added as an implicit dependency + const dependencies = + ancestor.type === SchemaType.DISCRIMINATED_UNION ? [`${ancestorPath}.${ancestor.key}`.slice(2)] : []; + + const ancestorPropertyValue = ancestor[property]; + if (ancestorPropertyValue === undefined || typeof ancestorPropertyValue === "boolean") { - return []; + return dependencies; } - return getConditionDependencies(ancestorPropertyValue, ancestorPath, schema).map((f) => f.slice(2)); + + return [ + ...dependencies, + ...getConditionDependencies(ancestorPropertyValue, ancestorPath, schema).map((f) => f.slice(2)), + ]; }); } diff --git a/packages/react-hook-form/src/hooks/use-discriminated-union-key-values.ts b/packages/react-hook-form/src/hooks/use-discriminated-union-key-values.ts new file mode 100644 index 0000000..c599323 --- /dev/null +++ b/packages/react-hook-form/src/hooks/use-discriminated-union-key-values.ts @@ -0,0 +1,18 @@ +import { type DiscriminatedUnionSchema, findSchemaByPath, SchemaType } from "dynz"; +import { useMemo } from "react"; +import { useDynzFormContext } from "./use-dynz-form-context"; + +export function useDiscriminatedUnionKeyValues(name: string) { + const { schema } = useDynzFormContext(); + + const unionSchema = findSchemaByPath(`$.${name}`, schema, SchemaType.DISCRIMINATED_UNION); + + return useMemo(() => { + const key = unionSchema.key; + + return unionSchema.schemas.map((member) => ({ + enabled: true, + value: member[key] as string | number | boolean, + })); + }, [unionSchema]); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fd3e51..ae00722 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,7 +116,7 @@ importers: dependencies: '@hookform/resolvers': specifier: ^5.2.1 - version: 5.2.2(react-hook-form@7.63.0(react@19.2.0)) + version: 5.2.2(react-hook-form@7.80.0(react@19.2.0)) '@radix-ui/react-label': specifier: ^2.1.7 version: 2.1.7(@types/react-dom@19.1.9(@types/react@19.1.15))(@types/react@19.1.15)(react-dom@19.1.1(react@19.2.0))(react@19.2.0) @@ -142,8 +142,8 @@ importers: specifier: ^4.3.4 version: 4.3.9(next@15.3.5(react-dom@19.1.1(react@19.2.0))(react@19.2.0))(react@19.2.0)(typescript@5.9.2) react-hook-form: - specifier: ^7.62.0 - version: 7.63.0(react@19.2.0) + specifier: ^7.75.0 + version: 7.80.0(react@19.2.0) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -3868,6 +3868,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-hook-form@7.80.0: + resolution: {integrity: sha512-4P+fk6oXsxY+6xSj7Euhc2sumQD8zQqCuVHoJwoyp9EchP+IUW9OESB7uHFJOKsIBQ4MQqYE84INJFqUCYNoOg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -5117,6 +5123,11 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.63.0(react@19.2.0) + '@hookform/resolvers@5.2.2(react-hook-form@7.80.0(react@19.2.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.80.0(react@19.2.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -8920,6 +8931,10 @@ snapshots: dependencies: react: 19.2.0 + react-hook-form@7.80.0(react@19.2.0): + dependencies: + react: 19.2.0 + react-is@16.13.1: {} react-medium-image-zoom@5.4.0(react-dom@19.1.1(react@19.2.0))(react@19.2.0):