@zeno-lib/forms 0.0.1 → 0.1.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.
Files changed (44) hide show
  1. package/package.json +21 -11
  2. package/src/addons/validation-spinner.tsx +3 -4
  3. package/src/create-form.tsx +49 -0
  4. package/src/create-zeno-form.tsx +531 -0
  5. package/src/fields/checkbox-field.tsx +15 -12
  6. package/src/fields/combobox-field.tsx +13 -14
  7. package/src/fields/date-picker-field.tsx +18 -15
  8. package/src/fields/input-field.tsx +12 -13
  9. package/src/fields/number-field.tsx +1 -2
  10. package/src/fields/otp-field.tsx +15 -12
  11. package/src/fields/radio-group-field.tsx +12 -13
  12. package/src/fields/reset-button.tsx +2 -3
  13. package/src/fields/select-field.tsx +12 -13
  14. package/src/fields/slider-field.tsx +11 -12
  15. package/src/fields/submit-button.tsx +3 -4
  16. package/src/fields/switch-field.tsx +15 -12
  17. package/src/fields/textarea-field.tsx +12 -13
  18. package/src/form-element.tsx +51 -0
  19. package/src/index.ts +14 -0
  20. package/src/lib/apply-validation-error.test.ts +1 -2
  21. package/src/lib/required-indicator.tsx +1 -1
  22. package/src/lib/schema-defaults.test.ts +1 -2
  23. package/src/lib/schema-required.test.ts +98 -6
  24. package/src/lib/schema-required.ts +126 -22
  25. package/src/lib/use-is-invalid.ts +4 -4
  26. package/src/lib/use-rebased-default-values.ts +42 -0
  27. package/src/lib/use-unsaved-changes-warning.ts +3 -3
  28. package/src/lib/validation-error.test.ts +1 -1
  29. package/src/lib/validation-logic.test.ts +1 -1
  30. package/src/lib/validation-modes.test-d.ts +1 -1
  31. package/src/lib/validation-modes.test.ts +12 -2
  32. package/src/lib/validation-modes.ts +3 -1
  33. package/src/use-app-fields.test-d.ts +8 -25
  34. package/src/use-app-fields.test.tsx +8 -32
  35. package/src/use-form.cascade.test.tsx +4 -6
  36. package/src/use-form.focus-invalid.test.tsx +198 -0
  37. package/src/use-form.reset-defaults.test.tsx +125 -0
  38. package/src/use-form.test-d.ts +2 -3
  39. package/src/use-form.test.tsx +12 -14
  40. package/src/use-form.unsaved-warning.test.tsx +4 -13
  41. package/src/use-form.validation-modes.test.tsx +10 -8
  42. package/src/form.tsx +0 -105
  43. package/src/use-app-fields.tsx +0 -272
  44. package/src/use-form.tsx +0 -450
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@zeno-lib/forms",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "exports": {
5
- ".": "./src/form.tsx",
6
- "./addons": "./src/addons/index.ts",
7
- "./fields": "./src/fields/index.ts",
5
+ ".": "./src/index.ts",
6
+ "./create-form": "./src/create-form.tsx",
7
+ "./lib/*": "./src/lib/*.ts",
8
8
  "./tanstack": "./src/tanstack.ts"
9
9
  },
10
10
  "files": [
@@ -23,21 +23,31 @@
23
23
  "next": ">=16",
24
24
  "react": ">=19",
25
25
  "react-dom": ">=19",
26
- "zod": ">=4",
27
- "@zeno-lib/ui": "^0.0.1"
26
+ "zod": ">=4"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "@tanstack/react-devtools": {
30
+ "optional": true
31
+ },
32
+ "@tanstack/react-form-devtools": {
33
+ "optional": true
34
+ }
28
35
  },
29
36
  "dependencies": {
30
- "@tanstack/react-form": "1.29.1",
31
- "@tanstack/react-form-nextjs": "1.29.1"
37
+ "@tanstack/react-form": "1.33.5",
38
+ "@tanstack/react-form-nextjs": "1.33.5"
32
39
  },
