@zod-utils/react-hook-form 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,6 +63,9 @@ npm install @zod-utils/react-hook-form zod react react-hook-form @hookform/resol
63
63
  ## Features
64
64
 
65
65
  - 🎣 **useZodForm** - Automatic type transformation for form inputs (nullable/undefined) while preserving Zod schema validation
66
+ - 📋 **FormSchemaProvider** - React Context for providing schema to form components
67
+ - ✅ **useIsRequiredField** - Hook to check if a field requires valid input
68
+ - 🔄 **Discriminated Union Support** - Full type-safe support for discriminated unions
66
69
  - 📦 **All core utilities** - Re-exports everything from `@zod-utils/core`
67
70
  - ⚛️ **React-optimized** - Built specifically for React applications
68
71
 
@@ -136,6 +139,7 @@ const form = useZodForm({
136
139
  - **Output validation**: Validated data matches your Zod schema exactly
137
140
  - **Type inference**: No manual type annotations needed - everything is inferred from the schema
138
141
  - **Zod integration**: Automatically sets up `zodResolver` for validation
142
+ - **Transform support**: Works with schemas that use `.transform()` - uses input types for form fields
139
143
 
140
144
  #### Using Without Default Values
141
145
 
@@ -272,6 +276,116 @@ const form2 = useZodForm<
272
276
 
273
277
  ---
274
278
 
279
+ ## Form Schema Context
280
+
281
+ The Form Schema Context system allows you to provide Zod schema context to deeply nested form components without prop drilling.
282
+
283
+ ### `FormSchemaProvider`
284
+
285
+ Provides schema context to all child components. Use this to wrap your form.
286
+
287
+ ```tsx
288
+ import { FormSchemaProvider } from "@zod-utils/react-hook-form";
289
+ import { z } from "zod";
290
+
291
+ const schema = z.object({
292
+ username: z.string().min(3).max(20),
293
+ email: z.string().email(),
294
+ });
295
+
296
+ function MyForm() {
297
+ return (
298
+ <FormSchemaProvider schema={schema}>
299
+ <form>
300
+ <UsernameField />
301
+ <EmailField />
302
+ </form>
303
+ </FormSchemaProvider>
304
+ );
305
+ }
306
+ ```
307
+
308
+ #### With Discriminated Union
309
+
310
+ For discriminated unions, pass the discriminator to enable type-safe field access:
311
+
312
+ ```tsx
313
+ const schema = z.discriminatedUnion("mode", [
314
+ z.object({ mode: z.literal("create"), name: z.string().min(1) }),
315
+ z.object({ mode: z.literal("edit"), id: z.number() }),
316
+ ]);
317
+
318
+ function CreateModeForm() {
319
+ return (
320
+ <FormSchemaProvider
321
+ schema={schema}
322
+ discriminator={{ key: "mode", value: "create" }}
323
+ >
324
+ <NameField /> {/* Only fields from 'create' variant are available */}
325
+ </FormSchemaProvider>
326
+ );
327
+ }
328
+ ```
329
+
330
+ ### `useFormSchema()`
331
+
332
+ Access the schema context from child components:
333
+
334
+ ```tsx
335
+ import { useFormSchema } from "@zod-utils/react-hook-form";
336
+
337
+ function FieldComponent() {
338
+ const context = useFormSchema();
339
+ if (!context) return null;
340
+
341
+ const { schema, discriminator } = context;
342
+ // Use schema for field-level logic
343
+ }
344
+ ```
345
+
346
+ ### `useIsRequiredField({ schema, fieldName, discriminator? })`
347
+
348
+ Hook to check if a field requires valid input (shows validation errors on submit).
349
+ The schema parameter is used for type inference only - the actual schema is retrieved from context.
350
+
351
+ ```tsx
352
+ import { useIsRequiredField } from "@zod-utils/react-hook-form";
353
+
354
+ function FormLabel({ name, schema }: { name: string; schema: z.ZodType }) {
355
+ const isRequired = useIsRequiredField({ schema, fieldName: name });
356
+
357
+ return (
358
+ <label>
359
+ {name}
360
+ {isRequired && <span className="text-red-500">*</span>}
361
+ </label>
362
+ );
363
+ }
364
+ ```
365
+
366
+ ### `isRequiredField({ schema, fieldName, discriminator? })`
367
+
368
+ Standalone function to check if a field requires valid input:
369
+
370
+ ```tsx
371
+ import { isRequiredField } from "@zod-utils/react-hook-form";
372
+ import { z } from "zod";
373
+
374
+ const schema = z.object({
375
+ username: z.string().min(1), // Required - min(1) rejects empty
376
+ email: z.string(), // Not required - accepts empty string
377
+ age: z.number(), // Required - numbers reject empty input
378
+ bio: z.string().optional(), // Not required - optional
379
+ });
380
+
381
+ isRequiredField({ schema, fieldName: "username" }); // true
382
+ isRequiredField({ schema, fieldName: "email" }); // false
383
+ isRequiredField({ schema, fieldName: "age" }); // true
384
+ isRequiredField({ schema, fieldName: "bio" }); // false
385
+ ```
386
+
387
+ ---
388
+
275
389
  ## Core Utilities (Re-exported)
276
390
 
277
391
  All utilities from `@zod-utils/core` are re-exported for convenience:
@@ -283,12 +397,25 @@ import {
283
397
  requiresValidInput,
284
398
  getPrimitiveType,
285
399
  removeDefault,
286
- extractDefault,
400
+ extractDefaultValue,
287
401
  type Simplify,
402
+ type ZodUnionCheck,
403
+
404
+ // Form schema context
405
+ FormSchemaContext,
406
+ FormSchemaProvider,
407
+ useFormSchema,
408
+ useIsRequiredField,
409
+ isRequiredField,
288
410
 
289
- // Type utilities (react-hook-form specific)
411
+ // Type utilities
290
412
  type PartialWithNullableObjects,
291
413
  type PartialWithAllNullables,
414
+ type Discriminator,
415
+ type DiscriminatorKey,
416
+ type DiscriminatorValue,
417
+ type InferredFieldValues,
418
+ type ValidFieldName,
292
419
  } from "@zod-utils/react-hook-form";
293
420
  ```
294
421
 
package/dist/index.d.mts CHANGED
@@ -1,9 +1,185 @@
1
- import { Simplify } from '@zod-utils/core';
1
+ import { DiscriminatorKey, DiscriminatorValue, Discriminator, Simplify } from '@zod-utils/core';
2
2
  export * from '@zod-utils/core';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { Context, ReactNode } from 'react';
5
+ import { z } from 'zod';
3
6
  import * as react_hook_form from 'react-hook-form';
4
- import { FieldValues, UseFormProps } from 'react-hook-form';
7
+ import { FieldValues, Path, UseFormProps } from 'react-hook-form';
5
8
  import { zodResolver } from '@hookform/resolvers/zod';
6
- import { z } from 'zod';
9
+
10
+ /**
11
+ * Type for the FormSchemaContext with full generic support.
12
+ * @internal
13
+ */
14
+ type FormSchemaContextType<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>> = Context<{
15
+ schema: TSchema;
16
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
17
+ } | null>;
18
+ /**
19
+ * Context value type for FormSchemaContext.
20
+ */
21
+ type FormSchemaContextValue<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>> = {
22
+ schema: TSchema;
23
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
24
+ } | null;
25
+ /**
26
+ * React Context for providing Zod schema to form components.
27
+ *
28
+ * Use with {@link FormSchemaProvider} to provide schema context, and
29
+ * {@link useFormSchema} to consume it in child components.
30
+ */
31
+ declare const FormSchemaContext: Context<{
32
+ schema: z.ZodType;
33
+ discriminator?: {
34
+ key: unknown;
35
+ value: unknown;
36
+ };
37
+ } | null>;
38
+ /**
39
+ * Hook to access the form schema from context.
40
+ *
41
+ * The optional `_params` argument is used for TypeScript type inference only.
42
+ * Pass your schema to get proper type narrowing of the context value.
43
+ *
44
+ * @param _params - Optional params for type inference (not used at runtime)
45
+ * @returns The schema context value or null if not within a provider
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * // Without type params (returns generic context)
50
+ * function MyFormField() {
51
+ * const context = useFormSchema();
52
+ * if (!context) return null;
53
+ *
54
+ * const { schema, discriminator } = context;
55
+ * // Use schema for validation or field extraction
56
+ * }
57
+ *
58
+ * // With type params (for type-safe schema access)
59
+ * function TypedFormField() {
60
+ * const context = useFormSchema({ schema: mySchema });
61
+ * // context.schema is now typed as typeof mySchema
62
+ * }
63
+ * ```
64
+ */
65
+ declare function useFormSchema<TSchema extends z.ZodType = z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema> = DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey> = DiscriminatorValue<TSchema, TDiscriminatorKey>>(_params?: {
66
+ schema: TSchema;
67
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
68
+ }): FormSchemaContextValue<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
69
+ /**
70
+ * Provider component that makes Zod schema available to all child components.
71
+ *
72
+ * Use this to wrap your form and provide schema context to nested components
73
+ * like field labels and validation indicators.
74
+ *
75
+ * @example
76
+ * Basic usage with ZodObject
77
+ * ```tsx
78
+ * const schema = z.object({
79
+ * name: z.string(),
80
+ * email: z.string().email().optional()
81
+ * });
82
+ *
83
+ * <FormSchemaProvider schema={schema}>
84
+ * <YourFormComponents />
85
+ * </FormSchemaProvider>
86
+ * ```
87
+ *
88
+ * @example
89
+ * Usage with discriminated union
90
+ * ```tsx
91
+ * const schema = z.discriminatedUnion('mode', [
92
+ * z.object({ mode: z.literal('create'), name: z.string() }),
93
+ * z.object({ mode: z.literal('edit'), id: z.number() })
94
+ * ]);
95
+ *
96
+ * <FormSchemaProvider
97
+ * schema={schema}
98
+ * discriminator={{ key: 'mode', value: 'create' }}
99
+ * >
100
+ * <YourFormComponents />
101
+ * </FormSchemaProvider>
102
+ * ```
103
+ */
104
+ declare function FormSchemaProvider<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ schema, discriminator, children, }: {
105
+ schema: TSchema;
106
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
107
+ children: ReactNode;
108
+ }): react_jsx_runtime.JSX.Element;
109
+ /**
110
+ * Hook to check if a field requires valid input based on the Zod schema.
111
+ *
112
+ * Uses the schema from {@link FormSchemaContext} to determine if a field
113
+ * will show validation errors when submitted with empty/invalid input.
114
+ *
115
+ * @param params - Schema, field name, and optional discriminator (schema used for type inference)
116
+ * @returns true if the field requires valid input, false otherwise
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * function MyFieldLabel({ name, schema }: { name: string; schema: z.ZodType }) {
121
+ * const isRequired = useIsRequiredField({ schema, fieldName: name });
122
+ *
123
+ * return (
124
+ * <label>
125
+ * {name}
126
+ * {isRequired && <span className="text-red-500">*</span>}
127
+ * </label>
128
+ * );
129
+ * }
130
+ * ```
131
+ */
132
+ declare function useIsRequiredField<TSchema extends z.ZodType, TName extends keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>>, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ fieldName, ...props }: {
133
+ schema: TSchema;
134
+ fieldName: TName;
135
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
136
+ }): boolean;
137
+ /**
138
+ * Determines if a field requires valid input (will show validation errors on empty/invalid input).
139
+ *
140
+ * Uses `requiresValidInput` from `@zod-utils/core` which checks the underlying field after
141
+ * removing defaults. This tells you if the field will error when user submits empty input.
142
+ *
143
+ * Returns false if the underlying field accepts:
144
+ * - `undefined` (via `.optional()`)
145
+ * - `null` (via `.nullable()`)
146
+ * - Empty strings (plain `z.string()` without `.min(1)`)
147
+ * - Empty arrays (plain `z.array()` without `.min(1)`)
148
+ *
149
+ * @param options - Schema, field name, and optional discriminator
150
+ * @returns true if the field requires valid input, false otherwise
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const schema = z.object({
155
+ * name: z.string().min(1),
156
+ * bio: z.string().optional(),
157
+ * });
158
+ *
159
+ * isRequiredField({ schema, fieldName: 'name' }); // true
160
+ * isRequiredField({ schema, fieldName: 'bio' }); // false
161
+ * ```
162
+ *
163
+ * @example
164
+ * With discriminated union
165
+ * ```typescript
166
+ * const schema = z.discriminatedUnion('mode', [
167
+ * z.object({ mode: z.literal('create'), name: z.string().min(1) }),
168
+ * z.object({ mode: z.literal('edit'), id: z.number() }),
169
+ * ]);
170
+ *
171
+ * isRequiredField({
172
+ * schema,
173
+ * fieldName: 'name',
174
+ * discriminator: { key: 'mode', value: 'create' },
175
+ * }); // true
176
+ * ```
177
+ */
178
+ declare function isRequiredField<TSchema extends z.ZodType, TName extends keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>>, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ schema, fieldName, discriminator, }: {
179
+ schema: TSchema;
180
+ fieldName: TName;
181
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
182
+ }): boolean;
7
183
 
