remix-validated-form 5.0.2 → 5.1.1-beta.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.turbo/turbo-build.log +152 -8
- package/dist/index.cjs.js +898 -63
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.esm.js +876 -15
- package/dist/index.esm.js.map +1 -1
- package/package.json +4 -4
- 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
|
-
}
|