33
40
  "devDependencies": {
34
41
  "@types/node": "25.6.0",
35
42
  "@types/react": "19.2.14",
36
43
  "@types/react-dom": "19.2.3",
37
- "typescript": "6.0.3",
44
+ "typescript": "7.0.2",
45
+ "vite": "8.0.16",
46
+ "vitest": "4.1.11",
38
47
  "zod": "^4.3.6",
39
- "@zeno-lib/typescript": "^1.0.0",
40
- "@zeno-lib/vitest": "^0.0.1"
48
+ "@zeno-lib/typescript": "^1.1.0",
49
+ "@zeno-lib/ui": "^0.0.1",
50
+ "@zeno-lib/test": "^0.1.0"
41
51
  },
42
52
  "scripts": {
43
53
  "test": "vitest --run",
@@ -1,9 +1,8 @@
1
1
  "use client"
2
2
 
3
- import { InputGroupAddon } from "@zeno-lib/ui/input-group"
4
- import { Spinner } from "@zeno-lib/ui/spinner"
5
-
6
- import { useFieldContext } from "../lib/contexts"
3
+ import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
4
+ import { InputGroupAddon } from "@/components/ui/input-group"
5
+ import { Spinner } from "@/components/ui/spinner"
7
6
 
8
7
  type ValidationSpinnerProps = {
9
8
  align?: "inline-start" | "inline-end"
@@ -0,0 +1,49 @@
1
+ /* biome-ignore-all lint/performance/noBarrelFile: composition root for your form. */
2
+ "use client"
3
+
4
+ import { createZenoForm } from "@zeno-lib/forms"
5
+
6
+ import { CheckboxField } from "./fields/checkbox-field"
7
+ import { ComboboxField } from "./fields/combobox-field"
8
+ import { DatePickerField } from "./fields/date-picker-field"
9
+ import { EmailField } from "./fields/email-field"
10
+ import { InputField } from "./fields/input-field"
11
+ import { NumberField } from "./fields/number-field"
12
+ import { OtpField } from "./fields/otp-field"
13
+ import { PasswordField } from "./fields/password-field"
14
+ import { RadioGroupField } from "./fields/radio-group-field"
15
+ import { ResetButton } from "./fields/reset-button"
16
+ import { SelectField } from "./fields/select-field"
17
+ import { SliderField } from "./fields/slider-field"
18
+ import { SubmitButton } from "./fields/submit-button"
19
+ import { SwitchField } from "./fields/switch-field"
20
+ import { TextAreaField } from "./fields/textarea-field"
21
+
22
+ // Wire the dropped-in field components into the headless factory. Edit the
23
+ // field files under `./fields` freely — they use your own shadcn primitives.
24
+ const { useAppForm, useForm, withFieldGroup, withForm } = createZenoForm({
25
+ fieldComponents: {
26
+ CheckboxField,
27
+ ComboboxField,
28
+ DatePickerField,
29
+ EmailField,
30
+ InputField,
31
+ NumberField,
32
+ OtpField,
33
+ PasswordField,
34
+ RadioGroupField,
35
+ SelectField,
36
+ SliderField,
37
+ SwitchField,
38
+ TextAreaField,
39
+ },
40
+ formComponents: {
41
+ ResetButton,
42
+ SubmitButton,
43
+ },
44
+ })
45
+
46
+ export * from "@zeno-lib/forms"
47
+ export { ValidationSpinner } from "./addons/validation-spinner"
48
+ export { RadioGroupFieldItem } from "./fields/radio-group-field"
49
+ export { useAppForm, useForm, withFieldGroup, withForm }
@@ -0,0 +1,531 @@
1
+ /* biome-ignore-all lint/performance/noBarrelFile: factory returns the wired form API. */
2
+ "use client"
3
+
4
+ import type {
5
+ AnyFormApi,
6
+ DeepKeys,
7
+ FormAsyncValidateOrFn,
8
+ FormOptions,
9
+ FormValidateOrFn,
10
+ } from "@tanstack/react-form"
11
+ import { createFormHook } from "@tanstack/react-form"
12
+ import { type ComponentProps, type ReactNode, useMemo } from "react"
13
+ import { applyValidationError } from "./lib/apply-validation-error"
14
+ import { fieldContext, formContext } from "./lib/contexts"
15
+ import { deepMergeDefaults, extractZodDefaults } from "./lib/schema-defaults"
16
+ import { getRequiredPaths } from "./lib/schema-required"
17
+ import { useRebasedDefaultValues } from "./lib/use-rebased-default-values"
18
+ import { useUnsavedChangesWarning } from "./lib/use-unsaved-changes-warning"
19
+ import { ValidationError } from "./lib/validation-error"
20
+ import { blurThenChangeLogic } from "./lib/validation-logic"
21
+ import {
22
+ DEFAULT_VALIDATION_MODE,
23
+ setFormZenoState,
24
+ type ValidationMode,
25
+ } from "./lib/validation-modes"
26
+
27
+ // Structural shape of a Standard Schema (Zod, Valibot, ArkType, …). Avoids a
28
+ // direct dependency on `@standard-schema/spec`; runtime validation goes through
29
+ // TanStack Form's standard-schema integration unchanged.
30
+ type StandardSchema<T> = {
31
+ readonly "~standard": {
32
+ readonly validate: (value: unknown) => unknown
33
+ readonly types?: { readonly input: T; readonly output: T }
34
+ }
35
+ }
36
+
37
+ // Recursive partial that preserves arrays, dates, and other structural types as
38
+ // atomic — only plain object keys become optional. Relaxes `defaultValues` so
39
+ // users can omit fields covered by the schema's `.default(...)` values or the
40
+ // string/array auto-fill (see `lib/schema-defaults.ts`).
41
+ type PartialFormData<T> = T extends readonly unknown[]
42
+ ? T
43
+ : T extends Date | RegExp | ((...args: never[]) => unknown)
44
+ ? T
45
+ : T extends object
46
+ ? { [K in keyof T]?: PartialFormData<T[K]> }
47
+ : T
48
+
49
+ type ZenoFormExtras<TFormData> = {
50
+ /**
51
+ * If `true`, shipped fields skip rendering their inline `<FieldError>`
52
+ * message. Fields still flip `data-invalid` / `aria-invalid`.
53
+ */
54
+ hideFieldErrors?: boolean
55
+ /**
56
+ * Show a `*` next to the label of every field the schema treats as required.
57
+ * Defaults to `true`. Required-ness is detected by probing the schema.
58
+ */
59
+ requiredIndicator?: boolean
60
+ /**
61
+ * Warn the user before they navigate away with unsaved changes.
62
+ * `"if-changed"` (or `true`) warns while values differ from defaults;
63
+ * `"if-touched"` warns after any edit.
64
+ */
65
+ unsavedChangesWarning?: boolean | "if-changed" | "if-touched"
66
+ /**
67
+ * Initial values, relaxed to a deep partial so schema-provided defaults can
68
+ * be omitted. See `lib/schema-defaults.ts`.
69
+ */
70
+ defaultValues?: PartialFormData<TFormData>
71
+ }
72
+
73
+ // Schema path — `schema` drives validation. `validators` (if present) selects
74
+ // *when* the schema fires; the matching `validationLogic` is wired internally.
75
+ type SchemaPathExtras<TFormData> = {
76
+ schema: StandardSchema<TFormData>
77
+ validators?: ValidationMode
78
+ validationLogic?: never
79
+ }
80
+
81
+ // Manual path — no schema. `validators`/`validationLogic` keep native types.
82
+ type ManualPathExtras = {
83
+ schema?: never
84
+ }
85
+
86
+ type SchemaFormOptions<
87
+ TFormData,
88
+ TOnMount extends undefined | FormValidateOrFn<TFormData>,
89
+ TOnChange extends undefined | FormValidateOrFn<TFormData>,
90
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
91
+ TOnBlur extends undefined | FormValidateOrFn<TFormData>,
92
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
93
+ TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
94
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
95
+ TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
96
+ TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
97
+ TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
98
+ TSubmitMeta,
99
+ > = Omit<
100
+ FormOptions<
101
+ TFormData,
102
+ TOnMount,
103
+ TOnChange,
104
+ TOnChangeAsync,
105
+ TOnBlur,
106
+ TOnBlurAsync,
107
+ TOnSubmit,
108
+ TOnSubmitAsync,
109
+ TOnDynamic,
110
+ TOnDynamicAsync,
111
+ TOnServer,
112
+ TSubmitMeta
113
+ >,
114
+ "defaultValues" | "validators" | "validationLogic"
115
+ > &
116
+ SchemaPathExtras<TFormData> &
117
+ ZenoFormExtras<TFormData>
118
+
119
+ type ManualFormOptions<
120
+ TFormData,
121
+ TOnMount extends undefined | FormValidateOrFn<TFormData>,
122
+ TOnChange extends undefined | FormValidateOrFn<TFormData>,
123
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
124
+ TOnBlur extends undefined | FormValidateOrFn<TFormData>,
125
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
126
+ TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
127
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
128
+ TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
129
+ TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
130
+ TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
131
+ TSubmitMeta,
132
+ > = Omit<
133
+ FormOptions<
134
+ TFormData,
135
+ TOnMount,
136
+ TOnChange,
137
+ TOnChangeAsync,
138
+ TOnBlur,
139
+ TOnBlurAsync,
140
+ TOnSubmit,
141
+ TOnSubmitAsync,
142
+ TOnDynamic,
143
+ TOnDynamicAsync,
144
+ TOnServer,
145
+ TSubmitMeta
146
+ >,
147
+ "defaultValues"
148
+ > &
149
+ ManualPathExtras &
150
+ ZenoFormExtras<TFormData>
151
+
152
+ type UseFormOptions<
153
+ TFormData,
154
+ TOnMount extends undefined | FormValidateOrFn<TFormData>,
155
+ TOnChange extends undefined | FormValidateOrFn<TFormData>,
156
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
157
+ TOnBlur extends undefined | FormValidateOrFn<TFormData>,
158
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
159
+ TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
160
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
161
+ TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
162
+ TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
163
+ TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
164
+ TSubmitMeta,
165
+ > =
166
+ | SchemaFormOptions<
167
+ TFormData,
168
+ TOnMount,
169
+ TOnChange,
170
+ TOnChangeAsync,
171
+ TOnBlur,
172
+ TOnBlurAsync,
173
+ TOnSubmit,
174
+ TOnSubmitAsync,
175
+ TOnDynamic,
176
+ TOnDynamicAsync,
177
+ TOnServer,
178
+ TSubmitMeta
179
+ >
180
+ | ManualFormOptions<
181
+ TFormData,
182
+ TOnMount,
183
+ TOnChange,
184
+ TOnChangeAsync,
185
+ TOnBlur,
186
+ TOnBlurAsync,
187
+ TOnSubmit,
188
+ TOnSubmitAsync,
189
+ TOnDynamic,
190
+ TOnDynamicAsync,
191
+ TOnServer,
192
+ TSubmitMeta
193
+ >
194
+
195
+ function buildValidatorsFromSchema<TFormData>(
196
+ schema: StandardSchema<TFormData>,
197
+ mode: ValidationMode
198
+ ) {
199
+ switch (mode) {
200
+ case "blur":
201
+ return { onBlur: schema }
202
+ case "submit":
203
+ return { onSubmit: schema }
204
+ default:
205
+ return { onChange: schema }
206
+ }
207
+ }
208
+
209
+ // Chain an internal form-level `onMount` listener after the caller's.
210
+ function withMountListener<L extends { onMount?: unknown } | undefined>(
211
+ listeners: L,
212
+ onMount: (props: { formApi: AnyFormApi }) => void
213
+ ): NonNullable<L> {
214
+ const user = listeners?.onMount as
215
+ | ((props: { formApi: AnyFormApi }) => void)
216
+ | undefined
217
+ return {
218
+ ...listeners,
219
+ onMount: (props: { formApi: AnyFormApi }) => {
220
+ onMount(props)
221
+ user?.(props)
222
+ },
223
+ } as NonNullable<L>
224
+ }
225
+
226
+ // --- typed field wrappers (generic over the injected field components) --------
227
+
228
+ type FormDataOf<F> = F extends { state: { values: infer V } } ? V : never
229
+
230
+ type AnyAppForm = {
231
+ AppField: unknown
232
+ state: { values: unknown }
233
+ }
234
+
235
+ // Loose validators/listeners types for the typed wrappers — name-aware
236
+ // validator typing from TanStack Form would collide with our per-field
237
+ // generics. Callers pass standard schemas or functions; both flow through to
238
+ // `<Field>` at runtime unchanged.
239
+ type AnyFieldValidators = {
240
+ onMount?: unknown
241
+ onChange?: unknown
242
+ onChangeAsync?: unknown
243
+ onChangeAsyncDebounceMs?: number
244
+ onBlur?: unknown
245
+ onBlurAsync?: unknown
246
+ onBlurAsyncDebounceMs?: number
247
+ onSubmit?: unknown
248
+ onSubmitAsync?: unknown
249
+ onSubmitAsyncDebounceMs?: number
250
+ onDynamic?: unknown
251
+ onDynamicAsync?: unknown
252
+ }
253
+
254
+ type AnyFieldListeners = {
255
+ onChange?: unknown
256
+ onChangeDebounceMs?: number
257
+ onBlur?: unknown
258
+ onBlurDebounceMs?: number
259
+ onMount?: unknown
260
+ onSubmit?: unknown
261
+ }
262
+
263
+ type WithValidators = {
264
+ validators?: AnyFieldValidators
265
+ listeners?: AnyFieldListeners
266
+ }
267
+
268
+ // biome-ignore lint/suspicious/noExplicitAny: field components accept arbitrary props.
269
+ type AnyComponent = React.ComponentType<any>
270
+
271
+ type FieldWrappers<FC extends Record<string, AnyComponent>, T> = {
272
+ [K in keyof FC]: <N extends DeepKeys<T>>(
273
+ props: ComponentProps<FC[K]> & { name: N } & WithValidators
274
+ ) => ReactNode
275
+ }
276
+
277
+ type CreateZenoFormConfig<
278
+ FC extends Record<string, AnyComponent>,
279
+ FMC extends Record<string, AnyComponent>,
280
+ > = {
281
+ fieldComponents: FC
282
+ formComponents: FMC
283
+ }
284
+
285
+ /**
286
+ * Build the wired Zeno form API from the caller's field + form components.
287
+ *
288
+ * The shadcn-style field components are dropped into the consumer's repo via the
289
+ * registry and injected here, so `@zeno-lib/forms` (npm) stays UI-free. Returns
290
+ * the `createFormHook` primitives plus the schema-aware `useForm` and the typed
291
+ * `useAppFields` wrappers (prop types are inferred from the injected components).
292
+ */
293
+ export function createZenoForm<
294
+ FC extends Record<string, AnyComponent>,
295
+ FMC extends Record<string, AnyComponent>,
296
+ >(config: CreateZenoFormConfig<FC, FMC>) {
297
+ const { useAppForm, withFieldGroup, withForm } = createFormHook({
298
+ fieldComponents: config.fieldComponents,
299
+ fieldContext,
300
+ formComponents: config.formComponents,
301
+ formContext,
302
+ })
303
+
304
+ function useAppFields<TForm extends AnyAppForm>(form: TForm) {
305
+ type T = FormDataOf<TForm>
306
+ return useMemo(() => {
307
+ const Field = form.AppField as React.ComponentType<{
308
+ name: string
309
+ validators?: AnyFieldValidators
310
+ listeners?: AnyFieldListeners
311
+ children: () => ReactNode
312
+ }>
313
+ const wrappers: Record<string, AnyComponent> = {}
314
+ for (const key of Object.keys(config.fieldComponents)) {
315
+ const Impl = config.fieldComponents[key] as AnyComponent
316
+ wrappers[key] = ({
317
+ name,
318
+ validators,
319
+ listeners,
320
+ ...props
321
+ }: {
322
+ name: string
323
+ validators?: AnyFieldValidators
324
+ listeners?: AnyFieldListeners
325
+ }) => (
326
+ <Field listeners={listeners} name={name} validators={validators}>
327
+ {() => <Impl {...props} />}
328
+ </Field>
329
+ )
330
+ }
331
+ return wrappers as FieldWrappers<FC, T>
332
+ }, [form])
333
+ }
334
+
335
+ function useForm<
336
+ TFormData,
337
+ TOnMount extends undefined | FormValidateOrFn<TFormData> = undefined,
338
+ TOnChange extends undefined | FormValidateOrFn<TFormData> = undefined,
339
+ TOnChangeAsync extends
340
+ | undefined
341
+ | FormAsyncValidateOrFn<TFormData> = undefined,
342
+ TOnBlur extends undefined | FormValidateOrFn<TFormData> = undefined,
343
+ TOnBlurAsync extends
344
+ | undefined
345
+ | FormAsyncValidateOrFn<TFormData> = undefined,
346
+ TOnSubmit extends undefined | FormValidateOrFn<TFormData> = undefined,
347
+ TOnSubmitAsync extends
348
+ | undefined
349
+ | FormAsyncValidateOrFn<TFormData> = undefined,
350
+ TOnDynamic extends undefined | FormValidateOrFn<TFormData> = undefined,
351
+ TOnDynamicAsync extends
352
+ | undefined
353
+ | FormAsyncValidateOrFn<TFormData> = undefined,
354
+ TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
355
+ TSubmitMeta = never,
356
+ >(
357
+ options: UseFormOptions<
358
+ TFormData,
359
+ TOnMount,
360
+ TOnChange,
361
+ TOnChangeAsync,
362
+ TOnBlur,
363
+ TOnBlurAsync,
364
+ TOnSubmit,
365
+ TOnSubmitAsync,
366
+ TOnDynamic,
367
+ TOnDynamicAsync,
368
+ TOnServer,
369
+ TSubmitMeta
370
+ >
371
+ ) {
372
+ type NativeFormOptions = FormOptions<
373
+ TFormData,
374
+ TOnMount,
375
+ TOnChange,
376
+ TOnChangeAsync,
377
+ TOnBlur,
378
+ TOnBlurAsync,
379
+ TOnSubmit,
380
+ TOnSubmitAsync,
381
+ TOnDynamic,
382
+ TOnDynamicAsync,
383
+ TOnServer,
384
+ TSubmitMeta
385
+ >
386
+ type NativeValidators = NativeFormOptions["validators"]
387
+ type NativeValidationLogic = NativeFormOptions["validationLogic"]
388
+
389
+ const {
390
+ schema,
391
+ validators: validatorsInput,
392
+ validationLogic: userValidationLogic,
393
+ hideFieldErrors = false,
394
+ requiredIndicator = true,
395
+ unsavedChangesWarning = false,
396
+ defaultValues: userDefaultValues,
397
+ onSubmit: userOnSubmit,
398
+ ...rest
399
+ } = options as Omit<NativeFormOptions, "defaultValues"> & {
400
+ schema?: StandardSchema<TFormData>
401
+ validators?: ValidationMode | NativeValidators
402
+ validationLogic?: NativeValidationLogic
403
+ defaultValues?: PartialFormData<TFormData>
404
+ hideFieldErrors?: boolean
405
+ requiredIndicator?: boolean
406
+ unsavedChangesWarning?: boolean | "if-changed" | "if-touched"
407
+ }
408
+
409
+ let schemaMode: ValidationMode | undefined
410
+ let resolvedValidators: NativeValidators | undefined
411
+ let resolvedValidationLogic: NativeValidationLogic | undefined
412
+
413
+ if (schema === undefined) {
414
+ schemaMode = undefined
415
+ resolvedValidators =
416
+ typeof validatorsInput === "object" && validatorsInput !== null
417
+ ? (validatorsInput as NativeValidators)
418
+ : undefined
419
+ resolvedValidationLogic = userValidationLogic
420
+ } else {
421
+ const mode: ValidationMode =
422
+ typeof validatorsInput === "string"
423
+ ? (validatorsInput as ValidationMode)
424
+ : DEFAULT_VALIDATION_MODE
425
+ schemaMode = mode
426
+ resolvedValidators = buildValidatorsFromSchema(
427
+ schema,
428
+ mode
429
+ ) as unknown as NativeValidators
430
+ resolvedValidationLogic =
431
+ mode === "blur-then-change"
432
+ ? (blurThenChangeLogic as unknown as NativeValidationLogic)
433
+ : undefined
434
+ }
435
+
436
+ const wrappedOnSubmit = userOnSubmit
437
+ ? ((async (props: { formApi: AnyFormApi; value: TFormData }) => {
438
+ try {
439
+ await (
440
+ userOnSubmit as unknown as (
441
+ p: typeof props
442
+ ) => unknown | Promise<unknown>
443
+ )(props)
444
+ } catch (error) {
445
+ if (error instanceof ValidationError) {
446
+ applyValidationError(props.formApi, error)
447
+ return
448
+ }
449
+ throw error
450
+ }
451
+ }) as typeof userOnSubmit)
452
+ : undefined
453
+
454
+ const requiredFields = useMemo(
455
+ () =>
456
+ schema && requiredIndicator
457
+ ? getRequiredPaths(schema as Parameters<typeof getRequiredPaths>[0])
458
+ : new Set<string>(),
459
+ [schema, requiredIndicator]
460
+ )
461
+
462
+ const schemaDefaults = useMemo(
463
+ () =>
464
+ schema
465
+ ? extractZodDefaults(
466
+ schema as Parameters<typeof extractZodDefaults>[0]
467
+ )
468
+ : undefined,
469
+ [schema]
470
+ )
471
+
472
+ const mergedDefaultValues = useMemo(
473
+ () =>
474
+ schemaDefaults
475
+ ? (deepMergeDefaults(
476
+ schemaDefaults,
477
+ userDefaultValues as Record<string, unknown> | undefined
478
+ ) as typeof userDefaultValues)
479
+ : userDefaultValues,
480
+ [schemaDefaults, userDefaultValues]
481
+ )
482
+
483
+ // Keep `formApi.reset(values)` from being undone by the next render; see
484
+ // `lib/use-rebased-default-values.ts`.
485
+ const rebased = useRebasedDefaultValues(mergedDefaultValues)
486
+ const listeners = withMountListener(rest.listeners, rebased.onMount)
487
+
488
+ const form = useAppForm<
489
+ TFormData,
490
+ TOnMount,
491
+ TOnChange,
492
+ TOnChangeAsync,
493
+ TOnBlur,
494
+ TOnBlurAsync,
495
+ TOnSubmit,
496
+ TOnSubmitAsync,
497
+ TOnDynamic,
498
+ TOnDynamicAsync,
499
+ TOnServer,
500
+ TSubmitMeta
501
+ >({
502
+ ...rest,
503
+ listeners,
504
+ ...(rebased.defaultValues === undefined
505
+ ? {}
506
+ : { defaultValues: rebased.defaultValues }),
507
+ ...(wrappedOnSubmit ? { onSubmit: wrappedOnSubmit } : {}),
508
+ ...(resolvedValidators ? { validators: resolvedValidators } : {}),
509
+ ...(resolvedValidationLogic
510
+ ? { validationLogic: resolvedValidationLogic }
511
+ : {}),
512
+ } as NativeFormOptions)
513
+
514
+ setFormZenoState(form, {
515
+ hideFieldErrors,
516
+ requiredFields,
517
+ requiredIndicator,
518
+ ...(schemaMode === undefined ? {} : { validation: schemaMode }),
519
+ })
520
+
521
+ useUnsavedChangesWarning(form, unsavedChangesWarning)
522
+
523
+ const fields = useAppFields(form)
524
+ return useMemo(
525
+ () => Object.assign(form, fields, config.formComponents),
526
+ [form, fields]
527
+ )
528
+ }
529
+
530
+ return { useAppFields, useAppForm, useForm, withFieldGroup, withForm }
531
+ }
@@ -1,23 +1,22 @@
1
1
  "use client"
2
2
 
3
- import { Checkbox } from "@zeno-lib/ui/checkbox"
3
+ import { describedBy } from "@zeno-lib/forms/lib/aria"
4
+ import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
5
+ import {
6
+ useHideFieldErrors,
7
+ useIsFieldRequired,
8
+ useIsInvalid,
9
+ } from "@zeno-lib/forms/lib/use-is-invalid"
10
+ import type { ComponentProps, ReactNode } from "react"
11
+ import { Checkbox } from "@/components/ui/checkbox"
4
12
  import {
5
13
  Field,
6
14
  FieldContent,
7
15
  FieldDescription,
8
16
  FieldError,
9
17
  FieldLabel,
10
- } from "@zeno-lib/ui/field"
11
- import type { ComponentProps, ReactNode } from "react"
12
-
13
- import { describedBy } from "../lib/aria"
14
- import { useFieldContext } from "../lib/contexts"
18
+ } from "@/components/ui/field"
15
19
  import { RequiredIndicator } from "../lib/required-indicator"
16
- import {
17
- useHideFieldErrors,
18
- useIsFieldRequired,
19
- useIsInvalid,
20
- } from "../lib/use-is-invalid"
21
20
 
22
21
  type CheckboxFieldProps = Omit<
23
22
  ComponentProps<typeof Checkbox>,
@@ -45,7 +44,11 @@ function CheckboxField({
45
44
  const isRequired = required ?? schemaRequired
46
45
 
47
46
  return (
48
- <Field data-invalid={isInvalid} orientation="horizontal">
47
+ <Field
48
+ data-field={field.name}
49
+ data-invalid={isInvalid}
50
+ orientation="horizontal"
51
+ >
49
52
  <Checkbox
50
53
  aria-describedby={describedBy(
51
54
  [description, descriptionId],