8
184
  /**
9
185
  * Helper type that adds `null` to object-type fields only (excludes arrays).
@@ -52,6 +228,38 @@ type PartialWithNullableObjects<T> = Simplify<Partial<AddNullToObjects<T>>>;
52
228
  type PartialWithAllNullables<T> = {
53
229
  [K in keyof T]?: T[K] | null;
54
230
  };
231
+ /**
232
+ * Infers field values from a Zod schema compatible with React Hook Form's FieldValues.
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * const schema = z.object({ name: z.string() });
237
+ * type Values = InferredFieldValues<typeof schema>;
238
+ * // { name: string } & FieldValues
239
+ * ```
240
+ */
241
+ type InferredFieldValues<TSchema extends z.ZodType> = z.input<TSchema> & FieldValues;
242
+ /**
243
+ * Type-safe field names for a specific discriminator value.
244
+ *
245
+ * Narrows field names to only those that exist for the given discriminator value
246
+ * in a discriminated union schema.
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const schema = z.discriminatedUnion('mode', [
251
+ * z.object({ mode: z.literal('create'), name: z.string() }),
252
+ * z.object({ mode: z.literal('edit'), id: z.number() }),
253
+ * ]);
254
+ *
255
+ * type CreateFields = ValidFieldName<typeof schema, 'mode', 'create'>;
256
+ * // "mode" | "name"
257
+ *
258
+ * type EditFields = ValidFieldName<typeof schema, 'mode', 'edit'>;
259
+ * // "mode" | "id"
260
+ * ```
261
+ */
262
+ type ValidFieldName<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>, TFieldValues extends InferredFieldValues<TSchema> = InferredFieldValues<TSchema>> = keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>> & Path<TFieldValues>;
55
263
 
56
264
  /**
57
265
  * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.
@@ -217,10 +425,10 @@ type PartialWithAllNullables<T> = {
217
425
  * @see https://zod.dev for Zod schema documentation
218
426
  * @since 0.1.0
219
427
  */
220
- declare const useZodForm: <TOutput extends FieldValues, TFormInput extends PartialWithAllNullables<TOutput> = PartialWithNullableObjects<TOutput>, TInput extends TFormInput = TFormInput, TDefault extends TFormInput = TFormInput>({ schema, zodResolverOptions, ...formOptions }: {
428
+ declare const useZodForm: <TInput extends FieldValues, TOutput extends FieldValues, TFormInput extends PartialWithAllNullables<TInput> = PartialWithNullableObjects<TInput>>({ schema, zodResolverOptions, ...formOptions }: {
221
429
  schema: z.ZodType<TOutput, TInput>;
222
- defaultValues?: TDefault;
430
+ defaultValues?: TFormInput;
223
431
  zodResolverOptions?: Parameters<typeof zodResolver>[1];
224
432
  } & Omit<UseFormProps<TFormInput, unknown, TOutput>, "resolver" | "defaultValues">) => react_hook_form.UseFormReturn<TFormInput, unknown, TOutput>;
225
433
 
226
- export { type PartialWithAllNullables, type PartialWithNullableObjects, useZodForm };
434
+ export { FormSchemaContext, type FormSchemaContextType, type FormSchemaContextValue, FormSchemaProvider, type InferredFieldValues, type PartialWithAllNullables, type PartialWithNullableObjects, type ValidFieldName, isRequiredField, useFormSchema, useIsRequiredField, useZodForm };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,185 @@
1
- import { Simplify } from '@zod-utils/core';
1
+ import { DiscriminatorKey, DiscriminatorValue, Discriminator, Simplify } from '@zod-utils/core';
2
2
  export * from '@zod-utils/core';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { Context, ReactNode } from 'react';
5
+ import { z } from 'zod';
3
6
  import * as react_hook_form from 'react-hook-form';
4
- import { FieldValues, UseFormProps } from 'react-hook-form';
7
+ import { FieldValues, Path, UseFormProps } from 'react-hook-form';
5
8
  import { zodResolver } from '@hookform/resolvers/zod';
6
- import { z } from 'zod';
9
+
10
+ /**
11
+ * Type for the FormSchemaContext with full generic support.
12
+ * @internal
13
+ */
14
+ type FormSchemaContextType<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>> = Context<{
15
+ schema: TSchema;
16
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
17
+ } | null>;
18
+ /**
19
+ * Context value type for FormSchemaContext.
20
+ */
21
+ type FormSchemaContextValue<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>> = {
22
+ schema: TSchema;
23
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
24
+ } | null;
25
+ /**
26
+ * React Context for providing Zod schema to form components.
27
+ *
28
+ * Use with {@link FormSchemaProvider} to provide schema context, and
29
+ * {@link useFormSchema} to consume it in child components.
30
+ */
31
+ declare const FormSchemaContext: Context<{
32
+ schema: z.ZodType;
33
+ discriminator?: {
34
+ key: unknown;
35
+ value: unknown;
36
+ };
37
+ } | null>;
38
+ /**
39
+ * Hook to access the form schema from context.
40
+ *
41
+ * The optional `_params` argument is used for TypeScript type inference only.
42
+ * Pass your schema to get proper type narrowing of the context value.
43
+ *
44
+ * @param _params - Optional params for type inference (not used at runtime)
45
+ * @returns The schema context value or null if not within a provider
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * // Without type params (returns generic context)
50
+ * function MyFormField() {
51
+ * const context = useFormSchema();
52
+ * if (!context) return null;
53
+ *
54
+ * const { schema, discriminator } = context;
55
+ * // Use schema for validation or field extraction
56
+ * }
57
+ *
58
+ * // With type params (for type-safe schema access)
59
+ * function TypedFormField() {
60
+ * const context = useFormSchema({ schema: mySchema });
61
+ * // context.schema is now typed as typeof mySchema
62
+ * }
63
+ * ```
64
+ */
65
+ declare function useFormSchema<TSchema extends z.ZodType = z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema> = DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey> = DiscriminatorValue<TSchema, TDiscriminatorKey>>(_params?: {
66
+ schema: TSchema;
67
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
68
+ }): FormSchemaContextValue<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
69
+ /**
70
+ * Provider component that makes Zod schema available to all child components.
71
+ *
72
+ * Use this to wrap your form and provide schema context to nested components
73
+ * like field labels and validation indicators.
74
+ *
75
+ * @example
76
+ * Basic usage with ZodObject
77
+ * ```tsx
78
+ * const schema = z.object({
79
+ * name: z.string(),
80
+ * email: z.string().email().optional()
81
+ * });
82
+ *
83
+ * <FormSchemaProvider schema={schema}>
84
+ * <YourFormComponents />
85
+ * </FormSchemaProvider>
86
+ * ```
87
+ *
88
+ * @example
89
+ * Usage with discriminated union
90
+ * ```tsx
91
+ * const schema = z.discriminatedUnion('mode', [
92
+ * z.object({ mode: z.literal('create'), name: z.string() }),
93
+ * z.object({ mode: z.literal('edit'), id: z.number() })
94
+ * ]);
95
+ *
96
+ * <FormSchemaProvider
97
+ * schema={schema}
98
+ * discriminator={{ key: 'mode', value: 'create' }}
99
+ * >
100
+ * <YourFormComponents />
101
+ * </FormSchemaProvider>
102
+ * ```
103
+ */
104
+ declare function FormSchemaProvider<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ schema, discriminator, children, }: {
105
+ schema: TSchema;
106
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
107
+ children: ReactNode;
108
+ }): react_jsx_runtime.JSX.Element;
109
+ /**
110
+ * Hook to check if a field requires valid input based on the Zod schema.
111
+ *
112
+ * Uses the schema from {@link FormSchemaContext} to determine if a field
113
+ * will show validation errors when submitted with empty/invalid input.
114
+ *
115
+ * @param params - Schema, field name, and optional discriminator (schema used for type inference)
116
+ * @returns true if the field requires valid input, false otherwise
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * function MyFieldLabel({ name, schema }: { name: string; schema: z.ZodType }) {
121
+ * const isRequired = useIsRequiredField({ schema, fieldName: name });
122
+ *
123
+ * return (
124
+ * <label>
125
+ * {name}
126
+ * {isRequired && <span className="text-red-500">*</span>}
127
+ * </label>
128
+ * );
129
+ * }
130
+ * ```
131
+ */
132
+ declare function useIsRequiredField<TSchema extends z.ZodType, TName extends keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>>, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ fieldName, ...props }: {
133
+ schema: TSchema;
134
+ fieldName: TName;
135
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
136
+ }): boolean;
137
+ /**
138
+ * Determines if a field requires valid input (will show validation errors on empty/invalid input).
139
+ *
140
+ * Uses `requiresValidInput` from `@zod-utils/core` which checks the underlying field after
141
+ * removing defaults. This tells you if the field will error when user submits empty input.
142
+ *
143
+ * Returns false if the underlying field accepts:
144
+ * - `undefined` (via `.optional()`)
145
+ * - `null` (via `.nullable()`)
146
+ * - Empty strings (plain `z.string()` without `.min(1)`)
147
+ * - Empty arrays (plain `z.array()` without `.min(1)`)
148
+ *
149
+ * @param options - Schema, field name, and optional discriminator
150
+ * @returns true if the field requires valid input, false otherwise
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const schema = z.object({
155
+ * name: z.string().min(1),
156
+ * bio: z.string().optional(),
157
+ * });
158
+ *
159
+ * isRequiredField({ schema, fieldName: 'name' }); // true
160
+ * isRequiredField({ schema, fieldName: 'bio' }); // false
161
+ * ```
162
+ *
163
+ * @example
164
+ * With discriminated union
165
+ * ```typescript
166
+ * const schema = z.discriminatedUnion('mode', [
167
+ * z.object({ mode: z.literal('create'), name: z.string().min(1) }),
168
+ * z.object({ mode: z.literal('edit'), id: z.number() }),
169
+ * ]);
170
+ *
171
+ * isRequiredField({
172
+ * schema,
173
+ * fieldName: 'name',
174
+ * discriminator: { key: 'mode', value: 'create' },
175
+ * }); // true
176
+ * ```
177
+ */
178
+ declare function isRequiredField<TSchema extends z.ZodType, TName extends keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>>, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ schema, fieldName, discriminator, }: {
179
+ schema: TSchema;
180
+ fieldName: TName;
181
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
182
+ }): boolean;
7
183
 
