@zeno-lib/forms 0.8.0 → 0.9.1

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.
@@ -1,7 +1,7 @@
1
1
  import { FieldControlProps } from "./lib/use-field-control.mjs";
2
2
  import { ValidationMode } from "./lib/validation-modes.mjs";
3
- import { DeepKeys, DeepValue, FormAsyncValidateOrFn, FormOptions, FormValidateOrFn } from "@tanstack/react-form";
4
- import { ComponentProps, ReactNode } from "react";
3
+ import { AppFieldExtendedReactFieldGroupApi, AppFieldExtendedReactFormApi, BaseFormOptions, DeepKeys, DeepValue, FieldsMap, FormAsyncValidateOrFn, FormOptions, FormValidateOrFn, WithFormProps } from "@tanstack/react-form";
4
+ import { ComponentProps, PropsWithChildren, ReactNode } from "react";
5
5
  //#region src/create-zeno-form.d.ts
6
6
  type StandardSchema<T> = {
7
7
  readonly "~standard": {
@@ -122,17 +122,26 @@ type CreateZenoFormConfig<FC extends Record<string, AnyComponent>, FMC extends R
122
122
  */
123
123
  export declare function createZenoForm<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>>(config: CreateZenoFormConfig<FC, FMC>): {
124
124
  useAppFields: <TForm extends AnyAppForm>(form: TForm) => FieldWrappers<FC, FormDataOf<TForm>>;
125
- useAppForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta>(props: FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>;
125
+ useAppForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta>(props: FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>;
126
126
  useForm: <TFormData, TOnMount extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TSubmitMeta = unknown>(options: UseFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").FormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & import("@tanstack/react-form").ReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & NoInfer<FMC> & {
127
127
  AppField: import("@tanstack/react-form").FieldComponent<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, NoInfer<FC>>;
128
- AppForm: import("react").ComponentType<import("react").PropsWithChildren<{}>>;
129
- } & FieldWrappers<FC, FormDataOf<import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>>> & FMC;
130
- withFieldGroup: <TFieldGroupData, TSubmitMeta, TRenderProps extends object = {}>({ render, props, defaultValues }: import("@tanstack/react-form").WithFieldGroupProps<TFieldGroupData, FC, FMC, TSubmitMeta, TRenderProps>) => <TFormData, TFields extends import("@tanstack/react-form").DeepKeysOfType<TFormData, TFieldGroupData | null | undefined> | import("@tanstack/react-form").FieldsMap<TFormData, TFieldGroupData>, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TFormSubmitMeta>(params: import("react").PropsWithChildren<NoInfer<TRenderProps> & {
131
- form: import("@tanstack/react-form").AppFieldExtendedReactFieldGroupApi<unknown, TFormData, string | import("@tanstack/react-form").FieldsMap<unknown, TFormData>, any, any, any, any, any, any, any, any, any, any, unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, FC, FMC> | import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, FC, FMC>;
128
+ AppForm: import("react").ComponentType<PropsWithChildren<{}>>;
129
+ } & FieldWrappers<FC, FormDataOf<AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>>> & FMC;
130
+ withFieldGroup: <TFieldGroupData, TSubmitMeta, TRenderProps extends object = {}>({ render, ...options }: BaseFormOptions<TFieldGroupData, TSubmitMeta> & {
131
+ props?: TRenderProps;
132
+ render: (props: PropsWithChildren<NoInfer<TRenderProps> & {
133
+ group: AppFieldExtendedReactFieldGroupApi<unknown, TFieldGroupData, string | FieldsMap<unknown, TFieldGroupData>, undefined | FormValidateOrFn<unknown>, undefined | FormValidateOrFn<unknown>, undefined | FormAsyncValidateOrFn<unknown>, undefined | FormValidateOrFn<unknown>, undefined | FormAsyncValidateOrFn<unknown>, undefined | FormValidateOrFn<unknown>, undefined | FormAsyncValidateOrFn<unknown>, undefined | FormValidateOrFn<unknown>, undefined | FormAsyncValidateOrFn<unknown>, undefined | FormAsyncValidateOrFn<unknown>, unknown extends TSubmitMeta ? never : TSubmitMeta, FC, FMC & FieldWrappers<FC, TFieldGroupData>>;
134
+ }>) => ReactNode;
135
+ }) => <TFormData, TFields extends import("@tanstack/react-form").DeepKeysOfType<TFormData, TFieldGroupData | null | undefined> | FieldsMap<TFormData, TFieldGroupData>, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TFormSubmitMeta>(params: PropsWithChildren<NoInfer<TRenderProps> & {
136
+ form: AppFieldExtendedReactFieldGroupApi<unknown, TFormData, string | FieldsMap<unknown, TFormData>, any, any, any, any, any, any, any, any, any, any, unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, FC, FMC> | AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, FC, FMC>;
132
137
  fields: TFields;
133
138
  }>) => ReturnType<import("react").FunctionComponent>;
134
- withForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta, TRenderProps extends object = {}>({ render, props }: import("@tanstack/react-form").WithFormProps<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC, TRenderProps>) => import("react").FunctionComponent<import("react").PropsWithChildren<NoInfer<[unknown] extends [TRenderProps] ? any : TRenderProps> & {
135
- form: import("@tanstack/react-form").AppFieldExtendedReactFormApi<[unknown] extends [TFormData] ? any : TFormData, [FormValidateOrFn<TFormData> | undefined] extends [TOnMount] ? [TOnMount] extends [TOnMount & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnMount : TOnMount, [FormValidateOrFn<TFormData> | undefined] extends [TOnChange] ? [TOnChange] extends [TOnChange & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnChange : TOnChange, [FormValidateOrFn<TFormData> | undefined] extends [TOnChangeAsync] ? [TOnChangeAsync] extends [TOnChangeAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnChangeAsync : TOnChangeAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnBlur] ? [TOnBlur] extends [TOnBlur & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnBlur : TOnBlur, [FormValidateOrFn<TFormData> | undefined] extends [TOnBlurAsync] ? [TOnBlurAsync] extends [TOnBlurAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnBlurAsync : TOnBlurAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnSubmit] ? [TOnSubmit] extends [TOnSubmit & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnSubmit : TOnSubmit, [FormValidateOrFn<TFormData> | undefined] extends [TOnSubmitAsync] ? [TOnSubmitAsync] extends [TOnSubmitAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnSubmitAsync : TOnSubmitAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnDynamic] ? [TOnDynamic] extends [TOnDynamic & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnDynamic : TOnDynamic, [FormValidateOrFn<TFormData> | undefined] extends [TOnDynamicAsync] ? [TOnDynamicAsync] extends [TOnDynamicAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnDynamicAsync : TOnDynamicAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnServer] ? [TOnServer] extends [TOnServer & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnServer : TOnServer, [unknown] extends [TSubmitMeta] ? any : TSubmitMeta, [unknown] extends [FC] ? any : FC, [unknown] extends [FMC] ? any : FMC>;
139
+ withForm: <TFormData, TOnMount extends SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData>, TSubmitMeta, TRenderProps extends object = {}>({ render, ...options }: Omit<WithFormProps<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC, TRenderProps>, "render"> & {
140
+ render: (props: PropsWithChildren<NoInfer<TRenderProps> & {
141
+ form: AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC & FieldWrappers<FC, TFormData>>;
142
+ }>) => ReactNode;
143
+ }) => import("react").FunctionComponent<PropsWithChildren<NoInfer<[unknown] extends [TRenderProps] ? any : TRenderProps> & {
144
+ form: AppFieldExtendedReactFormApi<[unknown] extends [TFormData] ? any : TFormData, [FormValidateOrFn<TFormData> | undefined] extends [TOnMount] ? [TOnMount] extends [TOnMount & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnMount : TOnMount, [FormValidateOrFn<TFormData> | undefined] extends [TOnChange] ? [TOnChange] extends [TOnChange & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnChange : TOnChange, [FormValidateOrFn<TFormData> | undefined] extends [TOnChangeAsync] ? [TOnChangeAsync] extends [TOnChangeAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnChangeAsync : TOnChangeAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnBlur] ? [TOnBlur] extends [TOnBlur & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnBlur : TOnBlur, [FormValidateOrFn<TFormData> | undefined] extends [TOnBlurAsync] ? [TOnBlurAsync] extends [TOnBlurAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnBlurAsync : TOnBlurAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnSubmit] ? [TOnSubmit] extends [TOnSubmit & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnSubmit : TOnSubmit, [FormValidateOrFn<TFormData> | undefined] extends [TOnSubmitAsync] ? [TOnSubmitAsync] extends [TOnSubmitAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnSubmitAsync : TOnSubmitAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnDynamic] ? [TOnDynamic] extends [TOnDynamic & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnDynamic : TOnDynamic, [FormValidateOrFn<TFormData> | undefined] extends [TOnDynamicAsync] ? [TOnDynamicAsync] extends [TOnDynamicAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnDynamicAsync : TOnDynamicAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnServer] ? [TOnServer] extends [TOnServer & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnServer : TOnServer, [unknown] extends [TSubmitMeta] ? any : TSubmitMeta, [unknown] extends [FC] ? any : FC, [unknown] extends [FMC] ? any : FMC>;
136
145
  }>>;
137
146
  };
138
147
  //#endregion
@@ -62,27 +62,63 @@ function withComponentDefaults(components, defaultProps) {
62
62
  function createZenoForm(config) {
63
63
  const fieldComponents = withComponentDefaults(config.fieldComponents, config.defaultProps);
64
64
  const formComponents = withComponentDefaults(config.formComponents, config.defaultProps);
65
- const { useAppForm, withFieldGroup, withForm } = createFormHook({
65
+ const { useAppForm, withFieldGroup: withTanStackFieldGroup, withForm: withTanStackForm } = createFormHook({
66
66
  fieldComponents,
67
67
  fieldContext,
68
68
  formComponents,
69
69
  formContext
70
70
  });
71
+ const fieldsByForm = /* @__PURE__ */ new WeakMap();
72
+ function getAppFields(form) {
73
+ const cached = fieldsByForm.get(form);
74
+ if (cached !== void 0) return cached;
75
+ const Field = form.AppField;
76
+ const wrappers = {};
77
+ for (const key of Object.keys(fieldComponents)) {
78
+ const Impl = fieldComponents[key];
79
+ wrappers[key] = ({ name, validators, listeners, ...props }) => /* @__PURE__ */ jsx(Field, {
80
+ listeners,
81
+ name,
82
+ validators,
83
+ children: () => /* @__PURE__ */ jsx(Impl, { ...props })
84
+ });
85
+ }
86
+ fieldsByForm.set(form, wrappers);
87
+ return wrappers;
88
+ }
71
89
  function useAppFields(form) {
72
- return useMemo(() => {
73
- const Field = form.AppField;
74
- const wrappers = {};
75
- for (const key of Object.keys(fieldComponents)) {
76
- const Impl = fieldComponents[key];
77
- wrappers[key] = ({ name, validators, listeners, ...props }) => /* @__PURE__ */ jsx(Field, {
78
- listeners,
79
- name,
80
- validators,
81
- children: () => /* @__PURE__ */ jsx(Impl, { ...props })
82
- });
83
- }
84
- return wrappers;
85
- }, [form]);
90
+ return getAppFields(form);
91
+ }
92
+ function withNamedFields(target) {
93
+ for (const [name, field] of Object.entries(getAppFields(target))) if (!(name in target)) Object.assign(target, { [name]: field });
94
+ return target;
95
+ }
96
+ /**
97
+ * TanStack's `withForm`, whose `render` gets a `form` carrying the typed
98
+ * fields, as `useForm`'s does: `<form.InputField name="email" />`.
99
+ */
100
+ function withForm({ render, ...options }) {
101
+ return withTanStackForm({
102
+ ...options,
103
+ render: (props) => render({
104
+ ...props,
105
+ form: withNamedFields(props.form)
106
+ })
107
+ });
108
+ }
109
+ /**
110
+ * TanStack's `withFieldGroup`, whose `render` gets a `group` carrying the
111
+ * typed fields of the group's own values, as `useForm`'s form does:
112
+ * `<group.InputField name="street" />`.
113
+ */
114
+ function withFieldGroup({ render, ...options }) {
115
+ return withTanStackFieldGroup({
116
+ ...options,
117
+ render: (props) => render({
118
+ ...props,
119
+ group: withNamedFields(props.group)
120
+ })
121
+ });
86
122
  }
87
123
  function useForm(options) {
88
124
  const { schema, validators: validatorsInput, validationLogic: userValidationLogic, disabled = false, hideFieldErrors = false, requiredIndicator = true, unsavedChangesWarning = false, defaultValues: userDefaultValues, onSubmit: userOnSubmit, ...rest } = options;
@@ -145,8 +181,7 @@ function createZenoForm(config) {
145
181
  });
146
182
  useDisabledOption(form, disabled);
147
183
  useUnsavedChangesWarning(form, unsavedChangesWarning);
148
- const fields = useAppFields(form);
149
- return useMemo(() => Object.assign(form, fields, formComponents), [form, fields]);
184
+ return useMemo(() => Object.assign(form, getAppFields(form), formComponents), [form]);
150
185
  }
151
186
  return {
152
187
  useAppFields,
@@ -1,14 +1,15 @@
1
1
  "use client";
2
2
  import { evaluate } from "@tanstack/react-form";
3
- import { useCallback, useRef } from "react";
3
+ import { useCallback, useLayoutEffect, useRef } from "react";
4
4
  //#region src/lib/use-rebased-default-values.ts
5
5
  function useRebasedDefaultValues(callerDefaults) {
6
6
  const liveForm = useRef(null);
7
7
  const lastCallerDefaults = useRef(callerDefaults);
8
8
  let defaultValues = callerDefaults;
9
- if (evaluate(lastCallerDefaults.current, callerDefaults)) {
10
- if (liveForm.current) defaultValues = liveForm.current.options.defaultValues;
11
- } else lastCallerDefaults.current = callerDefaults;
9
+ if (liveForm.current && evaluate(lastCallerDefaults.current, callerDefaults)) defaultValues = liveForm.current.options.defaultValues;
10
+ useLayoutEffect(() => {
11
+ lastCallerDefaults.current = callerDefaults;
12
+ }, [callerDefaults]);
12
13
  const onMount = useCallback((props) => {
13
14
  liveForm.current = props.formApi;
14
15
  }, []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeno-lib/forms",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "repository": {
5
5
  "url": "https://github.com/zeno-lib/zeno.git",
6
6
  "directory": "packages/forms"
@@ -0,0 +1,140 @@
1
+ import { cleanup, render, screen, within } 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
+ import { z } from "zod"
5
+ import {
6
+ Form,
7
+ FormProvider,
8
+ useAppForm,
9
+ useForm,
10
+ withFieldGroup,
11
+ withForm,
12
+ } from "./create-form"
13
+
14
+ const STREET_LABEL = /Street/
15
+ const CITY_LABEL = /City/
16
+ const BILLING_STREET_LABEL = /Billing street/
17
+
18
+ afterEach(() => {
19
+ cleanup()
20
+ })
21
+
22
+ const address = z.object({ city: z.string(), street: z.string() })
23
+ const schema = z.object({ billing: address, shipping: address })
24
+ type Values = z.infer<typeof schema>
25
+ const VALUES: Values = {
26
+ billing: { city: "", street: "" },
27
+ shipping: { city: "", street: "" },
28
+ }
29
+
30
+ const AddressGroup = withFieldGroup({
31
+ defaultValues: VALUES.billing,
32
+ props: { title: "" },
33
+ render: ({ group, title }) => (
34
+ <fieldset aria-label={title}>
35
+ <group.InputField label="Street" name="street" />
36
+ <group.InputField label="City" name="city" />
37
+ </fieldset>
38
+ ),
39
+ })
40
+
41
+ describe("withFieldGroup", () => {
42
+ test("a group's named field binds under the group's path", async () => {
43
+ const user = userEvent.setup()
44
+ let current: { state: { values: Values } } | undefined
45
+ function H() {
46
+ const form = useForm({ onSubmit: vi.fn(), schema })
47
+ current = form
48
+ return (
49
+ <FormProvider form={form}>
50
+ <Form>
51
+ <AddressGroup fields="billing" form={form} title="Billing" />
52
+ <AddressGroup fields="shipping" form={form} title="Shipping" />
53
+ </Form>
54
+ </FormProvider>
55
+ )
56
+ }
57
+ render(<H />)
58
+ const shipping = within(screen.getByRole("group", { name: "Shipping" }))
59
+ const street = shipping.getByLabelText(STREET_LABEL) as HTMLInputElement
60
+ expect(street.name).toBe("shipping.street")
61
+ await user.type(street, "Main St")
62
+ expect(current?.state.values.shipping.street).toBe("Main St")
63
+ expect(current?.state.values.billing.street).toBe("")
64
+ })
65
+
66
+ test("a group nested in a group binds under both paths", async () => {
67
+ const user = userEvent.setup()
68
+ const Nested = withFieldGroup({
69
+ defaultValues: { billing: VALUES.billing },
70
+ render: ({ group }) => (
71
+ <AddressGroup fields="billing" form={group} title="Nested" />
72
+ ),
73
+ })
74
+ const nestedSchema = z.object({ order: z.object({ billing: address }) })
75
+ let current: { state: { values: z.infer<typeof nestedSchema> } } | undefined
76
+ function H() {
77
+ const form = useForm({ onSubmit: vi.fn(), schema: nestedSchema })
78
+ current = form
79
+ return (
80
+ <FormProvider form={form}>
81
+ <Form>
82
+ <Nested fields="order" form={form} />
83
+ </Form>
84
+ </FormProvider>
85
+ )
86
+ }
87
+ render(<H />)
88
+ const city = screen.getByLabelText(CITY_LABEL) as HTMLInputElement
89
+ expect(city.name).toBe("order.billing.city")
90
+ await user.type(city, "Bern")
91
+ expect(current?.state.values.order.billing.city).toBe("Bern")
92
+ })
93
+ })
94
+
95
+ describe("withForm", () => {
96
+ const BillingSection = withForm({
97
+ defaultValues: VALUES,
98
+ render: ({ form }) => (
99
+ <form.InputField label="Billing street" name="billing.street" />
100
+ ),
101
+ })
102
+
103
+ test("a section's form has the fields when useAppForm made it", async () => {
104
+ const user = userEvent.setup()
105
+ let current: { state: { values: Values } } | undefined
106
+ function H() {
107
+ const form = useAppForm({ defaultValues: VALUES, onSubmit: vi.fn() })
108
+ current = form
109
+ return (
110
+ <FormProvider form={form}>
111
+ <Form>
112
+ <BillingSection form={form} />
113
+ </Form>
114
+ </FormProvider>
115
+ )
116
+ }
117
+ render(<H />)
118
+ await user.type(screen.getByLabelText(BILLING_STREET_LABEL), "Main St")
119
+ expect(current?.state.values.billing.street).toBe("Main St")
120
+ })
121
+
122
+ test("a section keeps the fields of useForm's form as they are", () => {
123
+ const seen: unknown[] = []
124
+ function H() {
125
+ const form = useForm({ onSubmit: vi.fn(), schema })
126
+ seen.push(form.InputField)
127
+ return (
128
+ <FormProvider form={form}>
129
+ <Form>
130
+ <BillingSection form={form} />
131
+ </Form>
132
+ </FormProvider>
133
+ )
134
+ }
135
+ const { rerender } = render(<H />)
136
+ rerender(<H />)
137
+ expect(seen).toHaveLength(2)
138
+ expect(seen[1]).toBe(seen[0])
139
+ })
140
+ })
@@ -3,14 +3,24 @@
3
3
 
4
4
  import type {
5
5
  AnyFormApi,
6
+ AppFieldExtendedReactFieldGroupApi,
7
+ AppFieldExtendedReactFormApi,
8
+ BaseFormOptions,
6
9
  DeepKeys,
7
10
  DeepValue,
11
+ FieldsMap,
8
12
  FormAsyncValidateOrFn,
9
13
  FormOptions,
10
14
  FormValidateOrFn,
15
+ WithFormProps,
11
16
  } from "@tanstack/react-form"
12
17
  import { createFormHook } from "@tanstack/react-form"
13
- import { type ComponentProps, type ReactNode, useMemo } from "react"
18
+ import {
19
+ type ComponentProps,
20
+ type PropsWithChildren,
21
+ type ReactNode,
22
+ useMemo,
23
+ } from "react"
14
24
  import { applyValidationError } from "./lib/apply-validation-error"
15
25
  import { fieldContext, formContext } from "./lib/contexts"
16
26
  import { withDefaultProps } from "./lib/default-props"
@@ -394,42 +404,211 @@ export function createZenoForm<
394
404
  config.formComponents,
395
405
  config.defaultProps
396
406
  )
397
- const { useAppForm, withFieldGroup, withForm } = createFormHook({
407
+ const {
408
+ useAppForm,
409
+ withFieldGroup: withTanStackFieldGroup,
410
+ withForm: withTanStackForm,
411
+ } = createFormHook({
398
412
  fieldComponents,
399
413
  fieldContext,
400
414
  formComponents,
401
415
  formContext,
402
416
  })
403
417
 
404
- function useAppFields<TForm extends AnyAppForm>(form: TForm) {
405
- type T = FormDataOf<TForm>
406
- return useMemo(() => {
407
- const Field = form.AppField as React.ComponentType<{
418
+ // One set of named fields per form, shared by `useForm`, `useAppFields` and
419
+ // every section and group that renders the form.
420
+ const fieldsByForm = new WeakMap<object, Record<string, AnyComponent>>()
421
+
422
+ function getAppFields<TForm extends AnyAppForm>(form: TForm) {
423
+ type Fields = FieldWrappers<FC, FormDataOf<TForm>>
424
+ const cached = fieldsByForm.get(form)
425
+ if (cached !== undefined) {
426
+ return cached as Fields
427
+ }
428
+ const Field = form.AppField as React.ComponentType<{
429
+ name: string
430
+ validators?: AnyFieldValidators
431
+ listeners?: AnyFieldListeners
432
+ children: () => ReactNode
433
+ }>
434
+ const wrappers: Record<string, AnyComponent> = {}
435
+ for (const key of Object.keys(fieldComponents)) {
436
+ const Impl = fieldComponents[key] as AnyComponent
437
+ wrappers[key] = ({
438
+ name,
439
+ validators,
440
+ listeners,
441
+ ...props
442
+ }: {
408
443
  name: string
409
444
  validators?: AnyFieldValidators
410
445
  listeners?: AnyFieldListeners
411
- children: () => ReactNode
412
- }>
413
- const wrappers: Record<string, AnyComponent> = {}
414
- for (const key of Object.keys(fieldComponents)) {
415
- const Impl = fieldComponents[key] as AnyComponent
416
- wrappers[key] = ({
417
- name,
418
- validators,
419
- listeners,
420
- ...props
421
- }: {
422
- name: string
423
- validators?: AnyFieldValidators
424
- listeners?: AnyFieldListeners
425
- }) => (
426
- <Field listeners={listeners} name={name} validators={validators}>
427
- {() => <Impl {...props} />}
428
- </Field>
429
- )
446
+ }) => (
447
+ <Field listeners={listeners} name={name} validators={validators}>
448
+ {() => <Impl {...props} />}
449
+ </Field>
450
+ )
451
+ }
452
+ fieldsByForm.set(form, wrappers)
453
+ return wrappers as Fields
454
+ }
455
+
456
+ function useAppFields<TForm extends AnyAppForm>(form: TForm) {
457
+ return getAppFields(form)
458
+ }
459
+
460
+ // Puts the named fields on `target` (a section's form, a group) where it has
461
+ // none of that name. The same fields every time, so running it on each
462
+ // render changes nothing after the first.
463
+ function withNamedFields<T extends AnyAppForm>(target: T): T {
464
+ for (const [name, field] of Object.entries(getAppFields(target))) {
465
+ if (!(name in target)) {
466
+ Object.assign(target, { [name]: field })
430
467
  }
431
- return wrappers as FieldWrappers<FC, T>
432
- }, [form])
468
+ }
469
+ return target
470
+ }
471
+
472
+ /**
473
+ * TanStack's `withForm`, whose `render` gets a `form` carrying the typed
474
+ * fields, as `useForm`'s does: `<form.InputField name="email" />`.
475
+ */
476
+ function withForm<
477
+ TFormData,
478
+ TOnMount extends SyncValidator<TFormData>,
479
+ TOnChange extends SyncValidator<TFormData>,
480
+ TOnChangeAsync extends AsyncValidator<TFormData>,
481
+ TOnBlur extends SyncValidator<TFormData>,
482
+ TOnBlurAsync extends AsyncValidator<TFormData>,
483
+ TOnSubmit extends SyncValidator<TFormData>,
484
+ TOnSubmitAsync extends AsyncValidator<TFormData>,
485
+ TOnDynamic extends SyncValidator<TFormData>,
486
+ TOnDynamicAsync extends AsyncValidator<TFormData>,
487
+ TOnServer extends AsyncValidator<TFormData>,
488
+ TSubmitMeta,
489
+ // biome-ignore lint/complexity/noBannedTypes: TanStack's own default, so a section without `props` infers as it does.
490
+ TRenderProps extends object = {},
491
+ >({
492
+ render,
493
+ ...options
494
+ }: Omit<
495
+ WithFormProps<
496
+ TFormData,
497
+ TOnMount,
498
+ TOnChange,
499
+ TOnChangeAsync,
500
+ TOnBlur,
501
+ TOnBlurAsync,
502
+ TOnSubmit,
503
+ TOnSubmitAsync,
504
+ TOnDynamic,
505
+ TOnDynamicAsync,
506
+ TOnServer,
507
+ TSubmitMeta,
508
+ FC,
509
+ FMC,
510
+ TRenderProps
511
+ >,
512
+ "render"
513
+ > & {
514
+ render: (
515
+ props: PropsWithChildren<
516
+ NoInfer<TRenderProps> & {
517
+ // The named fields ride in the form components' slot, so the type
518
+ // stays TanStack's and a nested `withFieldGroup` still infers it.
519
+ form: AppFieldExtendedReactFormApi<
520
+ TFormData,
521
+ TOnMount,
522
+ TOnChange,
523
+ TOnChangeAsync,
524
+ TOnBlur,
525
+ TOnBlurAsync,
526
+ TOnSubmit,
527
+ TOnSubmitAsync,
528
+ TOnDynamic,
529
+ TOnDynamicAsync,
530
+ TOnServer,
531
+ TSubmitMeta,
532
+ FC,
533
+ FMC & FieldWrappers<FC, TFormData>
534
+ >
535
+ }
536
+ >
537
+ ) => ReactNode
538
+ }) {
539
+ return withTanStackForm<
540
+ TFormData,
541
+ TOnMount,
542
+ TOnChange,
543
+ TOnChangeAsync,
544
+ TOnBlur,
545
+ TOnBlurAsync,
546
+ TOnSubmit,
547
+ TOnSubmitAsync,
548
+ TOnDynamic,
549
+ TOnDynamicAsync,
550
+ TOnServer,
551
+ TSubmitMeta,
552
+ TRenderProps
553
+ >({
554
+ ...options,
555
+ render: (props) =>
556
+ render({
557
+ ...props,
558
+ form: withNamedFields(props.form),
559
+ } as Parameters<typeof render>[0]),
560
+ })
561
+ }
562
+
563
+ /**
564
+ * TanStack's `withFieldGroup`, whose `render` gets a `group` carrying the
565
+ * typed fields of the group's own values, as `useForm`'s form does:
566
+ * `<group.InputField name="street" />`.
567
+ */
568
+ function withFieldGroup<
569
+ TFieldGroupData,
570
+ TSubmitMeta,
571
+ // biome-ignore lint/complexity/noBannedTypes: TanStack's own default, so a group without `props` infers as it does.
572
+ TRenderProps extends object = {},
573
+ >({
574
+ render,
575
+ ...options
576
+ }: BaseFormOptions<TFieldGroupData, TSubmitMeta> & {
577
+ props?: TRenderProps
578
+ render: (
579
+ props: PropsWithChildren<
580
+ NoInfer<TRenderProps> & {
581
+ // As `withForm`'s, with TanStack's own arguments for the rest.
582
+ group: AppFieldExtendedReactFieldGroupApi<
583
+ unknown,
584
+ TFieldGroupData,
585
+ string | FieldsMap<unknown, TFieldGroupData>,
586
+ undefined | FormValidateOrFn<unknown>,
587
+ undefined | FormValidateOrFn<unknown>,
588
+ undefined | FormAsyncValidateOrFn<unknown>,
589
+ undefined | FormValidateOrFn<unknown>,
590
+ undefined | FormAsyncValidateOrFn<unknown>,
591
+ undefined | FormValidateOrFn<unknown>,
592
+ undefined | FormAsyncValidateOrFn<unknown>,
593
+ undefined | FormValidateOrFn<unknown>,
594
+ undefined | FormAsyncValidateOrFn<unknown>,
595
+ undefined | FormAsyncValidateOrFn<unknown>,
596
+ unknown extends TSubmitMeta ? never : TSubmitMeta,
597
+ FC,
598
+ FMC & FieldWrappers<FC, TFieldGroupData>
599
+ >
600
+ }
601
+ >
602
+ ) => ReactNode
603
+ }) {
604
+ return withTanStackFieldGroup<TFieldGroupData, TSubmitMeta, TRenderProps>({
605
+ ...options,
606
+ render: (props) =>
607
+ render({
608
+ ...props,
609
+ group: withNamedFields(props.group),
610
+ } as Parameters<typeof render>[0]),
611
+ })
433
612
  }
434
613
 
435
614
  // An unset generic defaults to its constraint, which is what TanStack's own
@@ -647,10 +826,9 @@ export function createZenoForm<
647
826
  useDisabledOption(form, disabled)
648
827
  useUnsavedChangesWarning(form, unsavedChangesWarning)
649
828
 
650
- const fields = useAppFields(form)
651
829
  return useMemo(
652
- () => Object.assign(form, fields, formComponents),
653
- [form, fields]
830
+ () => Object.assign(form, getAppFields(form), formComponents),
831
+ [form]
654
832
  )
655
833
  }
656
834
 
@@ -15,7 +15,7 @@ afterEach(() => {
15
15
  vi.unstubAllGlobals()
16
16
  })
17
17
 
18
- const { useForm } = createZenoForm({
18
+ const { useAppForm, useForm, withFieldGroup, withForm } = createZenoForm({
19
19
  defaultProps: {
20
20
  FormError: { submitErrorMessage: "Not saved. Try again later." },
21
21
  MoneyField: { currency: "CHF", currencyDisplay: "code", placeholder: "0" },
@@ -162,4 +162,28 @@ describe("createZenoForm({ defaultProps })", () => {
162
162
  render(<AppFieldHarness />)
163
163
  expect(addonOf("Amount")).toBe("CHF")
164
164
  })
165
+
166
+ test("a section's and a group's fields get their defaults too", () => {
167
+ const AmountSection = withForm({
168
+ defaultValues: VALUES,
169
+ render: ({ form }) => <form.MoneyField label="Amount" name="amount" />,
170
+ })
171
+ const FeeGroup = withFieldGroup({
172
+ defaultValues: { fee: VALUES.fee },
173
+ render: ({ group }) => <group.MoneyField label="Fee" name="fee" />,
174
+ })
175
+ function SectionHarness() {
176
+ // `useAppForm`'s form has no named fields until a section renders it.
177
+ const form = useAppForm({ defaultValues: VALUES, onSubmit: vi.fn() })
178
+ return (
179
+ <FormProvider form={form}>
180
+ <AmountSection form={form} />
181
+ <FeeGroup fields={{ fee: "fee" }} form={form} />
182
+ </FormProvider>
183
+ )
184
+ }
185
+ render(<SectionHarness />)
186
+ expect(addonOf("Amount")).toBe("CHF")
187
+ expect(addonOf("Fee")).toBe("CHF")
188
+ })
165
189
  })
@@ -7,7 +7,7 @@ import {
7
7
  within,
8
8
  } from "@zeno-lib/test/testing-library"
9
9
  import userEvent from "@zeno-lib/test/user-event"
10
- import { type ReactNode, useState } from "react"
10
+ import { type ReactNode, StrictMode, useState } from "react"
11
11
  import { afterEach, describe, expect, onTestFinished, test, vi } from "vitest"
12
12
  import { z } from "zod"
13
13
  import { useForm } from "./create-form"
@@ -178,6 +178,13 @@ describe("FormDialog", () => {
178
178
  expect(name.value).toBe("Ada")
179
179
  })
180
180
 
181
+ test("opens with per-session defaults under Strict Mode", async () => {
182
+ const user = userEvent.setup()
183
+ render(<Harness />, { wrapper: StrictMode })
184
+ const name = await openEdit(user)
185
+ expect(name.value).toBe("Ada")
186
+ })
187
+
181
188
  test("an edit opened on a record with keys the schema lacks saves to a .strict() schema", async () => {
182
189
  const user = userEvent.setup()
183
190
  const strictSchema = schema.strict()
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import { type AnyFormApi, evaluate } from "@tanstack/react-form"
4
- import { useCallback, useRef } from "react"
4
+ import { useCallback, useLayoutEffect, useRef } from "react"
5
5
 
6
6
  // TanStack's `useForm` calls `formApi.update(options)` after every render. While
7
7
  // the form is untouched, `update` re-applies `options.defaultValues` whenever it
@@ -24,14 +24,19 @@ function useRebasedDefaultValues<T>(callerDefaults: T): {
24
24
  const lastCallerDefaults = useRef(callerDefaults)
25
25
 
26
26
  let defaultValues = callerDefaults
27
- if (evaluate(lastCallerDefaults.current, callerDefaults)) {
28
- if (liveForm.current) {
29
- defaultValues = liveForm.current.options.defaultValues as T
30
- }
31
- } else {
32
- lastCallerDefaults.current = callerDefaults
27
+ if (
28
+ liveForm.current &&
29
+ evaluate(lastCallerDefaults.current, callerDefaults)
30
+ ) {
31
+ defaultValues = liveForm.current.options.defaultValues as T
33
32
  }
34
33
 
34
+ // Write on commit, not during render: under Strict Mode the second render
35
+ // would see the new defaults as unchanged and forward the old ones.
36
+ useLayoutEffect(() => {
37
+ lastCallerDefaults.current = callerDefaults
38
+ }, [callerDefaults])
39
+
35
40
  const onMount = useCallback((props: { formApi: AnyFormApi }) => {
36
41
  liveForm.current = props.formApi
37
42
  }, [])
@@ -65,30 +65,35 @@ function titleInput() {
65
65
  }
66
66
 
67
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} />)
68
+ for (const reactStrictMode of [false, true]) {
69
+ test(`reset after submit sticks across rerenders with fixed defaults (reactStrictMode: ${reactStrictMode})`, async () => {
70
+ const user = userEvent.setup()
71
+ let state: Captured | undefined
72
+ const capture = vi.fn((next: Captured) => {
73
+ state = next
74
+ })
75
+ const record = { title: "Draft" }
76
+ const { rerender } = render(
77
+ <EditTitle capture={capture} record={record} />,
78
+ { reactStrictMode }
79
+ )
76
80
 
77
- await user.clear(titleInput())
78
- await user.type(titleInput(), "Saved title")
79
- await user.click(screen.getByRole("button", { name: SAVE }))
81
+ await user.clear(titleInput())
82
+ await user.type(titleInput(), "Saved title")
83
+ await user.click(screen.getByRole("button", { name: SAVE }))
80
84
 
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())
85
+ await waitFor(() => expect(state?.isTouched).toBe(false))
86
+ for (const tick of [1, 2, 3]) {
87
+ rerender(<EditTitle capture={capture} record={record} tick={tick} />)
88
+ }
89
+ await act(() => Promise.resolve())
86
90
 
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
- })
91
+ expect(titleInput().value).toBe("Saved title")
92
+ expect(state?.values).toEqual({ title: "Saved title" })
93
+ expect(state?.isDefaultValue).toBe(true)
94
+ expect(state?.isDirty).toBe(false)
95
+ })
96
+ }
92
97
 
93
98
  test("new caller defaults apply while the form is untouched", async () => {
94
99
  let state: Captured | undefined
@@ -37,6 +37,31 @@ const AddressGroup = withFieldGroup({
37
37
  render: () => null,
38
38
  })
39
39
 
40
+ // A section's `form` and a group's `group` carry the fields themselves.
41
+ withForm({
42
+ defaultValues: VALUES,
43
+ render: ({ form }) => {
44
+ form.InputField({ name: "address.city" })
45
+ // @ts-expect-error — not a path of the form
46
+ form.InputField({ name: "phone" })
47
+ return null
48
+ },
49
+ })
50
+
51
+ withFieldGroup({
52
+ defaultValues: VALUES.address,
53
+ props: { label: "" },
54
+ render: ({ group, label }) => {
55
+ expectTypeOf(label).toEqualTypeOf<string>()
56
+ expectTypeOf(group.state.values).toEqualTypeOf<Values["address"]>()
57
+ const { InputField } = group
58
+ InputField({ name: "street" })
59
+ // @ts-expect-error — a path of the form, not of the group
60
+ InputField({ name: "address.street" })
61
+ return null
62
+ },
63
+ })
64
+
40
65
  test("withForm takes the form useForm returns, with a schema", () => {
41
66
  const form = useForm({ onSubmit: () => undefined, schema })
42
67
  EmailSection({ form, title: "Email" })
@@ -2,13 +2,7 @@ import { cleanup, render, screen } from "@zeno-lib/test/testing-library"
2
2
  import userEvent from "@zeno-lib/test/user-event"
3
3
  import { afterEach, expect, test, vi } from "vitest"
4
4
  import { z } from "zod"
5
- import {
6
- Form,
7
- FormProvider,
8
- useAppFields,
9
- useForm,
10
- withForm,
11
- } from "./create-form"
5
+ import { Form, FormProvider, useForm, withForm } from "./create-form"
12
6
 
13
7
  const CITY = /City/
14
8
 
@@ -24,10 +18,7 @@ type Profile = z.infer<typeof schema>
24
18
 
25
19
  const AddressSection = withForm({
26
20
  defaultValues: {} as Profile,
27
- render: ({ form }) => {
28
- const { InputField } = useAppFields(form)
29
- return <InputField label="City" name="address.city" />
30
- },
21
+ render: ({ form }) => <form.InputField label="City" name="address.city" />,
31
22
  })
32
23
 
33
24
  test("a withForm section edits and validates the parent's form", async () => {