@zeno-lib/forms 0.5.0 → 0.7.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 (67) hide show
  1. package/dist/create-zeno-form.d.mts +27 -6
  2. package/dist/create-zeno-form.mjs +29 -6
  3. package/dist/form-element.d.mts +16 -3
  4. package/dist/form-element.mjs +38 -18
  5. package/dist/index.d.mts +3 -1
  6. package/dist/index.mjs +3 -1
  7. package/dist/lib/date-value.d.mts +24 -0
  8. package/dist/lib/date-value.mjs +37 -0
  9. package/dist/lib/default-props.d.mts +7 -0
  10. package/dist/lib/default-props.mjs +13 -0
  11. package/dist/lib/empty-value.mjs +25 -1
  12. package/dist/lib/form-disabled.d.mts +28 -0
  13. package/dist/lib/form-disabled.mjs +54 -0
  14. package/dist/lib/item-options.d.mts +45 -0
  15. package/dist/lib/item-options.mjs +70 -0
  16. package/dist/lib/schema-required.mjs +17 -0
  17. package/dist/lib/submit-action.d.mts +8 -2
  18. package/dist/lib/submit-action.mjs +4 -2
  19. package/dist/lib/use-field-control.d.mts +83 -0
  20. package/dist/lib/use-field-control.mjs +70 -0
  21. package/package.json +1 -1
  22. package/src/control-field.test.tsx +338 -0
  23. package/src/create-form.tsx +36 -28
  24. package/src/create-zeno-form.tsx +144 -52
  25. package/src/date-picker-field.test.tsx +108 -0
  26. package/src/default-props.test-d.ts +66 -0
  27. package/src/default-props.test.tsx +165 -0
  28. package/src/fields/checkbox-field.tsx +5 -0
  29. package/src/fields/checkbox-group-field.tsx +6 -2
  30. package/src/fields/combobox-field.tsx +107 -40
  31. package/src/fields/control-field.tsx +57 -0
  32. package/src/fields/date-picker-field.tsx +34 -11
  33. package/src/fields/field-shell.tsx +80 -0
  34. package/src/fields/index.ts +2 -0
  35. package/src/fields/input-field.tsx +16 -48
  36. package/src/fields/multi-select-field.tsx +100 -42
  37. package/src/fields/otp-field.tsx +8 -2
  38. package/src/fields/radio-group-field.tsx +9 -1
  39. package/src/fields/reset-button.tsx +4 -0
  40. package/src/fields/select-field.tsx +9 -1
  41. package/src/fields/slider-field.tsx +9 -1
  42. package/src/fields/submit-button.tsx +3 -1
  43. package/src/fields/switch-field.tsx +5 -0
  44. package/src/fields/textarea-field.tsx +16 -48
  45. package/src/form-dialog.test.tsx +102 -1
  46. package/src/form-dialog.tsx +143 -42
  47. package/src/form-disabled.test.tsx +329 -0
  48. package/src/form-element.tsx +64 -29
  49. package/src/grouped-options.test-d.ts +59 -0
  50. package/src/grouped-options.test.tsx +251 -0
  51. package/src/index.ts +7 -0
  52. package/src/lib/date-value.test.ts +14 -0
  53. package/src/lib/date-value.ts +70 -0
  54. package/src/lib/default-props.ts +19 -0
  55. package/src/lib/empty-value.test.ts +19 -0
  56. package/src/lib/empty-value.ts +45 -1
  57. package/src/lib/form-disabled.ts +95 -0
  58. package/src/lib/item-options.test.ts +11 -0
  59. package/src/lib/item-options.ts +165 -0
  60. package/src/lib/schema-required.test.ts +9 -0
  61. package/src/lib/schema-required.ts +33 -0
  62. package/src/lib/submit-action.ts +18 -4
  63. package/src/lib/use-field-control.ts +191 -0
  64. package/src/submit-action.test.tsx +103 -0
  65. package/src/use-app-fields.test-d.ts +51 -1
  66. package/src/with-form.test-d.ts +76 -0
  67. package/src/with-form.test.tsx +60 -0
