@zeno-lib/forms 0.7.1 → 0.9.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.
@@ -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
@@ -2,7 +2,7 @@
2
2
  import { applyValidationError } from "./lib/apply-validation-error.mjs";
3
3
  import { fieldContext, formContext } from "./lib/contexts.mjs";
4
4
  import { withDefaultProps } from "./lib/default-props.mjs";
5
- import { deepMergeDefaults, extractZodDefaults } from "./lib/schema-defaults.mjs";
5
+ import { deepMergeDefaults, extractZodDefaults, pickSchemaShape } from "./lib/schema-defaults.mjs";
6
6
  import { getRequiredPaths } from "./lib/schema-required.mjs";
7
7
  import { DEFAULT_VALIDATION_MODE, setFormZenoState } from "./lib/validation-modes.mjs";
8
8
  import { withEmptyStartValues } from "./lib/empty-value.mjs";
@@ -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;
@@ -100,22 +136,32 @@ function createZenoForm(config) {
100
136
  resolvedValidationLogic = mode === "blur-then-change" ? blurThenChangeLogic : void 0;
101
137
  }
102
138
  const wrappedOnSubmit = userOnSubmit ? (async (props) => {
103
- const { submissionAttempts } = props.formApi.state;
139
+ const { formApi } = props;
140
+ const { submissionAttempts } = formApi.state;
141
+ let superseded = false;
142
+ const watch = formApi.store.subscribe(() => {
143
+ if (formApi.state.submissionAttempts !== submissionAttempts) superseded = true;
144
+ });
104
145
  try {
105
146
  await userOnSubmit(props);
106
147
  } catch (error) {
107
148
  if (error instanceof ValidationError) {
108
- applyValidationError(props.formApi, error);
149
+ applyValidationError(formApi, error);
109
150
  return;
110
151
  }
111
- const { state } = props.formApi;
112
- if (state.values === props.value && state.submissionAttempts === submissionAttempts) setSubmitError(props.formApi, error);
152
+ if (!superseded) setSubmitError(formApi, error);
113
153
  throw error;
154
+ } finally {
155
+ watch.unsubscribe();
114
156
  }
115
157
  }) : void 0;
116
158
  const requiredFields = useMemo(() => schema ? getRequiredPaths(schema) : /* @__PURE__ */ new Set(), [schema]);
117
159
  const schemaDefaults = useMemo(() => schema ? withEmptyStartValues(schema, extractZodDefaults(schema)) : void 0, [schema]);
118
- const mergedDefaultValues = useMemo(() => schemaDefaults ? deepMergeDefaults(schemaDefaults, userDefaultValues) : userDefaultValues, [schemaDefaults, userDefaultValues]);
160
+ const mergedDefaultValues = useMemo(() => schemaDefaults ? deepMergeDefaults(schemaDefaults, pickSchemaShape(schema, userDefaultValues)) : userDefaultValues, [
161
+ schema,
162
+ schemaDefaults,
163
+ userDefaultValues
164
+ ]);
119
165
  const rebased = useRebasedDefaultValues(mergedDefaultValues);
120
166
  const listeners = withMountListener(rest.listeners, rebased.onMount);
121
167
  const form = useAppForm({
@@ -135,8 +181,7 @@ function createZenoForm(config) {
135
181
  });
136
182
  useDisabledOption(form, disabled);
137
183
  useUnsavedChangesWarning(form, unsavedChangesWarning);
138
- const fields = useAppFields(form);
139
- return useMemo(() => Object.assign(form, fields, formComponents), [form, fields]);
184
+ return useMemo(() => Object.assign(form, getAppFields(form), formComponents), [form]);
140
185
  }
