remix-validated-form 5.0.2 → 5.1.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/index.cjs.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/ValidatedForm.tsx +0 -427
- package/src/hooks.ts +0 -160
- package/src/index.ts +0 -12
- package/src/internal/MultiValueMap.ts +0 -44
- package/src/internal/constants.ts +0 -4
- package/src/internal/flatten.ts +0 -12
- package/src/internal/formContext.ts +0 -13
- package/src/internal/getInputProps.test.ts +0 -251
- package/src/internal/getInputProps.ts +0 -94
- package/src/internal/hooks.ts +0 -217
- package/src/internal/hydratable.ts +0 -28
- package/src/internal/logic/getCheckboxChecked.ts +0 -10
- package/src/internal/logic/getRadioChecked.ts +0 -18
- package/src/internal/logic/nestedObjectToPathObject.ts +0 -63
- package/src/internal/logic/requestSubmit.test.tsx +0 -24
- package/src/internal/logic/requestSubmit.ts +0 -103
- package/src/internal/state/arrayUtil.ts +0 -451
- package/src/internal/state/controlledFields.ts +0 -86
- package/src/internal/state/createFormStore.ts +0 -591
- package/src/internal/state/fieldArray.tsx +0 -197
- package/src/internal/state/storeHooks.ts +0 -9
- package/src/internal/state/types.ts +0 -1
- package/src/internal/submissionCallbacks.ts +0 -15
- package/src/internal/util.ts +0 -39
- package/src/server.ts +0 -53
- package/src/unreleased/formStateHooks.ts +0 -170
- package/src/userFacingFormContext.ts +0 -147
- package/src/validation/createValidator.ts +0 -53
- package/src/validation/types.ts +0 -72
- package/tsconfig.json +0 -8
@@ -1,197 +0,0 @@
|
|
1
|
-
import { nanoid } from "nanoid";
|
2
|
-
import React, { useMemo, useRef, useState } from "react";
|
3
|
-
import { useCallback } from "react";
|
4
|
-
import invariant from "tiny-invariant";
|
5
|
-
import { InternalFormContextValue } from "../formContext";
|
6
|
-
import {
|
7
|
-
useFieldDefaultValue,
|
8
|
-
useFieldError,
|
9
|
-
useInternalFormContext,
|
10
|
-
useInternalHasBeenSubmitted,
|
11
|
-
useSmartValidate,
|
12
|
-
} from "../hooks";
|
13
|
-
import * as arrayUtil from "./arrayUtil";
|
14
|
-
import { useRegisterControlledField } from "./controlledFields";
|
15
|
-
import { useFormStore } from "./storeHooks";
|
16
|
-
|
17
|
-
export type FieldArrayValidationBehavior = "onChange" | "onSubmit";
|
18
|
-
|
19
|
-
export type FieldArrayValidationBehaviorOptions = {
|
20
|
-
initial: FieldArrayValidationBehavior;
|
21
|
-
whenSubmitted: FieldArrayValidationBehavior;
|
22
|
-
};
|
23
|
-
|
24
|
-
export type FieldArrayItem<T> = {
|
25
|
-
/**
|
26
|
-
* The default value of the item.
|
27
|
-
* This does not update as the field is changed by the user.
|
28
|
-
*/
|
29
|
-
defaultValue: T;
|
30
|
-
/**
|
31
|
-
* A unique key for the item.
|
32
|
-
* Use this as the key prop when rendering the item.
|
33
|
-
*/
|
34
|
-
key: string;
|
35
|
-
};
|
36
|
-
|
37
|
-
const useInternalFieldArray = (
|
38
|
-
context: InternalFormContextValue,
|
39
|
-
field: string,
|
40
|
-
validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>
|
41
|
-
) => {
|
42
|
-
const value = useFieldDefaultValue(field, context);
|
43
|
-
useRegisterControlledField(context, field);
|
44
|
-
const hasBeenSubmitted = useInternalHasBeenSubmitted(context.formId);
|
45
|
-
const validateField = useSmartValidate(context.formId);
|
46
|
-
const error = useFieldError(field, context);
|
47
|
-
|
48
|
-
const resolvedValidationBehavior: FieldArrayValidationBehaviorOptions = {
|
49
|
-
initial: "onSubmit",
|
50
|
-
whenSubmitted: "onChange",
|
51
|
-
...validationBehavior,
|
52
|
-
};
|
53
|
-
|
54
|
-
const behavior = hasBeenSubmitted
|
55
|
-
? resolvedValidationBehavior.whenSubmitted
|
56
|
-
: resolvedValidationBehavior.initial;
|
57
|
-
|
58
|
-
const maybeValidate = useCallback(() => {
|
59
|
-
if (behavior === "onChange") {
|
60
|
-
validateField({ alwaysIncludeErrorsFromFields: [field] });
|
61
|
-
}
|
62
|
-
}, [behavior, field, validateField]);
|
63
|
-
|
64
|
-
invariant(
|
65
|
-
value === undefined || value === null || Array.isArray(value),
|
66
|
-
`FieldArray: defaultValue value for ${field} must be an array, null, or undefined`
|
67
|
-
);
|
68
|
-
|
69
|
-
const arr = useFormStore(
|
70
|
-
context.formId,
|
71
|
-
(state) => state.controlledFields.array
|
72
|
-
);
|
73
|
-
|
74
|
-
const arrayValue = useMemo<unknown[]>(() => value ?? [], [value]);
|
75
|
-
const keyRef = useRef<string[]>([]);
|
76
|
-
|
77
|
-
// If the lengths don't match up it means one of two things
|
78
|
-
// 1. The array has been modified outside of this hook
|
79
|
-
// 2. We're initializing the array
|
80
|
-
if (keyRef.current.length !== arrayValue.length) {
|
81
|
-
keyRef.current = arrayValue.map(() => nanoid());
|
82
|
-
}
|
83
|
-
|
84
|
-
const helpers = useMemo(
|
85
|
-
() => ({
|
86
|
-
push: (item: any) => {
|
87
|
-
arr.push(field, item);
|
88
|
-
keyRef.current.push(nanoid());
|
89
|
-
maybeValidate();
|
90
|
-
},
|
91
|
-
swap: (indexA: number, indexB: number) => {
|
92
|
-
arr.swap(field, indexA, indexB);
|
93
|
-
arrayUtil.swap(keyRef.current, indexA, indexB);
|
94
|
-
maybeValidate();
|
95
|
-
},
|
96
|
-
move: (from: number, to: number) => {
|
97
|
-
arr.move(field, from, to);
|
98
|
-
arrayUtil.move(keyRef.current, from, to);
|
99
|
-
maybeValidate();
|
100
|
-
},
|
101
|
-
insert: (index: number, value: any) => {
|
102
|
-
arr.insert(field, index, value);
|
103
|
-
arrayUtil.insert(keyRef.current, index, nanoid());
|
104
|
-
maybeValidate();
|
105
|
-
},
|
106
|
-
unshift: (value: any) => {
|
107
|
-
arr.unshift(field, value);
|
108
|
-
keyRef.current.unshift(nanoid());
|
109
|
-
maybeValidate();
|
110
|
-
},
|
111
|
-
remove: (index: number) => {
|
112
|
-
arr.remove(field, index);
|
113
|
-
arrayUtil.remove(keyRef.current, index);
|
114
|
-
maybeValidate();
|
115
|
-
},
|
116
|
-
pop: () => {
|
117
|
-
arr.pop(field);
|
118
|
-
keyRef.current.pop();
|
119
|
-
maybeValidate();
|
120
|
-
},
|
121
|
-
replace: (index: number, value: any) => {
|
122
|
-
arr.replace(field, index, value);
|
123
|
-
keyRef.current[index] = nanoid();
|
124
|
-
maybeValidate();
|
125
|
-
},
|
126
|
-
}),
|
127
|
-
[arr, field, maybeValidate]
|
128
|
-
);
|
129
|
-
|
130
|
-
const valueWithKeys = useMemo(() => {
|
131
|
-
const result: { defaultValue: any; key: string }[] = [];
|
132
|
-
arrayValue.forEach((item, index) => {
|
133
|
-
result[index] = {
|
134
|
-
key: keyRef.current[index],
|
135
|
-
defaultValue: item,
|
136
|
-
};
|
137
|
-
});
|
138
|
-
return result;
|
139
|
-
}, [arrayValue]);
|
140
|
-
|
141
|
-
return [valueWithKeys, helpers, error] as const;
|
142
|
-
};
|
143
|
-
|
144
|
-
export type FieldArrayHelpers<Item = any> = {
|
145
|
-
push: (item: Item) => void;
|
146
|
-
swap: (indexA: number, indexB: number) => void;
|
147
|
-
move: (from: number, to: number) => void;
|
148
|
-
insert: (index: number, value: Item) => void;
|
149
|
-
unshift: (value: Item) => void;
|
150
|
-
remove: (index: number) => void;
|
151
|
-
pop: () => void;
|
152
|
-
replace: (index: number, value: Item) => void;
|
153
|
-
};
|
154
|
-
|
155
|
-
export type UseFieldArrayOptions = {
|
156
|
-
formId?: string;
|
157
|
-
validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>;
|
158
|
-
};
|
159
|
-
|
160
|
-
export function useFieldArray<Item = any>(
|
161
|
-
name: string,
|
162
|
-
{ formId, validationBehavior }: UseFieldArrayOptions = {}
|
163
|
-
) {
|
164
|
-
const context = useInternalFormContext(formId, "FieldArray");
|
165
|
-
|
166
|
-
return useInternalFieldArray(context, name, validationBehavior) as [
|
167
|
-
items: FieldArrayItem<Item>[],
|
168
|
-
helpers: FieldArrayHelpers,
|
169
|
-
error: string | undefined
|
170
|
-
];
|
171
|
-
}
|
172
|
-
|
173
|
-
export type FieldArrayProps<Item> = {
|
174
|
-
name: string;
|
175
|
-
children: (
|
176
|
-
items: FieldArrayItem<Item>[],
|
177
|
-
helpers: FieldArrayHelpers<Item>,
|
178
|
-
error: string | undefined
|
179
|
-
) => React.ReactNode;
|
180
|
-
formId?: string;
|
181
|
-
validationBehavior?: FieldArrayValidationBehaviorOptions;
|
182
|
-
};
|
183
|
-
|
184
|
-
export function FieldArray<Item = any>({
|
185
|
-
name,
|
186
|
-
children,
|
187
|
-
formId,
|
188
|
-
validationBehavior,
|
189
|
-
}: FieldArrayProps<Item>) {
|
190
|
-
const context = useInternalFormContext(formId, "FieldArray");
|
191
|
-
const [value, helpers, error] = useInternalFieldArray(
|
192
|
-
context,
|
193
|
-
name,
|
194
|
-
validationBehavior
|
195
|
-
);
|
196
|
-
return <>{children(value, helpers, error)}</>;
|
197
|
-
}
|
@@ -1,9 +0,0 @@
|
|
1
|
-
import { FormState, useRootFormStore } from "./createFormStore";
|
2
|
-
import { InternalFormId } from "./types";
|
3
|
-
|
4
|
-
export const useFormStore = <T>(
|
5
|
-
formId: InternalFormId,
|
6
|
-
selector: (state: FormState) => T
|
7
|
-
) => {
|
8
|
-
return useRootFormStore((state) => selector(state.form(formId)));
|
9
|
-
};
|
@@ -1 +0,0 @@
|
|
1
|
-
export type InternalFormId = string | symbol;
|
@@ -1,15 +0,0 @@
|
|
1
|
-
import { useEffect, useRef } from "react";
|
2
|
-
|
3
|
-
export function useSubmitComplete(isSubmitting: boolean, callback: () => void) {
|
4
|
-
const isPending = useRef(false);
|
5
|
-
useEffect(() => {
|
6
|
-
if (isSubmitting) {
|
7
|
-
isPending.current = true;
|
8
|
-
}
|
9
|
-
|
10
|
-
if (!isSubmitting && isPending.current) {
|
11
|
-
isPending.current = false;
|
12
|
-
callback();
|
13
|
-
}
|
14
|
-
});
|
15
|
-
}
|
package/src/internal/util.ts
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
import type React from "react";
|
2
|
-
import { useEffect, useLayoutEffect, useRef } from "react";
|
3
|
-
import * as R from "remeda";
|
4
|
-
|
5
|
-
export const omit = (obj: any, ...keys: string[]) => {
|
6
|
-
const result = { ...obj };
|
7
|
-
for (const key of keys) {
|
8
|
-
delete result[key];
|
9
|
-
}
|
10
|
-
return result;
|
11
|
-
};
|
12
|
-
|
13
|
-
export const mergeRefs = <T = any>(
|
14
|
-
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T> | undefined>
|
15
|
-
): React.RefCallback<T> => {
|
16
|
-
return (value: T) => {
|
17
|
-
refs.filter(Boolean).forEach((ref) => {
|
18
|
-
if (typeof ref === "function") {
|
19
|
-
ref(value);
|
20
|
-
} else if (ref != null) {
|
21
|
-
(ref as React.MutableRefObject<T | null>).current = value;
|
22
|
-
}
|
23
|
-
});
|
24
|
-
};
|
25
|
-
};
|
26
|
-
|
27
|
-
export const useIsomorphicLayoutEffect =
|
28
|
-
typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
29
|
-
|
30
|
-
export const useDeepEqualsMemo = <T>(item: T): T => {
|
31
|
-
const ref = useRef<T>(item);
|
32
|
-
const areEqual = ref.current === item || R.equals(ref.current, item);
|
33
|
-
useEffect(() => {
|
34
|
-
if (!areEqual) {
|
35
|
-
ref.current = item;
|
36
|
-
}
|
37
|
-
});
|
38
|
-
return areEqual ? ref.current : item;
|
39
|
-
};
|
package/src/server.ts
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
import { json } from "@remix-run/server-runtime";
|
2
|
-
import {
|
3
|
-
formDefaultValuesKey,
|
4
|
-
FORM_DEFAULTS_FIELD,
|
5
|
-
} from "./internal/constants";
|
6
|
-
import {
|
7
|
-
ValidatorError,
|
8
|
-
ValidationErrorResponseData,
|
9
|
-
} from "./validation/types";
|
10
|
-
|
11
|
-
/**
|
12
|
-
* Takes the errors from a `Validator` and returns a `Response`.
|
13
|
-
* When you return this from your action, `ValidatedForm` on the frontend will automatically
|
14
|
-
* display the errors on the correct fields on the correct form.
|
15
|
-
*
|
16
|
-
* You can also provide a second argument to `validationError`
|
17
|
-
* to specify how to repopulate the form when JS is disabled.
|
18
|
-
*
|
19
|
-
* @example
|
20
|
-
* ```ts
|
21
|
-
* const result = validator.validate(await request.formData());
|
22
|
-
* if (result.error) return validationError(result.error, result.submittedData);
|
23
|
-
* ```
|
24
|
-
*/
|
25
|
-
export function validationError(
|
26
|
-
error: ValidatorError,
|
27
|
-
repopulateFields?: unknown,
|
28
|
-
init?: ResponseInit
|
29
|
-
) {
|
30
|
-
return json<ValidationErrorResponseData>(
|
31
|
-
{
|
32
|
-
fieldErrors: error.fieldErrors,
|
33
|
-
subaction: error.subaction,
|
34
|
-
repopulateFields,
|
35
|
-
formId: error.formId,
|
36
|
-
},
|
37
|
-
{ status: 422, ...init }
|
38
|
-
);
|
39
|
-
}
|
40
|
-
|
41
|
-
export type FormDefaults = {
|
42
|
-
[formDefaultsKey: `${typeof FORM_DEFAULTS_FIELD}_${string}`]: any;
|
43
|
-
};
|
44
|
-
|
45
|
-
// FIXME: Remove after https://github.com/egoist/tsup/issues/813 is fixed
|
46
|
-
export type internal_FORM_DEFAULTS_FIELD = typeof FORM_DEFAULTS_FIELD;
|
47
|
-
|
48
|
-
export const setFormDefaults = <DataType = any>(
|
49
|
-
formId: string,
|
50
|
-
defaultValues: Partial<DataType>
|
51
|
-
): FormDefaults => ({
|
52
|
-
[formDefaultValuesKey(formId)]: defaultValues,
|
53
|
-
});
|
@@ -1,170 +0,0 @@
|
|
1
|
-
import { useMemo } from "react";
|
2
|
-
import {} from "../internal/getInputProps";
|
3
|
-
import {
|
4
|
-
useInternalFormContext,
|
5
|
-
useClearError,
|
6
|
-
useSetTouched,
|
7
|
-
useDefaultValuesForForm,
|
8
|
-
useFieldErrorsForForm,
|
9
|
-
useInternalIsSubmitting,
|
10
|
-
useInternalHasBeenSubmitted,
|
11
|
-
useTouchedFields,
|
12
|
-
useInternalIsValid,
|
13
|
-
useFieldErrors,
|
14
|
-
useValidate,
|
15
|
-
useSetFieldErrors,
|
16
|
-
useResetFormElement,
|
17
|
-
useSyncedDefaultValues,
|
18
|
-
useFormActionProp,
|
19
|
-
useFormSubactionProp,
|
20
|
-
useSubmitForm,
|
21
|
-
useFormValues,
|
22
|
-
useSmartValidate,
|
23
|
-
} from "../internal/hooks";
|
24
|
-
import {
|
25
|
-
FieldErrors,
|
26
|
-
TouchedFields,
|
27
|
-
ValidationResult,
|
28
|
-
} from "../validation/types";
|
29
|
-
|
30
|
-
export type FormState = {
|
31
|
-
fieldErrors: FieldErrors;
|
32
|
-
isSubmitting: boolean;
|
33
|
-
hasBeenSubmitted: boolean;
|
34
|
-
touchedFields: TouchedFields;
|
35
|
-
defaultValues: { [fieldName: string]: any };
|
36
|
-
action?: string;
|
37
|
-
subaction?: string;
|
38
|
-
isValid: boolean;
|
39
|
-
};
|
40
|
-
|
41
|
-
/**
|
42
|
-
* Returns information about the form.
|
43
|
-
*
|
44
|
-
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
45
|
-
*/
|
46
|
-
export const useFormState = (formId?: string): FormState => {
|
47
|
-
const formContext = useInternalFormContext(formId, "useFormState");
|
48
|
-
const isSubmitting = useInternalIsSubmitting(formContext.formId);
|
49
|
-
const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId);
|
50
|
-
const touchedFields = useTouchedFields(formContext.formId);
|
51
|
-
const isValid = useInternalIsValid(formContext.formId);
|
52
|
-
const action = useFormActionProp(formContext.formId);
|
53
|
-
const subaction = useFormSubactionProp(formContext.formId);
|
54
|
-
|
55
|
-
const syncedDefaultValues = useSyncedDefaultValues(formContext.formId);
|
56
|
-
const defaultValuesToUse = useDefaultValuesForForm(formContext);
|
57
|
-
const hydratedDefaultValues =
|
58
|
-
defaultValuesToUse.hydrateTo(syncedDefaultValues);
|
59
|
-
|
60
|
-
const fieldErrorsFromState = useFieldErrors(formContext.formId);
|
61
|
-
const fieldErrorsToUse = useFieldErrorsForForm(formContext);
|
62
|
-
const hydratedFieldErrors = fieldErrorsToUse.hydrateTo(fieldErrorsFromState);
|
63
|
-
|
64
|
-
return useMemo(
|
65
|
-
() => ({
|
66
|
-
action,
|
67
|
-
subaction,
|
68
|
-
defaultValues: hydratedDefaultValues,
|
69
|
-
fieldErrors: hydratedFieldErrors ?? {},
|
70
|
-
hasBeenSubmitted,
|
71
|
-
isSubmitting,
|
72
|
-
touchedFields,
|
73
|
-
isValid,
|
74
|
-
}),
|
75
|
-
[
|
76
|
-
action,
|
77
|
-
hasBeenSubmitted,
|
78
|
-
hydratedDefaultValues,
|
79
|
-
hydratedFieldErrors,
|
80
|
-
isSubmitting,
|
81
|
-
isValid,
|
82
|
-
subaction,
|
83
|
-
touchedFields,
|
84
|
-
]
|
85
|
-
);
|
86
|
-
};
|
87
|
-
|
88
|
-
export type FormHelpers = {
|
89
|
-
/**
|
90
|
-
* Clear the error of the specified field.
|
91
|
-
*/
|
92
|
-
clearError: (fieldName: string) => void;
|
93
|
-
/**
|
94
|
-
* Validate the specified field.
|
95
|
-
*/
|
96
|
-
validateField: (fieldName: string) => Promise<string | null>;
|
97
|
-
/**
|
98
|
-
* Change the touched state of the specified field.
|
99
|
-
*/
|
100
|
-
setTouched: (fieldName: string, touched: boolean) => void;
|
101
|
-
/**
|
102
|
-
* Validate the whole form and populate any errors.
|
103
|
-
*/
|
104
|
-
validate: () => Promise<ValidationResult<unknown>>;
|
105
|
-
/**
|
106
|
-
* Clears all errors on the form.
|
107
|
-
*/
|
108
|
-
clearAllErrors: () => void;
|
109
|
-
/**
|
110
|
-
* Resets the form.
|
111
|
-
*
|
112
|
-
* _Note_: The equivalent behavior can be achieved by calling formElement.reset()
|
113
|
-
* or clicking a button element with `type="reset"`.
|
114
|
-
*/
|
115
|
-
reset: () => void;
|
116
|
-
/**
|
117
|
-
* Submits the form, running all validations first.
|
118
|
-
*
|
119
|
-
* _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
|
120
|
-
*/
|
121
|
-
submit: () => void;
|
122
|
-
/**
|
123
|
-
* Returns the current form values as FormData
|
124
|
-
*/
|
125
|
-
getValues: () => FormData;
|
126
|
-
};
|
127
|
-
|
128
|
-
/**
|
129
|
-
* Returns helpers that can be used to update the form state.
|
130
|
-
*
|
131
|
-
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
132
|
-
*/
|
133
|
-
export const useFormHelpers = (formId?: string): FormHelpers => {
|
134
|
-
const formContext = useInternalFormContext(formId, "useFormHelpers");
|
135
|
-
const setTouched = useSetTouched(formContext);
|
136
|
-
const validateField = useSmartValidate(formContext.formId);
|
137
|
-
const validate = useValidate(formContext.formId);
|
138
|
-
const clearError = useClearError(formContext);
|
139
|
-
const setFieldErrors = useSetFieldErrors(formContext.formId);
|
140
|
-
const reset = useResetFormElement(formContext.formId);
|
141
|
-
const submit = useSubmitForm(formContext.formId);
|
142
|
-
const getValues = useFormValues(formContext.formId);
|
143
|
-
return useMemo(
|
144
|
-
() => ({
|
145
|
-
setTouched,
|
146
|
-
validateField: async (fieldName: string) => {
|
147
|
-
const res = await validateField({
|
148
|
-
alwaysIncludeErrorsFromFields: [fieldName],
|
149
|
-
});
|
150
|
-
return res.error?.fieldErrors[fieldName] ?? null;
|
151
|
-
},
|
152
|
-
clearError,
|
153
|
-
validate,
|
154
|
-
clearAllErrors: () => setFieldErrors({}),
|
155
|
-
reset,
|
156
|
-
submit,
|
157
|
-
getValues,
|
158
|
-
}),
|
159
|
-
[
|
160
|
-
clearError,
|
161
|
-
reset,
|
162
|
-
setFieldErrors,
|
163
|
-
setTouched,
|
164
|
-
submit,
|
165
|
-
validate,
|
166
|
-
validateField,
|
167
|
-
getValues,
|
168
|
-
]
|
169
|
-
);
|
170
|
-
};
|
@@ -1,147 +0,0 @@
|
|
1
|
-
import { useCallback, useMemo } from "react";
|
2
|
-
import {
|
3
|
-
useInternalFormContext,
|
4
|
-
useRegisterReceiveFocus,
|
5
|
-
} from "./internal/hooks";
|
6
|
-
import { useFormHelpers, useFormState } from "./unreleased/formStateHooks";
|
7
|
-
import {
|
8
|
-
FieldErrors,
|
9
|
-
TouchedFields,
|
10
|
-
ValidationResult,
|
11
|
-
} from "./validation/types";
|
12
|
-
|
13
|
-
export type FormContextValue = {
|
14
|
-
/**
|
15
|
-
* All the errors in all the fields in the form.
|
16
|
-
*/
|
17
|
-
fieldErrors: FieldErrors;
|
18
|
-
/**
|
19
|
-
* Clear the errors of the specified fields.
|
20
|
-
*/
|
21
|
-
clearError: (...names: string[]) => void;
|
22
|
-
/**
|
23
|
-
* Validate the specified field.
|
24
|
-
*/
|
25
|
-
validateField: (fieldName: string) => Promise<string | null>;
|
26
|
-
/**
|
27
|
-
* The `action` prop of the form.
|
28
|
-
*/
|
29
|
-
action?: string;
|
30
|
-
/**
|
31
|
-
* The `subaction` prop of the form.
|
32
|
-
*/
|
33
|
-
subaction?: string;
|
34
|
-
/**
|
35
|
-
* Whether or not the form is submitting.
|
36
|
-
*/
|
37
|
-
isSubmitting: boolean;
|
38
|
-
/**
|
39
|
-
* Whether or not a submission has been attempted.
|
40
|
-
* This is true once the form has been submitted, even if there were validation errors.
|
41
|
-
* Resets to false when the form is reset.
|
42
|
-
*/
|
43
|
-
hasBeenSubmitted: boolean;
|
44
|
-
/**
|
45
|
-
* Whether or not the form is valid.
|
46
|
-
*/
|
47
|
-
isValid: boolean;
|
48
|
-
/**
|
49
|
-
* The default values of the form.
|
50
|
-
*/
|
51
|
-
defaultValues?: { [fieldName: string]: any };
|
52
|
-
/**
|
53
|
-
* Register a custom focus handler to be used when
|
54
|
-
* the field needs to receive focus due to a validation error.
|
55
|
-
*/
|
56
|
-
registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
|
57
|
-
/**
|
58
|
-
* Any fields that have been touched by the user.
|
59
|
-
*/
|
60
|
-
touchedFields: TouchedFields;
|
61
|
-
/**
|
62
|
-
* Change the touched state of the specified field.
|
63
|
-
*/
|
64
|
-
setFieldTouched: (fieldName: string, touched: boolean) => void;
|
65
|
-
/**
|
66
|
-
* Validate the whole form and populate any errors.
|
67
|
-
*/
|
68
|
-
validate: () => Promise<ValidationResult<unknown>>;
|
69
|
-
/**
|
70
|
-
* Clears all errors on the form.
|
71
|
-
*/
|
72
|
-
clearAllErrors: () => void;
|
73
|
-
/**
|
74
|
-
* Resets the form.
|
75
|
-
*
|
76
|
-
* _Note_: The equivalent behavior can be achieved by calling formElement.reset()
|
77
|
-
* or clicking a button element with `type="reset"`.
|
78
|
-
*/
|
79
|
-
reset: () => void;
|
80
|
-
/**
|
81
|
-
* Submits the form, running all validations first.
|
82
|
-
*
|
83
|
-
* _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
|
84
|
-
*/
|
85
|
-
submit: () => void;
|
86
|
-
/**
|
87
|
-
* Returns the current form values as FormData
|
88
|
-
*/
|
89
|
-
getValues: () => FormData;
|
90
|
-
};
|
91
|
-
|
92
|
-
/**
|
93
|
-
* Provides access to some of the internal state of the form.
|
94
|
-
*/
|
95
|
-
export const useFormContext = (formId?: string): FormContextValue => {
|
96
|
-
// Try to access context so we get our error specific to this hook if it's not there
|
97
|
-
const context = useInternalFormContext(formId, "useFormContext");
|
98
|
-
const state = useFormState(formId);
|
99
|
-
const {
|
100
|
-
clearError: internalClearError,
|
101
|
-
setTouched,
|
102
|
-
validateField,
|
103
|
-
clearAllErrors,
|
104
|
-
validate,
|
105
|
-
reset,
|
106
|
-
submit,
|
107
|
-
getValues,
|
108
|
-
} = useFormHelpers(formId);
|
109
|
-
|
110
|
-
const registerReceiveFocus = useRegisterReceiveFocus(context.formId);
|
111
|
-
|
112
|
-
const clearError = useCallback(
|
113
|
-
(...names: string[]) => {
|
114
|
-
names.forEach((name) => {
|
115
|
-
internalClearError(name);
|
116
|
-
});
|
117
|
-
},
|
118
|
-
[internalClearError]
|
119
|
-
);
|
120
|
-
|
121
|
-
return useMemo(
|
122
|
-
() => ({
|
123
|
-
...state,
|
124
|
-
setFieldTouched: setTouched,
|
125
|
-
validateField,
|
126
|
-
clearError,
|
127
|
-
registerReceiveFocus,
|
128
|
-
clearAllErrors,
|
129
|
-
validate,
|
130
|
-
reset,
|
131
|
-
submit,
|
132
|
-
getValues,
|
133
|
-
}),
|
134
|
-
[
|
135
|
-
clearAllErrors,
|
136
|
-
clearError,
|
137
|
-
registerReceiveFocus,
|
138
|
-
reset,
|
139
|
-
setTouched,
|
140
|
-
state,
|
141
|
-
submit,
|
142
|
-
validate,
|
143
|
-
validateField,
|
144
|
-
getValues,
|
145
|
-
]
|
146
|
-
);
|
147
|
-
};
|
@@ -1,53 +0,0 @@
|
|
1
|
-
import * as R from "remeda";
|
2
|
-
import { CreateValidatorArg, GenericObject, Validator } from "..";
|
3
|
-
import { FORM_ID_FIELD } from "../internal/constants";
|
4
|
-
import { objectFromPathEntries } from "../internal/flatten";
|
5
|
-
|
6
|
-
const preprocessFormData = (data: GenericObject | FormData): GenericObject => {
|
7
|
-
// A slightly janky way of determining if the data is a FormData object
|
8
|
-
// since node doesn't really have FormData
|
9
|
-
if ("entries" in data && typeof data.entries === "function")
|
10
|
-
return objectFromPathEntries([...data.entries()]);
|
11
|
-
return objectFromPathEntries(Object.entries(data));
|
12
|
-
};
|
13
|
-
|
14
|
-
const omitInternalFields = (data: GenericObject): GenericObject =>
|
15
|
-
R.omit(data, [FORM_ID_FIELD]);
|
16
|
-
|
17
|
-
/**
|
18
|
-
* Used to create a validator for a form.
|
19
|
-
* It provides built-in handling for unflattening nested objects and
|
20
|
-
* extracting the values from FormData.
|
21
|
-
*/
|
22
|
-
export function createValidator<T>(
|
23
|
-
validator: CreateValidatorArg<T>
|
24
|
-
): Validator<T> {
|
25
|
-
return {
|
26
|
-
validate: async (value) => {
|
27
|
-
const data = preprocessFormData(value);
|
28
|
-
const result = await validator.validate(omitInternalFields(data));
|
29
|
-
|
30
|
-
if (result.error) {
|
31
|
-
return {
|
32
|
-
data: undefined,
|
33
|
-
error: {
|
34
|
-
fieldErrors: result.error,
|
35
|
-
subaction: data.subaction,
|
36
|
-
formId: data[FORM_ID_FIELD],
|
37
|
-
},
|
38
|
-
submittedData: data,
|
39
|
-
formId: data[FORM_ID_FIELD],
|
40
|
-
};
|
41
|
-
}
|
42
|
-
|
43
|
-
return {
|
44
|
-
data: result.data,
|
45
|
-
error: undefined,
|
46
|
-
submittedData: data,
|
47
|
-
formId: data[FORM_ID_FIELD],
|
48
|
-
};
|
49
|
-
},
|
50
|
-
validateField: (data: GenericObject | FormData, field: string) =>
|
51
|
-
validator.validateField(preprocessFormData(data), field),
|
52
|
-
};
|
53
|
-
}
|