@@ -0,0 +1,76 @@
1
+ import { expectTypeOf, test } from "vitest"
2
+ import { z } from "zod"
3
+ import {
4
+ useAppFields,
5
+ useAppForm,
6
+ useForm,
7
+ withFieldGroup,
8
+ withForm,
9
+ } from "./create-form"
10
+
11
+ const schema = z.object({
12
+ address: z.object({ city: z.string(), street: z.string() }),
13
+ email: z.string(),
14
+ })
15
+ type Values = z.infer<typeof schema>
16
+ const VALUES: Values = { address: { city: "", street: "" }, email: "" }
17
+
18
+ // Typed by `defaultValues`, as in TanStack; `render` gets the kit's fields
19
+ // through `useAppFields`.
20
+ const EmailSection = withForm({
21
+ defaultValues: VALUES,
22
+ props: { title: "" },
23
+ render({ form, title }) {
24
+ expectTypeOf(title).toEqualTypeOf<string>()
25
+ expectTypeOf(form.state.values).toEqualTypeOf<Values>()
26
+ // biome-ignore lint/correctness/useHookAtTopLevel: `withForm` runs `render` inside a component; a .ts file can't return JSX to show it.
27
+ const { InputField } = useAppFields(form)
28
+ InputField({ name: "address.city" })
29
+ // @ts-expect-error — not a path of the form
30
+ InputField({ name: "phone" })
31
+ return null
32
+ },
33
+ })
34
+
35
+ const AddressGroup = withFieldGroup({
36
+ defaultValues: VALUES.address,
37
+ render: () => null,
38
+ })
39
+
40
+ test("withForm takes the form useForm returns, with a schema", () => {
41
+ const form = useForm({ onSubmit: () => undefined, schema })
42
+ EmailSection({ form, title: "Email" })
43
+ AddressGroup({ fields: "address", form })
44
+ })
45
+
46
+ test("withForm takes the form useForm returns, with a validation mode", () => {
47
+ const form = useForm({
48
+ onSubmit: () => undefined,
49
+ schema,
50
+ validators: "change",
51
+ })
52
+ EmailSection({ form, title: "Email" })
53
+ })
54
+
55
+ test("withForm takes the form useForm returns, without a schema", () => {
56
+ const form = useForm<Values>({
57
+ defaultValues: VALUES,
58
+ onSubmit: () => undefined,
59
+ })
60
+ EmailSection({ form, title: "Email" })
61
+ AddressGroup({ fields: "address", form })
62
+ })
63
+
64
+ test("withForm still takes the form useAppForm returns", () => {
65
+ const form = useAppForm({ defaultValues: VALUES, onSubmit: () => undefined })
66
+ EmailSection({ form, title: "Email" })
67
+ })
68
+
69
+ test("withForm rejects a form with other values", () => {
70
+ const form = useForm({
71
+ onSubmit: () => undefined,
72
+ schema: z.object({ name: z.string() }),
73
+ })
74
+ // @ts-expect-error — the section needs `email` and `address`
75
+ EmailSection({ form, title: "Email" })
76
+ })
@@ -0,0 +1,60 @@
1
+ import { cleanup, render, screen } from "@zeno-lib/test/testing-library"
2
+ import userEvent from "@zeno-lib/test/user-event"
3
+ import { afterEach, expect, test, vi } from "vitest"
4
+ import { z } from "zod"
5
+ import {
6
+ Form,
7
+ FormProvider,
8
+ useAppFields,
9
+ useForm,
10
+ withForm,
11
+ } from "./create-form"
12
+
13
+ const CITY = /City/
14
+
15
+ afterEach(() => {
16
+ cleanup()
17
+ })
18
+
19
+ const schema = z.object({
20
+ address: z.object({ city: z.string().min(1, "Enter a city") }),
21
+ name: z.string(),
22
+ })
23
+ type Profile = z.infer<typeof schema>
24
+
25
+ const AddressSection = withForm({
26
+ defaultValues: {} as Profile,
27
+ render: ({ form }) => {
28
+ const { InputField } = useAppFields(form)
29
+ return <InputField label="City" name="address.city" />
30
+ },
31
+ })
32
+
33
+ test("a withForm section edits and validates the parent's form", async () => {
34
+ const user = userEvent.setup()
35
+ const onSubmit = vi.fn()
36
+ function Profile() {
37
+ const form = useForm({ onSubmit: ({ value }) => onSubmit(value), schema })
38
+ const { SubmitButton } = form
39
+ return (
40
+ <FormProvider form={form}>
41
+ <Form>
42
+ <AddressSection form={form} />
43
+ <SubmitButton>Save</SubmitButton>
44
+ </Form>
45
+ </FormProvider>
46
+ )
47
+ }
48
+ render(<Profile />)
49
+
50
+ await user.click(screen.getByRole("button", { name: "Save" }))
51
+ expect(await screen.findByText("Enter a city")).toBeTruthy()
52
+ expect(document.activeElement).toBe(screen.getByLabelText(CITY))
53
+
54
+ await user.type(screen.getByLabelText(CITY), "Basel")
55
+ await user.click(screen.getByRole("button", { name: "Save" }))
56
+ expect(onSubmit).toHaveBeenCalledWith({
57
+ address: { city: "Basel" },
58
+ name: "",
59
+ })
60
+ })