@zeno-lib/forms 0.0.1 → 0.2.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 (63) hide show
  1. package/package.json +23 -11
  2. package/src/addons/validation-spinner.tsx +3 -4
  3. package/src/create-form.tsx +59 -0
  4. package/src/create-zeno-form.tsx +531 -0
  5. package/src/fields/checkbox-field.tsx +15 -12
  6. package/src/fields/checkbox-group-field.tsx +160 -0
  7. package/src/fields/combobox-field.tsx +13 -14
  8. package/src/fields/date-picker-field.tsx +18 -15
  9. package/src/fields/index.ts +5 -0
  10. package/src/fields/input-field.tsx +12 -13
  11. package/src/fields/money-field.tsx +123 -0
  12. package/src/fields/multi-select-field.tsx +193 -0
  13. package/src/fields/number-field.tsx +1 -2
  14. package/src/fields/otp-field.tsx +15 -12
  15. package/src/fields/percentage-field.tsx +73 -0
  16. package/src/fields/radio-group-field.tsx +12 -13
  17. package/src/fields/reset-button.tsx +2 -3
  18. package/src/fields/select-field.tsx +12 -13
  19. package/src/fields/slider-field.tsx +11 -12
  20. package/src/fields/submit-button.tsx +3 -4
  21. package/src/fields/switch-field.tsx +15 -12
  22. package/src/fields/textarea-field.tsx +12 -13
  23. package/src/fields/year-field.tsx +56 -0
  24. package/src/form-dialog.test.tsx +161 -0
  25. package/src/form-dialog.tsx +253 -0
  26. package/src/form-element.tsx +51 -0
  27. package/src/formatted-number-fields.test.tsx +236 -0
  28. package/src/index.ts +27 -0
  29. package/src/lib/action-result.ts +61 -0
  30. package/src/lib/apply-validation-error.test.ts +16 -6
  31. package/src/lib/apply-validation-error.ts +38 -6
  32. package/src/lib/formatted-number.test.ts +157 -0
  33. package/src/lib/formatted-number.ts +307 -0
  34. package/src/lib/required-indicator.tsx +1 -1
  35. package/src/lib/schema-defaults.test.ts +1 -2
  36. package/src/lib/schema-required.test.ts +98 -6
  37. package/src/lib/schema-required.ts +126 -22
  38. package/src/lib/submit-action.test-d.ts +83 -0
  39. package/src/lib/submit-action.ts +178 -0
  40. package/src/lib/use-form-dialog.ts +184 -0
  41. package/src/lib/use-formatted-number.ts +235 -0
  42. package/src/lib/use-is-invalid.ts +4 -4
  43. package/src/lib/use-rebased-default-values.ts +42 -0
  44. package/src/lib/use-unsaved-changes-warning.ts +3 -3
  45. package/src/lib/validation-error.test.ts +1 -1
  46. package/src/lib/validation-logic.test.ts +1 -1
  47. package/src/lib/validation-modes.test-d.ts +1 -1
  48. package/src/lib/validation-modes.test.ts +12 -2
  49. package/src/lib/validation-modes.ts +3 -1
  50. package/src/multi-value-fields.test.tsx +116 -0
  51. package/src/submit-action.test.tsx +421 -0
  52. package/src/use-app-fields.test-d.ts +8 -25
  53. package/src/use-app-fields.test.tsx +8 -32
  54. package/src/use-form.cascade.test.tsx +4 -6
  55. package/src/use-form.focus-invalid.test.tsx +198 -0
  56. package/src/use-form.reset-defaults.test.tsx +125 -0
  57. package/src/use-form.test-d.ts +2 -3
  58. package/src/use-form.test.tsx +12 -14
  59. package/src/use-form.unsaved-warning.test.tsx +4 -13
  60. package/src/use-form.validation-modes.test.tsx +10 -8
  61. package/src/form.tsx +0 -105
  62. package/src/use-app-fields.tsx +0 -272
  63. package/src/use-form.tsx +0 -450