8
184
  /**
9
185
  * Helper type that adds `null` to object-type fields only (excludes arrays).
@@ -52,6 +228,38 @@ type PartialWithNullableObjects<T> = Simplify<Partial<AddNullToObjects<T>>>;
52
228
  type PartialWithAllNullables<T> = {
53
229
  [K in keyof T]?: T[K] | null;
54
230
  };
231
+ /**
232
+ * Infers field values from a Zod schema compatible with React Hook Form's FieldValues.
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * const schema = z.object({ name: z.string() });
237
+ * type Values = InferredFieldValues<typeof schema>;
238
+ * // { name: string } & FieldValues
239
+ * ```
240
+ */
241
+ type InferredFieldValues<TSchema extends z.ZodType> = z.input<TSchema> & FieldValues;
242
+ /**
243
+ * Type-safe field names for a specific discriminator value.
244
+ *
245
+ * Narrows field names to only those that exist for the given discriminator value
246
+ * in a discriminated union schema.
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const schema = z.discriminatedUnion('mode', [
251
+ * z.object({ mode: z.literal('create'), name: z.string() }),
252
+ * z.object({ mode: z.literal('edit'), id: z.number() }),
253
+ * ]);
254
+ *
255
+ * type CreateFields = ValidFieldName<typeof schema, 'mode', 'create'>;
256
+ * // "mode" | "name"
257
+ *
258
+ * type EditFields = ValidFieldName<typeof schema, 'mode', 'edit'>;
259
+ * // "mode" | "id"
260
+ * ```
261
+ */
262
+ type ValidFieldName<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>, TFieldValues extends InferredFieldValues<TSchema> = InferredFieldValues<TSchema>> = keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>> & Path<TFieldValues>;
55
263
 
56
264
  /**
57
265
  * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.
@@ -217,10 +425,10 @@ type PartialWithAllNullables<T> = {
217
425
  * @see https://zod.dev for Zod schema documentation
218
426
  * @since 0.1.0
219
427
  */