141
186
  return {
142
187
  useAppFields,
@@ -8,6 +8,7 @@ type SchemaDef = {
8
8
  readonly type?: string;
9
9
  readonly innerType?: SchemaNode;
10
10
  readonly shape?: Record<string, SchemaNode>;
11
+ readonly catchall?: SchemaNode;
11
12
  readonly in?: SchemaNode;
12
13
  readonly element?: SchemaNode;
13
14
  readonly defaultValue?: unknown;
@@ -20,6 +21,7 @@ type ExtractResult = {
20
21
  };
21
22
  declare function extractZodDefaults(schema: SchemaNode): Record<string, unknown>;
22
23
  declare function extractZodDefaultAt(schema: SchemaNode, keys: readonly PropertyKey[]): ExtractResult;
24
+ declare function pickSchemaShape<T>(schema: SchemaNode, value: T): T;
23
25
  declare function deepMergeDefaults(schemaDefaults: Record<string, unknown>, userDefaults: Record<string, unknown> | undefined): Record<string, unknown>;
24
26
  //#endregion
25
- export { deepMergeDefaults, extractZodDefaultAt, extractZodDefaults };
27
+ export { deepMergeDefaults, extractZodDefaultAt, extractZodDefaults, pickSchemaShape };
@@ -92,6 +92,48 @@ function extractZodDefaultAt(schema, keys) {
92
92
  function isPlainObject(value) {
93
93
  return typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
94
94
  }
95
+ function acceptsOnlyShape(def) {
96
+ return def.catchall === void 0 || getDef(def.catchall)?.type === "never";
97
+ }
98
+ function pickShape(node, value) {
99
+ let current = node;
100
+ for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth += 1) {
101
+ const def = getDef(current);
102
+ switch (def?.type) {
103
+ case "object": {
104
+ const { shape } = def;
105
+ if (!(shape && isPlainObject(value) && acceptsOnlyShape(def))) return value;
106
+ const result = {};
107
+ for (const [key, field] of Object.entries(shape)) if (Object.hasOwn(value, key)) result[key] = pickShape(field, value[key]);
108
+ return result;
109
+ }
110
+ case "array": {
111
+ const { element } = def;
112
+ return element && Array.isArray(value) ? value.map((item) => pickShape(element, item)) : value;
113
+ }
114
+ case "default":
115
+ case "nonoptional":
116
+ case "nullable":
117
+ case "optional":
118
+ case "prefault":
119
+ case "readonly":
120
+ current = def.innerType;
121
+ continue;
122
+ case "pipe":
123
+ current = def.in;
124
+ continue;
125
+ default: return value;
126
+ }
127
+ }
128
+ return value;
129
+ }
130
+ function pickSchemaShape(schema, value) {
131
+ try {
132
+ return pickShape(schema, value);
133
+ } catch {
134
+ return value;
135
+ }
136
+ }
95
137
  function deepMergeDefaults(schemaDefaults, userDefaults) {
96
138
  if (!userDefaults) return schemaDefaults;
97
139
  const result = { ...schemaDefaults };
@@ -104,4 +146,4 @@ function deepMergeDefaults(schemaDefaults, userDefaults) {
104
146
  return result;
105
147
  }
106
148
  //#endregion
107
- export { deepMergeDefaults, extractZodDefaultAt, extractZodDefaults };
149
+ export { deepMergeDefaults, extractZodDefaultAt, extractZodDefaults, pickSchemaShape };
@@ -1,5 +1,6 @@
1
1
  import { applyValidationError } from "./apply-validation-error.mjs";
2
2
  import { toActionError } from "./action-result.mjs";
3
+ import { pickSchemaShape } from "./schema-defaults.mjs";
3
4
  import { getFormSchema } from "./validation-modes.mjs";
4
5
  import { ValidationError } from "./validation-error.mjs";
5
6
  //#region src/lib/submit-action.ts
@@ -36,10 +37,14 @@ async function submitAction(props, action, options = {}) {
36
37
  return result;
37
38
  }
38
39
  const { reset } = options;
39
- if (reset === true) formApi.reset(result.data);
40
+ if (reset === true) formApi.reset(toFormValues(formApi, result.data));
40
41
  else if (reset === "values") formApi.reset(value);
41
- else if (typeof reset === "function") formApi.reset(reset(result.data));
42
+ else if (typeof reset === "function") formApi.reset(toFormValues(formApi, reset(result.data)));
42
43
  return result;
43
44
  }
45
+ function toFormValues(formApi, data) {
46
+ const schema = getFormSchema(formApi);
47
+ return schema ? pickSchemaShape(schema, data) : data;
48
+ }
44
49
  //#endregion
45
50
  export { applyActionError, submitAction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeno-lib/forms",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "repository": {
5
5
  "url": "https://github.com/zeno-lib/zeno.git",
6
6
  "directory": "packages/forms"
@@ -37,7 +37,7 @@
37
37
  "@types/react": "19.3.0",
38
38
  "@types/react-dom": "19.3.0",
39
39
  "@zeno-lib/test": "^0.1.1",
40
- "@zeno-lib/typescript": "^1.1.0",
40
+ "@zeno-lib/typescript": "^1.2.0",
41
41
  "@zeno-lib/ui": "^0.0.1",
42
42
  "lucide-react": "^1.52.0",
43
43
  "tsdown": "^0.23.0",
@@ -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
+ })