@@ -0,0 +1,198 @@
1
+ import {
2
+ cleanup,
3
+ render,
4
+ screen,
5
+ waitFor,
6
+ } from "@zeno-lib/test/testing-library"
7
+ import userEvent from "@zeno-lib/test/user-event"
8
+ import type { ReactNode } from "react"
9
+ import { afterEach, describe, expect, test, vi } from "vitest"
10
+ import { z } from "zod"
11
+ import { Form, FormProvider, RadioGroupFieldItem, useForm } from "./create-form"
12
+
13
+ afterEach(() => {
14
+ cleanup()
15
+ })
16
+
17
+ const schema = z.object({
18
+ email: z.email("Enter a valid email"),
19
+ name: z.string().min(1, "Required"),
20
+ })
21
+
22
+ // Forms in these tests reuse field names (and so ids), so look controls up
23
+ // inside their own form rather than through label association.
24
+ function control(form: string, name: string) {
25
+ return screen
26
+ .getByRole("form", { name: form })
27
+ .querySelector<HTMLElement>(`[name="${name}"]`)
28
+ }
29
+
30
+ function ProfileForm({
31
+ id,
32
+ label,
33
+ onSubmitInvalid,
34
+ outsideSubmit,
35
+ }: {
36
+ id?: string
37
+ label: string
38
+ onSubmitInvalid?: () => void
39
+ outsideSubmit?: boolean
40
+ }) {
41
+ const form = useForm({
42
+ ...(onSubmitInvalid ? { onSubmitInvalid } : {}),
43
+ defaultValues: { email: "valid@example.com" },
44
+ onSubmit: vi.fn(),
45
+ schema,
46
+ })
47
+ const { EmailField, InputField, SubmitButton } = form
48
+ const submit: ReactNode = (
49
+ <SubmitButton form={id}>{`${label} submit`}</SubmitButton>
50
+ )
51
+ return (
52
+ <FormProvider form={form}>
53
+ <Form aria-label={label} id={id}>
54
+ <EmailField name="email" />
55
+ <InputField label="Name" name="name" />
56
+ {outsideSubmit ? null : submit}
57
+ </Form>
58
+ {outsideSubmit ? submit : null}
59
+ </FormProvider>
60
+ )
61
+ }
62
+
63
+ describe("<Form> — focus the first invalid field on a failed submit", () => {
64
+ test("focuses the first invalid field; onSubmitInvalid passes through", async () => {
65
+ const user = userEvent.setup()
66
+ const onSubmitInvalid = vi.fn()
67
+ render(<ProfileForm label="Profile" onSubmitInvalid={onSubmitInvalid} />)
68
+ await user.click(screen.getByRole("button", { name: "Profile submit" }))
69
+ await waitFor(() =>
70
+ expect(document.activeElement).toBe(control("Profile", "name"))
71
+ )
72
+ expect(onSubmitInvalid).toHaveBeenCalledTimes(1)
73
+ })
74
+
75
+ test("only the submitted form's field gets focus", async () => {
76
+ const user = userEvent.setup()
77
+ render(
78
+ <>
79
+ <ProfileForm label="Page" />
80
+ <div role="dialog">
81
+ <ProfileForm label="Dialog" />
82
+ </div>
83
+ </>
84
+ )
85
+ await user.click(screen.getByRole("button", { name: "Page submit" }))
86
+ await waitFor(() =>
87
+ expect(document.activeElement).toBe(control("Page", "name"))
88
+ )
89
+ // The page form is still invalid, but the dialog's submit must stay in
90
+ // the dialog.
91
+ await user.click(screen.getByRole("button", { name: "Dialog submit" }))
92
+ await waitFor(() =>
93
+ expect(document.activeElement).toBe(control("Dialog", "name"))
94
+ )
95
+ })
96
+
97
+ test("works for an external <button form={id}> submit", async () => {
98
+ const user = userEvent.setup()
99
+ render(<ProfileForm id="profile-form" label="Profile" outsideSubmit />)
100
+ await user.click(screen.getByRole("button", { name: "Profile submit" }))
101
+ await waitFor(() =>
102
+ expect(document.activeElement).toBe(control("Profile", "name"))
103
+ )
104
+ })
105
+
106
+ test("a radio group or slider root hands focus to its tabbable child", async () => {
107
+ const user = userEvent.setup()
108
+ function Harness({ sliderFirst }: { sliderFirst: boolean }) {
109
+ const form = useForm({
110
+ defaultValues: { volume: 10 },
111
+ onSubmit: vi.fn(),
112
+ schema: z.object({
113
+ plan: z.enum(["basic", "pro"]),
114
+ volume: z.number().min(50),
115
+ }),
116
+ })
117
+ const { RadioGroupField, SliderField, SubmitButton } = form
118
+ const radio = (
119
+ <RadioGroupField key="plan" label="Plan" name="plan">
120
+ <RadioGroupFieldItem value="basic">Basic</RadioGroupFieldItem>
121
+ <RadioGroupFieldItem value="pro">Pro</RadioGroupFieldItem>
122
+ </RadioGroupField>
123
+ )
124
+ const slider = <SliderField key="volume" label="Volume" name="volume" />
125
+ return (
126
+ <FormProvider form={form}>
127
+ <Form aria-label="Settings">
128
+ {sliderFirst ? [slider, radio] : [radio, slider]}
129
+ <SubmitButton>Save</SubmitButton>
130
+ </Form>
131
+ </FormProvider>
132
+ )
133
+ }
134
+
135
+ const { unmount } = render(<Harness sliderFirst={false} />)
136
+ await user.click(screen.getByRole("button", { name: "Save" }))
137
+ await waitFor(() =>
138
+ expect(document.activeElement?.getAttribute("role")).toBe("radio")
139
+ )
140
+ unmount()
141
+
142
+ render(<Harness sliderFirst />)
143
+ await user.click(screen.getByRole("button", { name: "Save" }))
144
+ await waitFor(() =>
145
+ expect(document.activeElement?.closest('[data-field="volume"]')).not.toBe(
146
+ null
147
+ )
148
+ )
149
+ })
150
+ })
151
+
152
+ describe("fields — data-field and array rows", () => {
153
+ test("every Field root carries data-field={name}; array rows get the required indicator", () => {
154
+ const rowsSchema = z.object({
155
+ email: z.email(),
156
+ members: z.array(
157
+ z.object({
158
+ name: z.string().min(1),
159
+ note: z.string().optional(),
160
+ })
161
+ ),
162
+ })
163
+ function Harness() {
164
+ const form = useForm({
165
+ defaultValues: { members: [{ name: "" }, { name: "" }] },
166
+ onSubmit: vi.fn(),
167
+ schema: rowsSchema,
168
+ })
169
+ const { EmailField, InputField } = form
170
+ return (
171
+ <FormProvider form={form}>
172
+ <Form>
173
+ <EmailField label="Email" name="email" />
174
+ <InputField label="Row 1 name" name="members[0].name" />
175
+ <InputField label="Row 2 name" name="members[1].name" />
176
+ <InputField label="Row 2 note" name="members[1].note" />
177
+ </Form>
178
+ </FormProvider>
179
+ )
180
+ }
181
+ const { container } = render(<Harness />)
182
+ for (const name of [
183
+ "email",
184
+ "members[0].name",
185
+ "members[1].name",
186
+ "members[1].note",
187
+ ]) {
188
+ const root = container.querySelector(`[data-field="${name}"]`)
189
+ expect(root).not.toBeNull()
190
+ expect(root?.getAttribute("data-slot")).toBe("field")
191
+ }
192
+ const indicator = (label: string) =>
193
+ screen.getByText(label).querySelector('[data-slot="required-indicator"]')
194
+ expect(indicator("Row 1 name")).not.toBeNull()
195
+ expect(indicator("Row 2 name")).not.toBeNull()
196
+ expect(indicator("Row 2 note")).toBeNull()
197
+ })
198
+ })
@@ -0,0 +1,125 @@
1
+ import {
2
+ act,
3
+ cleanup,
4
+ render,
5
+ screen,
6
+ waitFor,
7
+ } from "@zeno-lib/test/testing-library"
8
+ import userEvent from "@zeno-lib/test/user-event"
9
+ import { afterEach, describe, expect, test, vi } from "vitest"
10
+ import { z } from "zod"
11
+ import { Form, FormProvider, useForm } from "./create-form"
12
+
13
+ afterEach(() => {
14
+ cleanup()
15
+ })
16
+
17
+ const TITLE = /Title/
18
+ const SAVE = /Save/
19
+
20
+ const schema = z.object({ title: z.string().min(1) })
21
+
22
+ type Captured = {
23
+ isDefaultValue: boolean
24
+ isDirty: boolean
25
+ isTouched: boolean
26
+ values: { title: string }
27
+ }
28
+
29
+ function EditTitle({
30
+ capture,
31
+ record,
32
+ tick,
33
+ }: {
34
+ capture: (state: Captured) => void
35
+ record: { title: string }
36
+ tick?: number
37
+ }) {
38
+ const form = useForm({
39
+ // A fresh literal every render, like most callers write it.
40
+ defaultValues: { title: record.title },
41
+ onSubmit: ({ formApi, value }) => {
42
+ formApi.reset(value)
43
+ },
44
+ schema,
45
+ })
46
+ const { InputField, SubmitButton } = form
47
+ capture({
48
+ isDefaultValue: form.state.isDefaultValue,
49
+ isDirty: form.state.isDirty,
50
+ isTouched: form.state.isTouched,
51
+ values: form.state.values,
52
+ })
53
+ return (
54
+ <FormProvider form={form}>
55
+ <Form data-tick={tick}>
56
+ <InputField label="Title" name="title" />
57
+ <SubmitButton>Save</SubmitButton>
58
+ </Form>
59
+ </FormProvider>
60
+ )
61
+ }
62
+
63
+ function titleInput() {
64
+ return screen.getByLabelText(TITLE) as HTMLInputElement
65
+ }
66
+
67
+ describe("useForm — reset(values) rebases defaults", () => {
68
+ test("reset after submit sticks across rerenders with fixed defaults", async () => {
69
+ const user = userEvent.setup()
70
+ let state: Captured | undefined
71
+ const capture = vi.fn((next: Captured) => {
72
+ state = next
73
+ })
74
+ const record = { title: "Draft" }
75
+ const { rerender } = render(<EditTitle capture={capture} record={record} />)
76
+
77
+ await user.clear(titleInput())
78
+ await user.type(titleInput(), "Saved title")
79
+ await user.click(screen.getByRole("button", { name: SAVE }))
80
+
81
+ await waitFor(() => expect(state?.isTouched).toBe(false))
82
+ for (const tick of [1, 2, 3]) {
83
+ rerender(<EditTitle capture={capture} record={record} tick={tick} />)
84
+ }
85
+ await act(() => Promise.resolve())
86
+
87
+ expect(titleInput().value).toBe("Saved title")
88
+ expect(state?.values).toEqual({ title: "Saved title" })
89
+ expect(state?.isDefaultValue).toBe(true)
90
+ expect(state?.isDirty).toBe(false)
91
+ })
92
+
93
+ test("new caller defaults apply while the form is untouched", async () => {
94
+ let state: Captured | undefined
95
+ const capture = (next: Captured) => {
96
+ state = next
97
+ }
98
+ const { rerender } = render(
99
+ <EditTitle capture={capture} record={{ title: "v1" }} />
100
+ )
101
+ expect(titleInput().value).toBe("v1")
102
+
103
+ rerender(<EditTitle capture={capture} record={{ title: "v2" }} />)
104
+ await waitFor(() => expect(titleInput().value).toBe("v2"))
105
+ expect(state?.isDefaultValue).toBe(true)
106
+ })
107
+
108
+ test("new caller defaults are not applied once the form is touched", async () => {
109
+ const user = userEvent.setup()
110
+ let state: Captured | undefined
111
+ const capture = (next: Captured) => {
112
+ state = next
113
+ }
114
+ const { rerender } = render(
115
+ <EditTitle capture={capture} record={{ title: "v1" }} />
116
+ )
117
+ await user.type(titleInput(), " edited")
118
+
119
+ rerender(<EditTitle capture={capture} record={{ title: "v2" }} />)
120
+ await act(() => Promise.resolve())
121
+
122
+ expect(titleInput().value).toBe("v1 edited")
123
+ expect(state?.values).toEqual({ title: "v1 edited" })
124
+ })
125
+ })
@@ -1,7 +1,6 @@
1
- import { expectTypeOf, test } from "@zeno-lib/vitest"
1
+ import { expectTypeOf, test } from "vitest"
2
2
  import { z } from "zod"