220
- declare const useZodForm: <TOutput extends FieldValues, TFormInput extends PartialWithAllNullables<TOutput> = PartialWithNullableObjects<TOutput>, TInput extends TFormInput = TFormInput, TDefault extends TFormInput = TFormInput>({ schema, zodResolverOptions, ...formOptions }: {
428
+ declare const useZodForm: <TInput extends FieldValues, TOutput extends FieldValues, TFormInput extends PartialWithAllNullables<TInput> = PartialWithNullableObjects<TInput>>({ schema, zodResolverOptions, ...formOptions }: {
221
429
  schema: z.ZodType<TOutput, TInput>;
222
- defaultValues?: TDefault;
430
+ defaultValues?: TFormInput;
223
431
  zodResolverOptions?: Parameters<typeof zodResolver>[1];
224
432
  } & Omit<UseFormProps<TFormInput, unknown, TOutput>, "resolver" | "defaultValues">) => react_hook_form.UseFormReturn<TFormInput, unknown, TOutput>;
225
433
 
226
- export { type PartialWithAllNullables, type PartialWithNullableObjects, useZodForm };
434
+ export { FormSchemaContext, type FormSchemaContextType, type FormSchemaContextValue, FormSchemaProvider, type InferredFieldValues, type PartialWithAllNullables, type PartialWithNullableObjects, type ValidFieldName, isRequiredField, useFormSchema, useIsRequiredField, useZodForm };
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@zod-utils/core');
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
4
6
  var zod = require('@hookform/resolvers/zod');
5
7
  var reactHookForm = require('react-hook-form');
6
8
 
7
9
  var __defProp = Object.defineProperty;
8
- var __defProps = Object.defineProperties;
9
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
10
10
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
11
11
  var __hasOwnProp = Object.prototype.hasOwnProperty;
12
12
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -22,7 +22,6 @@ var __spreadValues = (a, b) => {
22
22
  }
23
23
  return a;
24
24
  };
25
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
26
25
  var __objRest = (source, exclude) => {
27
26
  var target = {};
28
27
  for (var prop in source)
@@ -35,6 +34,51 @@ var __objRest = (source, exclude) => {
35
34
  }
36
35
  return target;
37
36
  };
37
+ var FormSchemaContext = react.createContext(null);
38
+ function useFormSchema(_params) {
39
+ return react.useContext(
40
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
41
+ FormSchemaContext
42
+ );
43
+ }
44
+ function FormSchemaProvider({
45
+ schema,
46
+ discriminator,
47
+ children
48
+ }) {
49
+ return /* @__PURE__ */ jsxRuntime.jsx(FormSchemaContext.Provider, { value: { schema, discriminator }, children });
50
+ }
51
+ function useIsRequiredField(_a) {
52
+ var _b = _a, {
53
+ fieldName
54
+ } = _b; __objRest(_b, [
55
+ "fieldName"
56
+ ]);
57
+ const context = useFormSchema();
58
+ if (!context) {
59
+ return false;
60
+ }
61
+ return isRequiredField({
62
+ schema: context.schema,
63
+ fieldName,
64
+ discriminator: context.discriminator
65
+ });
66
+ }
67
+ function isRequiredField({
68
+ schema,
69
+ fieldName,
70
+ discriminator
71
+ }) {
72
+ const field = core.extractFieldFromSchema({
73
+ schema,
74
+ fieldName,
75
+ discriminator
76
+ });
77
+ if (!field) {
78
+ return false;
79
+ }
80
+ return core.requiresValidInput(field);
81
+ }
38
82
  var useZodForm = (_a) => {
39
83
  var _b = _a, {
40
84
  schema,
@@ -43,18 +87,17 @@ var useZodForm = (_a) => {
43
87
  "schema",
44
88
  "zodResolverOptions"
45
89
  ]);
46
- const resolver = zod.zodResolver(
47
- schema,
48
- zodResolverOptions
49
- );
50
- const defaultValues = formOptions.defaultValues;
51
- return reactHookForm.useForm(__spreadProps(__spreadValues({
90
+ const resolver = zod.zodResolver(schema, zodResolverOptions);
91
+ return reactHookForm.useForm(__spreadValues({
52
92
  resolver
53
- }, formOptions), {
54
- defaultValues
55
- }));
93
+ }, formOptions));
56
94
  };
57
95
 
96
+ exports.FormSchemaContext = FormSchemaContext;
97
+ exports.FormSchemaProvider = FormSchemaProvider;
98
+ exports.isRequiredField = isRequiredField;
99
+ exports.useFormSchema = useFormSchema;
100
+ exports.useIsRequiredField = useIsRequiredField;
58
101
  exports.useZodForm = useZodForm;
59
102
  Object.keys(core).forEach(function (k) {
60
103
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/use-zod-form.ts"],"names":["zodResolver","useForm"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiLO,IAAM,UAAA,GAAa,CAMxB,EAAA,KAWI;AAXJ,EAAA,IAAA,EAAA,GAAA,EAAA,EACA;AAAA,IAAA,MAAA;AAAA,IACA;AAAA,GAzLF,GAuLE,EAAA,EAGG,WAAA,GAAA,SAAA,CAHH,EAAA,EAGG;AAAA,IAFH,QAAA;AAAA,IACA;AAAA,GAAA,CAAA;AAUA,EAAA,MAAM,QAAA,GAAWA,eAAA;AAAA,IACf,MAAA;AAAA,IACA;AAAA,GACF;AAIA,EAAA,MAAM,gBAAgB,WAAA,CAAY,aAAA;AAElC,EAAA,OAAOC,qBAAA,CAAQ,aAAA,CAAA,cAAA,CAAA;AAAA,IACb;AAAA,GAAA,EACG,WAAA,CAAA,EAFU;AAAA,IAGb;AAAA,GACF,CAAC,CAAA;AACH","file":"index.js","sourcesContent":["import { zodResolver } from '@hookform/resolvers/zod';\nimport {\n type DefaultValues,\n type FieldValues,\n type UseFormProps,\n useForm,\n} from 'react-hook-form';\nimport type { z } from 'zod';\nimport type {\n PartialWithAllNullables,\n PartialWithNullableObjects,\n} from './types';\n\n/**\n * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.\n *\n * This hook eliminates the TypeScript friction between React Hook Form's nullable field values\n * and Zod's strict output types. It uses a two-type schema pattern where:\n * - **Input type** (`PartialWithNullableObjects<TOutput>`): Form fields accept `null | undefined` during editing\n * - **Output type** (`TOutput`): Validated data matches exact schema type (no `null | undefined`)\n *\n * **Key Benefits:**\n * - ✅ No more \"Type 'null' is not assignable to...\" TypeScript errors\n * - ✅ Use `form.setValue()` and `form.reset()` with `null` values freely\n * - ✅ Validated output is still type-safe with exact Zod schema types\n * - ✅ Automatic zodResolver setup - no manual configuration needed\n *\n * @template TOutput - The Zod schema output type (extends FieldValues)\n * @template TInput - The Zod schema input type (accepts nullable/undefined values during form editing)\n *\n * @param options - Configuration object\n * @param options.schema - Zod schema with two-type signature `z.ZodType<TOutput, TInput>`\n * @param options.defaultValues - Default form values (shallow partial - nested objects must be complete if provided)\n * @param options.zodResolverOptions - Optional zodResolver configuration\n * @param options....formOptions - All other react-hook-form useForm options\n *\n * @returns React Hook Form instance with type-safe methods\n *\n * @example\n * Basic usage with required fields\n * ```typescript\n * import { useZodForm } from '@zod-utils/react-hook-form';\n * import { z } from 'zod';\n *\n * const schema = z.object({\n * name: z.string().min(1), // Required field\n * age: z.number().min(0),\n * }) satisfies z.ZodType<{ name: string; age: number }, any>;\n *\n * function MyForm() {\n * const form = useZodForm({ schema });\n *\n * // ✅ These work without type errors:\n * form.setValue('name', null); // Accepts null during editing\n * form.reset({ name: null, age: null }); // Reset with null\n *\n * const onSubmit = (data: { name: string; age: number }) => {\n * // ✅ data is exact type - no null | undefined\n * console.log(data.name.toUpperCase()); // Safe to use string methods\n * };\n *\n * return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;\n * }\n * ```\n *\n * @example\n * With default values\n * ```typescript\n * const schema = z.object({\n * username: z.string(),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * }) satisfies z.ZodType<{\n * username: string;\n * email: string;\n * notifications: boolean;\n * }, any>;\n *\n * const form = useZodForm({\n * schema,\n * defaultValues: {\n * username: '',\n * email: '',\n * // notifications gets default from schema\n * },\n * });\n * ```\n *\n * @example\n * Without default values (all fields are optional during editing)\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * email: z.string().email(),\n * age: z.number(),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * // ✅ No defaultValues needed - fields are optional during editing\n * const form = useZodForm({ schema });\n *\n * // Form fields can be set individually as user types\n * form.setValue('name', 'John');\n * form.setValue('email', 'john@example.com');\n * form.setValue('age', 25);\n *\n * // All fields must be valid on submit (per schema validation)\n * ```\n *\n * @example\n * With optional and nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string(),\n * description: z.string().optional(), // Optional in output\n * tags: z.array(z.string()).nullable(), // Nullable in output\n * }) satisfies z.ZodType<{\n * title: string;\n * description?: string;\n * tags: string[] | null;\n * }, any>;\n *\n * const form = useZodForm({ schema });\n *\n * // All fields accept null/undefined during editing\n * form.setValue('title', null);\n * form.setValue('description', undefined);\n * form.setValue('tags', null);\n * ```\n *\n * @example\n * With zodResolver options\n * ```typescript\n * const form = useZodForm({\n * schema,\n * zodResolverOptions: {\n * async: true, // Enable async validation\n * errorMap: customErrorMap, // Custom error messages\n * },\n * });\n * ```\n *\n * @example\n * Complete form example\n * ```typescript\n * const userSchema = z.object({\n * name: z.string().min(1, 'Name is required'),\n * email: z.string().email('Invalid email'),\n * age: z.number().min(18, 'Must be 18+'),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * function UserForm() {\n * const form = useZodForm({\n * schema: userSchema,\n * defaultValues: { name: '', email: '', age: null },\n * });\n *\n * const onSubmit = (data: { name: string; email: string; age: number }) => {\n * // Type-safe: data has exact types, no null/undefined\n * console.log(`${data.name} is ${data.age} years old`);\n * };\n *\n * return (\n * <form onSubmit={form.handleSubmit(onSubmit)}>\n * <input {...form.register('name')} />\n * <input {...form.register('email')} type=\"email\" />\n * <input {...form.register('age', { valueAsNumber: true })} type=\"number\" />\n * <button type=\"submit\">Submit</button>\n * </form>\n * );\n * }\n * ```\n *\n * @see {@link PartialWithNullableObjects} for the type transformation utility\n * @see https://react-hook-form.com/docs/useform for React Hook Form documentation\n * @see https://zod.dev for Zod schema documentation\n * @since 0.1.0\n */\nexport const useZodForm = <\n TOutput extends FieldValues,\n TFormInput extends\n PartialWithAllNullables<TOutput> = PartialWithNullableObjects<TOutput>,\n TInput extends TFormInput = TFormInput,\n TDefault extends TFormInput = TFormInput,\n>({\n schema,\n zodResolverOptions,\n ...formOptions\n}: {\n schema: z.ZodType<TOutput, TInput>;\n defaultValues?: TDefault;\n zodResolverOptions?: Parameters<typeof zodResolver>[1];\n} & Omit<\n UseFormProps<TFormInput, unknown, TOutput>,\n 'resolver' | 'defaultValues'\n>) => {\n const resolver = zodResolver<TFormInput, unknown, TOutput>(\n schema,\n zodResolverOptions,\n );\n\n // can't help but to assert here - because DefaultValues<TDefault> makes object fields DeepPartial\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const defaultValues = formOptions.defaultValues as DefaultValues<TDefault>;\n\n return useForm({\n resolver,\n ...formOptions,\n defaultValues,\n });\n};\n"]}
1
+ {"version":3,"sources":["../src/context.tsx","../src/use-zod-form.ts"],"names":["createContext","useContext","jsx","extractFieldFromSchema","requiresValidInput","zodResolver","useForm"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDO,IAAM,iBAAA,GAAoBA,oBAMvB,IAAI;AA6BP,SAAS,cAUd,OAAA,EAQyE;AACzE,EAAA,OAAOC,gBAAA;AAAA;AAAA,IAEL;AAAA,GAQF;AACF;AAqCO,SAAS,kBAAA,CAId;AAAA,EACA,MAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAA,EAQG;AACD,EAAA,uBACEC,cAAA,CAAC,kBAAkB,QAAA,EAAlB,EAA2B,OAAO,EAAE,MAAA,EAAQ,aAAA,EAAc,EACxD,QAAA,EACH,CAAA;AAEJ;AAyBO,SAAS,mBAQd,EAAA,EAWU;AAXV,EAAA,IAAA,EAAA,GAAA,EAAA,CAAA,CACA;AAAA,IAAA;AAAA,GAjNF,GAgNE,EAAA,CAAA,CAEG,SAAA,CAFH,EAAA,EAEG;AAAA,IADH;AAAA,GAAA;AAWA,EAAA,MAAM,OAAA,GAAU,cAIT,CAAA;AAEP,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,eAAA,CAAgB;AAAA,IACrB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,SAAA;AAAA,IACA,eAAe,OAAA,CAAQ;AAAA,GACxB,CAAA;AACH;AA2CO,SAAS,eAAA,CAQd;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAQY;AACV,EAAA,MAAM,QAAQC,2BAAA,CAAuB;AAAA,IACnC,MAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AAED,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAOC,wBAAmB,KAAK,CAAA;AACjC;AC1IO,IAAM,UAAA,GAAa,CAKxB,EAAA,KAWI;AAXJ,EAAA,IAAA,EAAA,GAAA,EAAA,EACA;AAAA,IAAA,MAAA;AAAA,IACA;AAAA,GAnLF,GAiLE,EAAA,EAGG,WAAA,GAAA,SAAA,CAHH,EAAA,EAGG;AAAA,IAFH,QAAA;AAAA,IACA;AAAA,GAAA,CAAA;AAUA,EAAA,MAAM,QAAA,GAAWC,eAAA,CAAY,MAAA,EAAQ,kBAAkB,CAAA;AAGvD,EAAA,OAAOC,qBAAA,CAAQ,cAAA,CAAA;AAAA,IACb;AAAA,GAAA,EACG,WAAA,CACqD,CAAA;AAC5D","file":"index.js","sourcesContent":["'use client';\n\nimport {\n type Discriminator,\n type DiscriminatorKey,\n type DiscriminatorValue,\n extractFieldFromSchema,\n requiresValidInput,\n} from '@zod-utils/core';\nimport { type Context, createContext, type ReactNode, useContext } from 'react';\nimport type { z } from 'zod';\n\n/**\n * Type for the FormSchemaContext with full generic support.\n * @internal\n */\nexport type FormSchemaContextType<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n> = Context<{\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n} | null>;\n\n/**\n * Context value type for FormSchemaContext.\n */\nexport type FormSchemaContextValue<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n> = {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n} | null;\n\n/**\n * React Context for providing Zod schema to form components.\n *\n * Use with {@link FormSchemaProvider} to provide schema context, and\n * {@link useFormSchema} to consume it in child components.\n */\nexport const FormSchemaContext = createContext<{\n schema: z.ZodType;\n discriminator?: {\n key: unknown;\n value: unknown;\n };\n} | null>(null);\n\n/**\n * Hook to access the form schema from context.\n *\n * The optional `_params` argument is used for TypeScript type inference only.\n * Pass your schema to get proper type narrowing of the context value.\n *\n * @param _params - Optional params for type inference (not used at runtime)\n * @returns The schema context value or null if not within a provider\n *\n * @example\n * ```tsx\n * // Without type params (returns generic context)\n * function MyFormField() {\n * const context = useFormSchema();\n * if (!context) return null;\n *\n * const { schema, discriminator } = context;\n * // Use schema for validation or field extraction\n * }\n *\n * // With type params (for type-safe schema access)\n * function TypedFormField() {\n * const context = useFormSchema({ schema: mySchema });\n * // context.schema is now typed as typeof mySchema\n * }\n * ```\n */\nexport function useFormSchema<\n TSchema extends z.ZodType = z.ZodType,\n TDiscriminatorKey extends\n DiscriminatorKey<TSchema> = DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<\n TSchema,\n TDiscriminatorKey\n > = DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>(\n // Parameter used for type inference only, not at runtime\n _params?: {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n },\n): FormSchemaContextValue<TSchema, TDiscriminatorKey, TDiscriminatorValue> {\n return useContext(\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n FormSchemaContext as Context<{\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n } | null>,\n );\n}\n\n/**\n * Provider component that makes Zod schema available to all child components.\n *\n * Use this to wrap your form and provide schema context to nested components\n * like field labels and validation indicators.\n *\n * @example\n * Basic usage with ZodObject\n * ```tsx\n * const schema = z.object({\n * name: z.string(),\n * email: z.string().email().optional()\n * });\n *\n * <FormSchemaProvider schema={schema}>\n * <YourFormComponents />\n * </FormSchemaProvider>\n * ```\n *\n * @example\n * Usage with discriminated union\n * ```tsx\n * const schema = z.discriminatedUnion('mode', [\n * z.object({ mode: z.literal('create'), name: z.string() }),\n * z.object({ mode: z.literal('edit'), id: z.number() })\n * ]);\n *\n * <FormSchemaProvider\n * schema={schema}\n * discriminator={{ key: 'mode', value: 'create' }}\n * >\n * <YourFormComponents />\n * </FormSchemaProvider>\n * ```\n */\nexport function FormSchemaProvider<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n schema,\n discriminator,\n children,\n}: {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n children: ReactNode;\n}) {\n return (\n <FormSchemaContext.Provider value={{ schema, discriminator }}>\n {children}\n </FormSchemaContext.Provider>\n );\n}\n\n/**\n * Hook to check if a field requires valid input based on the Zod schema.\n *\n * Uses the schema from {@link FormSchemaContext} to determine if a field\n * will show validation errors when submitted with empty/invalid input.\n *\n * @param params - Schema, field name, and optional discriminator (schema used for type inference)\n * @returns true if the field requires valid input, false otherwise\n *\n * @example\n * ```tsx\n * function MyFieldLabel({ name, schema }: { name: string; schema: z.ZodType }) {\n * const isRequired = useIsRequiredField({ schema, fieldName: name });\n *\n * return (\n * <label>\n * {name}\n * {isRequired && <span className=\"text-red-500\">*</span>}\n * </label>\n * );\n * }\n * ```\n */\nexport function useIsRequiredField<\n TSchema extends z.ZodType,\n TName extends keyof Extract<\n Required<z.input<TSchema>>,\n Record<TDiscriminatorKey, TDiscriminatorValue>\n >,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n fieldName,\n ...props\n}: {\n schema: TSchema;\n fieldName: TName;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n}): boolean {\n const context = useFormSchema<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >(props);\n\n if (!context) {\n return false;\n }\n\n return isRequiredField({\n schema: context.schema,\n fieldName,\n discriminator: context.discriminator,\n });\n}\n\n/**\n * Determines if a field requires valid input (will show validation errors on empty/invalid input).\n *\n * Uses `requiresValidInput` from `@zod-utils/core` which checks the underlying field after\n * removing defaults. This tells you if the field will error when user submits empty input.\n *\n * Returns false if the underlying field accepts:\n * - `undefined` (via `.optional()`)\n * - `null` (via `.nullable()`)\n * - Empty strings (plain `z.string()` without `.min(1)`)\n * - Empty arrays (plain `z.array()` without `.min(1)`)\n *\n * @param options - Schema, field name, and optional discriminator\n * @returns true if the field requires valid input, false otherwise\n *\n * @example\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * bio: z.string().optional(),\n * });\n *\n * isRequiredField({ schema, fieldName: 'name' }); // true\n * isRequiredField({ schema, fieldName: 'bio' }); // false\n * ```\n *\n * @example\n * With discriminated union\n * ```typescript\n * const schema = z.discriminatedUnion('mode', [\n * z.object({ mode: z.literal('create'), name: z.string().min(1) }),\n * z.object({ mode: z.literal('edit'), id: z.number() }),\n * ]);\n *\n * isRequiredField({\n * schema,\n * fieldName: 'name',\n * discriminator: { key: 'mode', value: 'create' },\n * }); // true\n * ```\n */\nexport function isRequiredField<\n TSchema extends z.ZodType,\n TName extends keyof Extract<\n Required<z.input<TSchema>>,\n Record<TDiscriminatorKey, TDiscriminatorValue>\n >,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n schema,\n fieldName,\n discriminator,\n}: {\n schema: TSchema;\n fieldName: TName;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n}): boolean {\n const field = extractFieldFromSchema({\n schema,\n fieldName,\n discriminator,\n });\n\n if (!field) {\n return false;\n }\n\n return requiresValidInput(field);\n}\n","import { zodResolver } from '@hookform/resolvers/zod';\nimport { type FieldValues, type UseFormProps, useForm } from 'react-hook-form';\nimport type { z } from 'zod';\nimport type {\n PartialWithAllNullables,\n PartialWithNullableObjects,\n} from './types';\n\n/**\n * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.\n *\n * This hook eliminates the TypeScript friction between React Hook Form's nullable field values\n * and Zod's strict output types. It uses a two-type schema pattern where:\n * - **Input type** (`PartialWithNullableObjects<TOutput>`): Form fields accept `null | undefined` during editing\n * - **Output type** (`TOutput`): Validated data matches exact schema type (no `null | undefined`)\n *\n * **Key Benefits:**\n * - ✅ No more \"Type 'null' is not assignable to...\" TypeScript errors\n * - ✅ Use `form.setValue()` and `form.reset()` with `null` values freely\n * - ✅ Validated output is still type-safe with exact Zod schema types\n * - ✅ Automatic zodResolver setup - no manual configuration needed\n *\n * @template TOutput - The Zod schema output type (extends FieldValues)\n * @template TInput - The Zod schema input type (accepts nullable/undefined values during form editing)\n *\n * @param options - Configuration object\n * @param options.schema - Zod schema with two-type signature `z.ZodType<TOutput, TInput>`\n * @param options.defaultValues - Default form values (shallow partial - nested objects must be complete if provided)\n * @param options.zodResolverOptions - Optional zodResolver configuration\n * @param options....formOptions - All other react-hook-form useForm options\n *\n * @returns React Hook Form instance with type-safe methods\n *\n * @example\n * Basic usage with required fields\n * ```typescript\n * import { useZodForm } from '@zod-utils/react-hook-form';\n * import { z } from 'zod';\n *\n * const schema = z.object({\n * name: z.string().min(1), // Required field\n * age: z.number().min(0),\n * }) satisfies z.ZodType<{ name: string; age: number }, any>;\n *\n * function MyForm() {\n * const form = useZodForm({ schema });\n *\n * // ✅ These work without type errors:\n * form.setValue('name', null); // Accepts null during editing\n * form.reset({ name: null, age: null }); // Reset with null\n *\n * const onSubmit = (data: { name: string; age: number }) => {\n * // ✅ data is exact type - no null | undefined\n * console.log(data.name.toUpperCase()); // Safe to use string methods\n * };\n *\n * return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;\n * }\n * ```\n *\n * @example\n * With default values\n * ```typescript\n * const schema = z.object({\n * username: z.string(),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * }) satisfies z.ZodType<{\n * username: string;\n * email: string;\n * notifications: boolean;\n * }, any>;\n *\n * const form = useZodForm({\n * schema,\n * defaultValues: {\n * username: '',\n * email: '',\n * // notifications gets default from schema\n * },\n * });\n * ```\n *\n * @example\n * Without default values (all fields are optional during editing)\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * email: z.string().email(),\n * age: z.number(),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * // ✅ No defaultValues needed - fields are optional during editing\n * const form = useZodForm({ schema });\n *\n * // Form fields can be set individually as user types\n * form.setValue('name', 'John');\n * form.setValue('email', 'john@example.com');\n * form.setValue('age', 25);\n *\n * // All fields must be valid on submit (per schema validation)\n * ```\n *\n * @example\n * With optional and nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string(),\n * description: z.string().optional(), // Optional in output\n * tags: z.array(z.string()).nullable(), // Nullable in output\n * }) satisfies z.ZodType<{\n * title: string;\n * description?: string;\n * tags: string[] | null;\n * }, any>;\n *\n * const form = useZodForm({ schema });\n *\n * // All fields accept null/undefined during editing\n * form.setValue('title', null);\n * form.setValue('description', undefined);\n * form.setValue('tags', null);\n * ```\n *\n * @example\n * With zodResolver options\n * ```typescript\n * const form = useZodForm({\n * schema,\n * zodResolverOptions: {\n * async: true, // Enable async validation\n * errorMap: customErrorMap, // Custom error messages\n * },\n * });\n * ```\n *\n * @example\n * Complete form example\n * ```typescript\n * const userSchema = z.object({\n * name: z.string().min(1, 'Name is required'),\n * email: z.string().email('Invalid email'),\n * age: z.number().min(18, 'Must be 18+'),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * function UserForm() {\n * const form = useZodForm({\n * schema: userSchema,\n * defaultValues: { name: '', email: '', age: null },\n * });\n *\n * const onSubmit = (data: { name: string; email: string; age: number }) => {\n * // Type-safe: data has exact types, no null/undefined\n * console.log(`${data.name} is ${data.age} years old`);\n * };\n *\n * return (\n * <form onSubmit={form.handleSubmit(onSubmit)}>\n * <input {...form.register('name')} />\n * <input {...form.register('email')} type=\"email\" />\n * <input {...form.register('age', { valueAsNumber: true })} type=\"number\" />\n * <button type=\"submit\">Submit</button>\n * </form>\n * );\n * }\n * ```\n *\n * @see {@link PartialWithNullableObjects} for the type transformation utility\n * @see https://react-hook-form.com/docs/useform for React Hook Form documentation\n * @see https://zod.dev for Zod schema documentation\n * @since 0.1.0\n */\nexport const useZodForm = <\n TInput extends FieldValues,\n TOutput extends FieldValues,\n TFormInput extends\n PartialWithAllNullables<TInput> = PartialWithNullableObjects<TInput>,\n>({\n schema,\n zodResolverOptions,\n ...formOptions\n}: {\n schema: z.ZodType<TOutput, TInput>;\n defaultValues?: TFormInput;\n zodResolverOptions?: Parameters<typeof zodResolver>[1];\n} & Omit<\n UseFormProps<TFormInput, unknown, TOutput>,\n 'resolver' | 'defaultValues'\n>) => {\n const resolver = zodResolver(schema, zodResolverOptions);\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return useForm({\n resolver,\n ...formOptions,\n } as unknown as UseFormProps<TFormInput, unknown, TOutput>);\n};\n"]}
package/dist/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
+ import { extractFieldFromSchema, requiresValidInput } from '@zod-utils/core';
1
2
  export * from '@zod-utils/core';
3
+ import { createContext, useContext } from 'react';
4
+ import { jsx } from 'react/jsx-runtime';
2
5
  import { zodResolver } from '@hookform/resolvers/zod';
3
6
  import { useForm } from 'react-hook-form';
4
7
 
5
8
  var __defProp = Object.defineProperty;
6
- var __defProps = Object.defineProperties;
7
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
8
9
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
10
  var __hasOwnProp = Object.prototype.hasOwnProperty;
10
11
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -20,7 +21,6 @@ var __spreadValues = (a, b) => {
20
21
  }
21
22
  return a;
22
23
  };
23
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
24
24
  var __objRest = (source, exclude) => {
25
25
  var target = {};
26
26
  for (var prop in source)
@@ -33,6 +33,51 @@ var __objRest = (source, exclude) => {
33
33
  }
34
34
  return target;
35
35
  };
36
+ var FormSchemaContext = createContext(null);
37
+ function useFormSchema(_params) {
38
+ return useContext(
39
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
40
+ FormSchemaContext
41
+ );
42
+ }
43
+ function FormSchemaProvider({
44
+ schema,
45
+ discriminator,
46
+ children
47
+ }) {
48
+ return /* @__PURE__ */ jsx(FormSchemaContext.Provider, { value: { schema, discriminator }, children });
49
+ }
50
+ function useIsRequiredField(_a) {
51
+ var _b = _a, {
52
+ fieldName
53
+ } = _b; __objRest(_b, [
54
+ "fieldName"
55
+ ]);
56
+ const context = useFormSchema();
57
+ if (!context) {
58
+ return false;
59
+ }
60
+ return isRequiredField({
61
+ schema: context.schema,
62
+ fieldName,
63
+ discriminator: context.discriminator
64
+ });
65
+ }
66
+ function isRequiredField({
67
+ schema,
68
+ fieldName,
69
+ discriminator
70
+ }) {
71
+ const field = extractFieldFromSchema({
72
+ schema,
73
+ fieldName,
74
+ discriminator
75
+ });
76
+ if (!field) {
77
+ return false;
78
+ }
79
+ return requiresValidInput(field);
80
+ }
36
81
  var useZodForm = (_a) => {
37
82
  var _b = _a, {
38
83
  schema,
@@ -41,18 +86,12 @@ var useZodForm = (_a) => {
41
86
  "schema",
42
87
  "zodResolverOptions"
43
88
  ]);
44
- const resolver = zodResolver(
45
- schema,
46
- zodResolverOptions
47
- );
48
- const defaultValues = formOptions.defaultValues;
49
- return useForm(__spreadProps(__spreadValues({
89
+ const resolver = zodResolver(schema, zodResolverOptions);
90
+ return useForm(__spreadValues({
50
91
  resolver
51
- }, formOptions), {
52
- defaultValues
53
- }));
92
+ }, formOptions));
54
93
  };
55
94
 
56
- export { useZodForm };
95
+ export { FormSchemaContext, FormSchemaProvider, isRequiredField, useFormSchema, useIsRequiredField, useZodForm };
57
96
  //# sourceMappingURL=index.mjs.map
58
97
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/use-zod-form.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiLO,IAAM,UAAA,GAAa,CAMxB,EAAA,KAWI;AAXJ,EAAA,IAAA,EAAA,GAAA,EAAA,EACA;AAAA,IAAA,MAAA;AAAA,IACA;AAAA,GAzLF,GAuLE,EAAA,EAGG,WAAA,GAAA,SAAA,CAHH,EAAA,EAGG;AAAA,IAFH,QAAA;AAAA,IACA;AAAA,GAAA,CAAA;AAUA,EAAA,MAAM,QAAA,GAAW,WAAA;AAAA,IACf,MAAA;AAAA,IACA;AAAA,GACF;AAIA,EAAA,MAAM,gBAAgB,WAAA,CAAY,aAAA;AAElC,EAAA,OAAO,OAAA,CAAQ,aAAA,CAAA,cAAA,CAAA;AAAA,IACb;AAAA,GAAA,EACG,WAAA,CAAA,EAFU;AAAA,IAGb;AAAA,GACF,CAAC,CAAA;AACH","file":"index.mjs","sourcesContent":["import { zodResolver } from '@hookform/resolvers/zod';\nimport {\n type DefaultValues,\n type FieldValues,\n type UseFormProps,\n useForm,\n} from 'react-hook-form';\nimport type { z } from 'zod';\nimport type {\n PartialWithAllNullables,\n PartialWithNullableObjects,\n} from './types';\n\n/**\n * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.\n *\n * This hook eliminates the TypeScript friction between React Hook Form's nullable field values\n * and Zod's strict output types. It uses a two-type schema pattern where:\n * - **Input type** (`PartialWithNullableObjects<TOutput>`): Form fields accept `null | undefined` during editing\n * - **Output type** (`TOutput`): Validated data matches exact schema type (no `null | undefined`)\n *\n * **Key Benefits:**\n * - ✅ No more \"Type 'null' is not assignable to...\" TypeScript errors\n * - ✅ Use `form.setValue()` and `form.reset()` with `null` values freely\n * - ✅ Validated output is still type-safe with exact Zod schema types\n * - ✅ Automatic zodResolver setup - no manual configuration needed\n *\n * @template TOutput - The Zod schema output type (extends FieldValues)\n * @template TInput - The Zod schema input type (accepts nullable/undefined values during form editing)\n *\n * @param options - Configuration object\n * @param options.schema - Zod schema with two-type signature `z.ZodType<TOutput, TInput>`\n * @param options.defaultValues - Default form values (shallow partial - nested objects must be complete if provided)\n * @param options.zodResolverOptions - Optional zodResolver configuration\n * @param options....formOptions - All other react-hook-form useForm options\n *\n * @returns React Hook Form instance with type-safe methods\n *\n * @example\n * Basic usage with required fields\n * ```typescript\n * import { useZodForm } from '@zod-utils/react-hook-form';\n * import { z } from 'zod';\n *\n * const schema = z.object({\n * name: z.string().min(1), // Required field\n * age: z.number().min(0),\n * }) satisfies z.ZodType<{ name: string; age: number }, any>;\n *\n * function MyForm() {\n * const form = useZodForm({ schema });\n *\n * // ✅ These work without type errors:\n * form.setValue('name', null); // Accepts null during editing\n * form.reset({ name: null, age: null }); // Reset with null\n *\n * const onSubmit = (data: { name: string; age: number }) => {\n * // ✅ data is exact type - no null | undefined\n * console.log(data.name.toUpperCase()); // Safe to use string methods\n * };\n *\n * return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;\n * }\n * ```\n *\n * @example\n * With default values\n * ```typescript\n * const schema = z.object({\n * username: z.string(),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * }) satisfies z.ZodType<{\n * username: string;\n * email: string;\n * notifications: boolean;\n * }, any>;\n *\n * const form = useZodForm({\n * schema,\n * defaultValues: {\n * username: '',\n * email: '',\n * // notifications gets default from schema\n * },\n * });\n * ```\n *\n * @example\n * Without default values (all fields are optional during editing)\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * email: z.string().email(),\n * age: z.number(),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * // ✅ No defaultValues needed - fields are optional during editing\n * const form = useZodForm({ schema });\n *\n * // Form fields can be set individually as user types\n * form.setValue('name', 'John');\n * form.setValue('email', 'john@example.com');\n * form.setValue('age', 25);\n *\n * // All fields must be valid on submit (per schema validation)\n * ```\n *\n * @example\n * With optional and nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string(),\n * description: z.string().optional(), // Optional in output\n * tags: z.array(z.string()).nullable(), // Nullable in output\n * }) satisfies z.ZodType<{\n * title: string;\n * description?: string;\n * tags: string[] | null;\n * }, any>;\n *\n * const form = useZodForm({ schema });\n *\n * // All fields accept null/undefined during editing\n * form.setValue('title', null);\n * form.setValue('description', undefined);\n * form.setValue('tags', null);\n * ```\n *\n * @example\n * With zodResolver options\n * ```typescript\n * const form = useZodForm({\n * schema,\n * zodResolverOptions: {\n * async: true, // Enable async validation\n * errorMap: customErrorMap, // Custom error messages\n * },\n * });\n * ```\n *\n * @example\n * Complete form example\n * ```typescript\n * const userSchema = z.object({\n * name: z.string().min(1, 'Name is required'),\n * email: z.string().email('Invalid email'),\n * age: z.number().min(18, 'Must be 18+'),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * function UserForm() {\n * const form = useZodForm({\n * schema: userSchema,\n * defaultValues: { name: '', email: '', age: null },\n * });\n *\n * const onSubmit = (data: { name: string; email: string; age: number }) => {\n * // Type-safe: data has exact types, no null/undefined\n * console.log(`${data.name} is ${data.age} years old`);\n * };\n *\n * return (\n * <form onSubmit={form.handleSubmit(onSubmit)}>\n * <input {...form.register('name')} />\n * <input {...form.register('email')} type=\"email\" />\n * <input {...form.register('age', { valueAsNumber: true })} type=\"number\" />\n * <button type=\"submit\">Submit</button>\n * </form>\n * );\n * }\n * ```\n *\n * @see {@link PartialWithNullableObjects} for the type transformation utility\n * @see https://react-hook-form.com/docs/useform for React Hook Form documentation\n * @see https://zod.dev for Zod schema documentation\n * @since 0.1.0\n */\nexport const useZodForm = <\n TOutput extends FieldValues,\n TFormInput extends\n PartialWithAllNullables<TOutput> = PartialWithNullableObjects<TOutput>,\n TInput extends TFormInput = TFormInput,\n TDefault extends TFormInput = TFormInput,\n>({\n schema,\n zodResolverOptions,\n ...formOptions\n}: {\n schema: z.ZodType<TOutput, TInput>;\n defaultValues?: TDefault;\n zodResolverOptions?: Parameters<typeof zodResolver>[1];\n} & Omit<\n UseFormProps<TFormInput, unknown, TOutput>,\n 'resolver' | 'defaultValues'\n>) => {\n const resolver = zodResolver<TFormInput, unknown, TOutput>(\n schema,\n zodResolverOptions,\n );\n\n // can't help but to assert here - because DefaultValues<TDefault> makes object fields DeepPartial\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const defaultValues = formOptions.defaultValues as DefaultValues<TDefault>;\n\n return useForm({\n resolver,\n ...formOptions,\n defaultValues,\n });\n};\n"]}
1
+ {"version":3,"sources":["../src/context.tsx","../src/use-zod-form.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDO,IAAM,iBAAA,GAAoB,cAMvB,IAAI;AA6BP,SAAS,cAUd,OAAA,EAQyE;AACzE,EAAA,OAAO,UAAA;AAAA;AAAA,IAEL;AAAA,GAQF;AACF;AAqCO,SAAS,kBAAA,CAId;AAAA,EACA,MAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAA,EAQG;AACD,EAAA,uBACE,GAAA,CAAC,kBAAkB,QAAA,EAAlB,EAA2B,OAAO,EAAE,MAAA,EAAQ,aAAA,EAAc,EACxD,QAAA,EACH,CAAA;AAEJ;AAyBO,SAAS,mBAQd,EAAA,EAWU;AAXV,EAAA,IAAA,EAAA,GAAA,EAAA,CAAA,CACA;AAAA,IAAA;AAAA,GAjNF,GAgNE,EAAA,CAAA,CAEG,SAAA,CAFH,EAAA,EAEG;AAAA,IADH;AAAA,GAAA;AAWA,EAAA,MAAM,OAAA,GAAU,cAIT,CAAA;AAEP,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,eAAA,CAAgB;AAAA,IACrB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,SAAA;AAAA,IACA,eAAe,OAAA,CAAQ;AAAA,GACxB,CAAA;AACH;AA2CO,SAAS,eAAA,CAQd;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAQY;AACV,EAAA,MAAM,QAAQ,sBAAA,CAAuB;AAAA,IACnC,MAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AAED,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,mBAAmB,KAAK,CAAA;AACjC;AC1IO,IAAM,UAAA,GAAa,CAKxB,EAAA,KAWI;AAXJ,EAAA,IAAA,EAAA,GAAA,EAAA,EACA;AAAA,IAAA,MAAA;AAAA,IACA;AAAA,GAnLF,GAiLE,EAAA,EAGG,WAAA,GAAA,SAAA,CAHH,EAAA,EAGG;AAAA,IAFH,QAAA;AAAA,IACA;AAAA,GAAA,CAAA;AAUA,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,MAAA,EAAQ,kBAAkB,CAAA;AAGvD,EAAA,OAAO,OAAA,CAAQ,cAAA,CAAA;AAAA,IACb;AAAA,GAAA,EACG,WAAA,CACqD,CAAA;AAC5D","file":"index.mjs","sourcesContent":["'use client';\n\nimport {\n type Discriminator,\n type DiscriminatorKey,\n type DiscriminatorValue,\n extractFieldFromSchema,\n requiresValidInput,\n} from '@zod-utils/core';\nimport { type Context, createContext, type ReactNode, useContext } from 'react';\nimport type { z } from 'zod';\n\n/**\n * Type for the FormSchemaContext with full generic support.\n * @internal\n */\nexport type FormSchemaContextType<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n> = Context<{\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n} | null>;\n\n/**\n * Context value type for FormSchemaContext.\n */\nexport type FormSchemaContextValue<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n> = {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n} | null;\n\n/**\n * React Context for providing Zod schema to form components.\n *\n * Use with {@link FormSchemaProvider} to provide schema context, and\n * {@link useFormSchema} to consume it in child components.\n */\nexport const FormSchemaContext = createContext<{\n schema: z.ZodType;\n discriminator?: {\n key: unknown;\n value: unknown;\n };\n} | null>(null);\n\n/**\n * Hook to access the form schema from context.\n *\n * The optional `_params` argument is used for TypeScript type inference only.\n * Pass your schema to get proper type narrowing of the context value.\n *\n * @param _params - Optional params for type inference (not used at runtime)\n * @returns The schema context value or null if not within a provider\n *\n * @example\n * ```tsx\n * // Without type params (returns generic context)\n * function MyFormField() {\n * const context = useFormSchema();\n * if (!context) return null;\n *\n * const { schema, discriminator } = context;\n * // Use schema for validation or field extraction\n * }\n *\n * // With type params (for type-safe schema access)\n * function TypedFormField() {\n * const context = useFormSchema({ schema: mySchema });\n * // context.schema is now typed as typeof mySchema\n * }\n * ```\n */\nexport function useFormSchema<\n TSchema extends z.ZodType = z.ZodType,\n TDiscriminatorKey extends\n DiscriminatorKey<TSchema> = DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<\n TSchema,\n TDiscriminatorKey\n > = DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>(\n // Parameter used for type inference only, not at runtime\n _params?: {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n },\n): FormSchemaContextValue<TSchema, TDiscriminatorKey, TDiscriminatorValue> {\n return useContext(\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n FormSchemaContext as Context<{\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n } | null>,\n );\n}\n\n/**\n * Provider component that makes Zod schema available to all child components.\n *\n * Use this to wrap your form and provide schema context to nested components\n * like field labels and validation indicators.\n *\n * @example\n * Basic usage with ZodObject\n * ```tsx\n * const schema = z.object({\n * name: z.string(),\n * email: z.string().email().optional()\n * });\n *\n * <FormSchemaProvider schema={schema}>\n * <YourFormComponents />\n * </FormSchemaProvider>\n * ```\n *\n * @example\n * Usage with discriminated union\n * ```tsx\n * const schema = z.discriminatedUnion('mode', [\n * z.object({ mode: z.literal('create'), name: z.string() }),\n * z.object({ mode: z.literal('edit'), id: z.number() })\n * ]);\n *\n * <FormSchemaProvider\n * schema={schema}\n * discriminator={{ key: 'mode', value: 'create' }}\n * >\n * <YourFormComponents />\n * </FormSchemaProvider>\n * ```\n */\nexport function FormSchemaProvider<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n schema,\n discriminator,\n children,\n}: {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n children: ReactNode;\n}) {\n return (\n <FormSchemaContext.Provider value={{ schema, discriminator }}>\n {children}\n </FormSchemaContext.Provider>\n );\n}\n\n/**\n * Hook to check if a field requires valid input based on the Zod schema.\n *\n * Uses the schema from {@link FormSchemaContext} to determine if a field\n * will show validation errors when submitted with empty/invalid input.\n *\n * @param params - Schema, field name, and optional discriminator (schema used for type inference)\n * @returns true if the field requires valid input, false otherwise\n *\n * @example\n * ```tsx\n * function MyFieldLabel({ name, schema }: { name: string; schema: z.ZodType }) {\n * const isRequired = useIsRequiredField({ schema, fieldName: name });\n *\n * return (\n * <label>\n * {name}\n * {isRequired && <span className=\"text-red-500\">*</span>}\n * </label>\n * );\n * }\n * ```\n */\nexport function useIsRequiredField<\n TSchema extends z.ZodType,\n TName extends keyof Extract<\n Required<z.input<TSchema>>,\n Record<TDiscriminatorKey, TDiscriminatorValue>\n >,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n fieldName,\n ...props\n}: {\n schema: TSchema;\n fieldName: TName;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n}): boolean {\n const context = useFormSchema<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >(props);\n\n if (!context) {\n return false;\n }\n\n return isRequiredField({\n schema: context.schema,\n fieldName,\n discriminator: context.discriminator,\n });\n}\n\n/**\n * Determines if a field requires valid input (will show validation errors on empty/invalid input).\n *\n * Uses `requiresValidInput` from `@zod-utils/core` which checks the underlying field after\n * removing defaults. This tells you if the field will error when user submits empty input.\n *\n * Returns false if the underlying field accepts:\n * - `undefined` (via `.optional()`)\n * - `null` (via `.nullable()`)\n * - Empty strings (plain `z.string()` without `.min(1)`)\n * - Empty arrays (plain `z.array()` without `.min(1)`)\n *\n * @param options - Schema, field name, and optional discriminator\n * @returns true if the field requires valid input, false otherwise\n *\n * @example\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * bio: z.string().optional(),\n * });\n *\n * isRequiredField({ schema, fieldName: 'name' }); // true\n * isRequiredField({ schema, fieldName: 'bio' }); // false\n * ```\n *\n * @example\n * With discriminated union\n * ```typescript\n * const schema = z.discriminatedUnion('mode', [\n * z.object({ mode: z.literal('create'), name: z.string().min(1) }),\n * z.object({ mode: z.literal('edit'), id: z.number() }),\n * ]);\n *\n * isRequiredField({\n * schema,\n * fieldName: 'name',\n * discriminator: { key: 'mode', value: 'create' },\n * }); // true\n * ```\n */\nexport function isRequiredField<\n TSchema extends z.ZodType,\n TName extends keyof Extract<\n Required<z.input<TSchema>>,\n Record<TDiscriminatorKey, TDiscriminatorValue>\n >,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n schema,\n fieldName,\n discriminator,\n}: {\n schema: TSchema;\n fieldName: TName;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n}): boolean {\n const field = extractFieldFromSchema({\n schema,\n fieldName,\n discriminator,\n });\n\n if (!field) {\n return false;\n }\n\n return requiresValidInput(field);\n}\n","import { zodResolver } from '@hookform/resolvers/zod';\nimport { type FieldValues, type UseFormProps, useForm } from 'react-hook-form';\nimport type { z } from 'zod';\nimport type {\n PartialWithAllNullables,\n PartialWithNullableObjects,\n} from './types';\n\n/**\n * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.\n *\n * This hook eliminates the TypeScript friction between React Hook Form's nullable field values\n * and Zod's strict output types. It uses a two-type schema pattern where:\n * - **Input type** (`PartialWithNullableObjects<TOutput>`): Form fields accept `null | undefined` during editing\n * - **Output type** (`TOutput`): Validated data matches exact schema type (no `null | undefined`)\n *\n * **Key Benefits:**\n * - ✅ No more \"Type 'null' is not assignable to...\" TypeScript errors\n * - ✅ Use `form.setValue()` and `form.reset()` with `null` values freely\n * - ✅ Validated output is still type-safe with exact Zod schema types\n * - ✅ Automatic zodResolver setup - no manual configuration needed\n *\n * @template TOutput - The Zod schema output type (extends FieldValues)\n * @template TInput - The Zod schema input type (accepts nullable/undefined values during form editing)\n *\n * @param options - Configuration object\n * @param options.schema - Zod schema with two-type signature `z.ZodType<TOutput, TInput>`\n * @param options.defaultValues - Default form values (shallow partial - nested objects must be complete if provided)\n * @param options.zodResolverOptions - Optional zodResolver configuration\n * @param options....formOptions - All other react-hook-form useForm options\n *\n * @returns React Hook Form instance with type-safe methods\n *\n * @example\n * Basic usage with required fields\n * ```typescript\n * import { useZodForm } from '@zod-utils/react-hook-form';\n * import { z } from 'zod';\n *\n * const schema = z.object({\n * name: z.string().min(1), // Required field\n * age: z.number().min(0),\n * }) satisfies z.ZodType<{ name: string; age: number }, any>;\n *\n * function MyForm() {\n * const form = useZodForm({ schema });\n *\n * // ✅ These work without type errors:\n * form.setValue('name', null); // Accepts null during editing\n * form.reset({ name: null, age: null }); // Reset with null\n *\n * const onSubmit = (data: { name: string; age: number }) => {\n * // ✅ data is exact type - no null | undefined\n * console.log(data.name.toUpperCase()); // Safe to use string methods\n * };\n *\n * return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;\n * }\n * ```\n *\n * @example\n * With default values\n * ```typescript\n * const schema = z.object({\n * username: z.string(),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * }) satisfies z.ZodType<{\n * username: string;\n * email: string;\n * notifications: boolean;\n * }, any>;\n *\n * const form = useZodForm({\n * schema,\n * defaultValues: {\n * username: '',\n * email: '',\n * // notifications gets default from schema\n * },\n * });\n * ```\n *\n * @example\n * Without default values (all fields are optional during editing)\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * email: z.string().email(),\n * age: z.number(),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * // ✅ No defaultValues needed - fields are optional during editing\n * const form = useZodForm({ schema });\n *\n * // Form fields can be set individually as user types\n * form.setValue('name', 'John');\n * form.setValue('email', 'john@example.com');\n * form.setValue('age', 25);\n *\n * // All fields must be valid on submit (per schema validation)\n * ```\n *\n * @example\n * With optional and nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string(),\n * description: z.string().optional(), // Optional in output\n * tags: z.array(z.string()).nullable(), // Nullable in output\n * }) satisfies z.ZodType<{\n * title: string;\n * description?: string;\n * tags: string[] | null;\n * }, any>;\n *\n * const form = useZodForm({ schema });\n *\n * // All fields accept null/undefined during editing\n * form.setValue('title', null);\n * form.setValue('description', undefined);\n * form.setValue('tags', null);\n * ```\n *\n * @example\n * With zodResolver options\n * ```typescript\n * const form = useZodForm({\n * schema,\n * zodResolverOptions: {\n * async: true, // Enable async validation\n * errorMap: customErrorMap, // Custom error messages\n * },\n * });\n * ```\n *\n * @example\n * Complete form example\n * ```typescript\n * const userSchema = z.object({\n * name: z.string().min(1, 'Name is required'),\n * email: z.string().email('Invalid email'),\n * age: z.number().min(18, 'Must be 18+'),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * function UserForm() {\n * const form = useZodForm({\n * schema: userSchema,\n * defaultValues: { name: '', email: '', age: null },\n * });\n *\n * const onSubmit = (data: { name: string; email: string; age: number }) => {\n * // Type-safe: data has exact types, no null/undefined\n * console.log(`${data.name} is ${data.age} years old`);\n * };\n *\n * return (\n * <form onSubmit={form.handleSubmit(onSubmit)}>\n * <input {...form.register('name')} />\n * <input {...form.register('email')} type=\"email\" />\n * <input {...form.register('age', { valueAsNumber: true })} type=\"number\" />\n * <button type=\"submit\">Submit</button>\n * </form>\n * );\n * }\n * ```\n *\n * @see {@link PartialWithNullableObjects} for the type transformation utility\n * @see https://react-hook-form.com/docs/useform for React Hook Form documentation\n * @see https://zod.dev for Zod schema documentation\n * @since 0.1.0\n */\nexport const useZodForm = <\n TInput extends FieldValues,\n TOutput extends FieldValues,\n TFormInput extends\n PartialWithAllNullables<TInput> = PartialWithNullableObjects<TInput>,\n>({\n schema,\n zodResolverOptions,\n ...formOptions\n}: {\n schema: z.ZodType<TOutput, TInput>;\n defaultValues?: TFormInput;\n zodResolverOptions?: Parameters<typeof zodResolver>[1];\n} & Omit<\n UseFormProps<TFormInput, unknown, TOutput>,\n 'resolver' | 'defaultValues'\n>) => {\n const resolver = zodResolver(schema, zodResolverOptions);\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return useForm({\n resolver,\n ...formOptions,\n } as unknown as UseFormProps<TFormInput, unknown, TOutput>);\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zod-utils/react-hook-form",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "React Hook Form integration and utilities for Zod schemas",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",