@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.
- package/dist/create-zeno-form.d.mts +18 -9
- package/dist/create-zeno-form.mjs +68 -23
- package/dist/lib/schema-defaults.d.mts +3 -1
- package/dist/lib/schema-defaults.mjs +43 -1
- package/dist/lib/submit-action.mjs +7 -2
- package/package.json +2 -2
- package/src/composition.test.tsx +140 -0
- package/src/create-zeno-form.tsx +237 -44
- package/src/default-props.test.tsx +25 -1
- package/src/form-dialog.test.tsx +34 -0
- package/src/form-error.test.tsx +47 -0
- package/src/lib/schema-defaults.test.ts +128 -1
- package/src/lib/schema-defaults.ts +74 -2
- package/src/lib/submit-action.ts +17 -2
- package/src/submit-action.test.tsx +45 -0
- package/src/use-form.test-d.ts +23 -0
- package/src/use-form.test.tsx +46 -0
- package/src/with-form.test-d.ts +25 -0
- package/src/with-form.test.tsx +2 -11
|
@@ -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>) =>
|
|
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<
|
|
129
|
-
} & FieldWrappers<FC, FormDataOf<
|
|
130
|
-
withFieldGroup: <TFieldGroupData, TSubmitMeta, TRenderProps extends object = {}>({ render,
|
|
131
|
-
|
|
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
|
|
135
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 {
|
|
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(
|
|
149
|
+
applyValidationError(formApi, error);
|
|
109
150
|
return;
|
|
110
151
|
}
|
|
111
|
-
|
|
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, [
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
+
})
|