3
-
4
- import { useForm } from "./use-form"
3
+ import { useForm } from "./create-form"
5
4
 
6
5
  const stringSchema = z.object({
7
6
  age: z.number(),
@@ -1,12 +1,10 @@
1
- import { afterEach, describe, expect, test, vi } from "@zeno-lib/vitest"
2
- import { act, cleanup, render, screen } from "@zeno-lib/vitest/testing-library"
3
- import userEvent from "@zeno-lib/vitest/user-event"
1
+ import { act, cleanup, render, screen } from "@zeno-lib/test/testing-library"
2
+ import userEvent from "@zeno-lib/test/user-event"
4
3
  import { useRef } from "react"
4
+ import { afterEach, describe, expect, test, vi } from "vitest"
5
5
  import { z } from "zod"
6
-
7
- import { Form, FormProvider } from "./form"
6
+ import { Form, FormProvider, useForm } from "./create-form"
8
7
  import { ValidationError } from "./lib/validation-error"
9
- import { useForm } from "./use-form"
10
8
 
11
9
  const EMAIL_LABEL = /Email/
12
10
  const VALID_EMAIL_MSG = /Enter a valid email/i
@@ -76,7 +74,7 @@ describe("useForm — required indicator", () => {
76
74
  return (
77
75
  <FormProvider form={form}>
78
76
  <Form>
79
- <EmailField label="Email" />
77
+ <EmailField label="Email" name="email" />
80
78
  <InputField label="Role" name="role" />
81
79
  </Form>
82
80
  </FormProvider>
@@ -105,7 +103,7 @@ describe("useForm — required indicator", () => {
105
103
  return (
106
104
  <FormProvider form={form}>
107
105
  <Form>
108
- <EmailField label="Email" />
106
+ <EmailField label="Email" name="email" />
109
107
  </Form>
110
108
  </FormProvider>
111
109
  )
@@ -134,7 +132,7 @@ describe("useForm — hideFieldErrors", () => {
134
132
  return (
135
133
  <FormProvider form={form}>
136
134
  <Form>
137
- <EmailField label="Email" />
135
+ <EmailField label="Email" name="email" />
138
136
  <SubmitButton>Submit</SubmitButton>
139
137
  </Form>
140
138
  </FormProvider>
@@ -164,7 +162,7 @@ describe("useForm — ValidationError from onSubmit", () => {
164
162
  return (
165
163
  <FormProvider form={form}>
166
164
  <Form>
167
- <EmailField label="Email" />
165
+ <EmailField label="Email" name="email" />
168
166
  <SubmitButton>Submit</SubmitButton>
169
167
  </Form>
170
168
  </FormProvider>
@@ -195,7 +193,7 @@ describe("useForm — ValidationError from onSubmit", () => {
195
193
  return (
196
194
  <FormProvider form={form}>
197
195
  <Form>
198
- <EmailField label="Email" />
196
+ <EmailField label="Email" name="email" />
199
197
  <SubmitButton>Submit</SubmitButton>
200
198
  </Form>
201
199
  </FormProvider>
@@ -224,7 +222,7 @@ describe("useForm — ValidationError from onSubmit", () => {
224
222
  return (
225
223
  <FormProvider form={form}>
226
224
  <Form>
227
- <EmailField label="Email" />
225
+ <EmailField label="Email" name="email" />
228
226
  <SubmitButton>Submit</SubmitButton>
229
227
  </Form>
230
228
  </FormProvider>
@@ -247,7 +245,7 @@ describe("useForm — ValidationError from onSubmit", () => {
247
245
  return (
248
246
  <FormProvider form={form}>
249
247
  <Form>
250
- <EmailField label="Email" />
248
+ <EmailField label="Email" name="email" />
251
249
  <SubmitButton>Submit</SubmitButton>
252
250
  </Form>
253
251
  </FormProvider>
@@ -275,7 +273,7 @@ describe("useForm — SubmitButton state", () => {
275
273
  return (
276
274
  <FormProvider form={form}>
277
275
  <Form>
278
- <EmailField label="Email" />
276
+ <EmailField label="Email" name="email" />
279
277
  <SubmitButton>Submit</SubmitButton>
280
278
  </Form>
281
279
  </FormProvider>
@@ -1,17 +1,8 @@
1
- import {
2
- afterEach,
3
- beforeEach,
4
- describe,
5
- expect,
6
- test,
7
- vi,
8
- } from "@zeno-lib/vitest"
9
- import { act, cleanup, render, screen } from "@zeno-lib/vitest/testing-library"
10
- import userEvent from "@zeno-lib/vitest/user-event"
1
+ import { act, cleanup, render, screen } from "@zeno-lib/test/testing-library"
2
+ import userEvent from "@zeno-lib/test/user-event"
3
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"
11
4
  import { z } from "zod"
12
-
13
- import { Form, FormProvider } from "./form"
14
- import { useForm } from "./use-form"
5
+ import { Form, FormProvider, useForm } from "./create-form"
15
6
 
16
7
  const NAME_LABEL = /Name/
17
8
 
@@ -1,11 +1,9 @@
1
- import { afterEach, describe, expect, test, vi } from "@zeno-lib/vitest"
2
- import { cleanup, render, screen } from "@zeno-lib/vitest/testing-library"
3
- import userEvent from "@zeno-lib/vitest/user-event"
1
+ import { cleanup, render, screen } from "@zeno-lib/test/testing-library"
2
+ import userEvent from "@zeno-lib/test/user-event"
3
+ import { afterEach, describe, expect, test, vi } from "vitest"
4
4
  import { z } from "zod"
5
-
6
- import { Form, FormProvider } from "./form"
5
+ import { Form, FormProvider, useForm } from "./create-form"
7
6
  import type { ValidationMode } from "./lib/validation-modes"
8
- import { useForm } from "./use-form"
9
7
 
10
8
  afterEach(() => {
11
9
  cleanup()
@@ -25,7 +23,7 @@ function Harness({ mode }: { mode?: ValidationMode }) {
25
23
  return (
26
24
  <FormProvider form={form}>
27
25
  <Form>
28
- <EmailField label="Email" />
26
+ <EmailField label="Email" name="email" />
29
27
  <SubmitButton>Submit</SubmitButton>
30
28
  </Form>
31
29
  </FormProvider>
@@ -133,7 +131,11 @@ describe("per-field validators bypass form-level gating", () => {
133
131
  return (
134
132
  <FormProvider form={form}>
135
133
  <Form>
136
- <EmailField label="Email" validators={{ onChange: validator }} />
134
+ <EmailField
135
+ label="Email"
136
+ name="email"
137
+ validators={{ onChange: validator }}
138
+ />
137
139
  </Form>
138
140
  </FormProvider>
139
141
  )
package/src/form.tsx DELETED
@@ -1,105 +0,0 @@
1
- /* biome-ignore-all lint/performance/noBarrelFile: so consumers depend only on `@zeno-lib/forms`. */
2
- "use client"
3
-
4
- import { type AnyFormApi, createFormHook } from "@tanstack/react-form"
5
- import type { ComponentProps, FormEvent, ReactNode } from "react"
6
-
7
- import { CheckboxField } from "./fields/checkbox-field"
8
- import { ComboboxField } from "./fields/combobox-field"
9
- import { DatePickerField } from "./fields/date-picker-field"
10
- import { EmailField } from "./fields/email-field"
11
- import { InputField } from "./fields/input-field"
12
- import { NumberField } from "./fields/number-field"
13
- import { OtpField } from "./fields/otp-field"
14
- import { PasswordField } from "./fields/password-field"
15
- import { RadioGroupField } from "./fields/radio-group-field"
16
- import { ResetButton } from "./fields/reset-button"
17
- import { SelectField } from "./fields/select-field"
18
- import { SliderField } from "./fields/slider-field"
19
- import { SubmitButton } from "./fields/submit-button"
20
- import { SwitchField } from "./fields/switch-field"
21
- import { TextAreaField } from "./fields/textarea-field"
22
- import {
23
- fieldContext,
24
- formContext,
25
- FormProvider as RawFormProvider,
26
- useFormContext,
27
- } from "./lib/contexts"
28
-
29
- export * from "./addons"
30
- export { RadioGroupFieldItem } from "./fields/radio-group-field"
31
- export { applyValidationError } from "./lib/apply-validation-error"
32
- export { useFieldContext, useFormContext } from "./lib/contexts"
33
- export {
34
- RequiredIndicator,
35
- type RequiredIndicatorProps,
36
- } from "./lib/required-indicator"
37
- export {
38
- useHideFieldErrors,
39
- useIsFieldRequired,
40
- useIsInvalid,
41
- } from "./lib/use-is-invalid"
42
- export { type FieldMessage, ValidationError } from "./lib/validation-error"
43
- export { blurThenChangeLogic } from "./lib/validation-logic"
44
- export type { ValidationMode } from "./lib/validation-modes"
45
- export { useAppFields } from "./use-app-fields"
46
- export { useForm } from "./use-form"
47
-
48
- const { useAppForm, withFieldGroup, withForm } = createFormHook({
49
- fieldComponents: {
50
- CheckboxField,
51
- ComboboxField,
52
- DatePickerField,
53
- EmailField,
54
- InputField,
55
- NumberField,
56
- OtpField,
57
- PasswordField,
58
- RadioGroupField,
59
- SelectField,
60
- SliderField,
61
- SwitchField,
62
- TextAreaField,
63
- },
64
- fieldContext,
65
- formComponents: {
66
- ResetButton,
67
- SubmitButton,
68
- },
69
- formContext,
70
- })
71
-
72
- export { useAppForm, withFieldGroup, withForm }
73
-
74
- type FormProviderProps = {
75
- children: ReactNode
76
- form: { handleSubmit: () => unknown }
77
- }
78
-
79
- function FormProvider({ children, form }: FormProviderProps) {
80
- return (
81
- <RawFormProvider value={form as AnyFormApi}>{children}</RawFormProvider>
82
- )
83
- }
84
-
85
- type FormProps = Omit<ComponentProps<"form">, "onSubmit">
86
-
87
- function Form({ children, className, ...props }: FormProps) {
88
- const form = useFormContext()
89
- return (
90
- <form
91
- className={className}
92
- noValidate
93
- onSubmit={(event: FormEvent<HTMLFormElement>) => {
94
- event.preventDefault()
95
- event.stopPropagation()
96
- Promise.resolve(form.handleSubmit()).catch(() => undefined)
97
- }}
98
- {...props}
99
- >
100
- {children}
101
- </form>
102
- )
103
- }
104
-
105
- export { Form, FormProvider }