remix-validated-form 4.1.4-beta.0 → 4.1.6
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/.turbo/turbo-build.log +2 -2
- package/browser/ValidatedForm.js +31 -36
- package/browser/hooks.js +13 -16
- package/browser/internal/customState.d.ts +105 -0
- package/browser/internal/customState.js +46 -0
- package/browser/internal/getInputProps.js +4 -14
- package/browser/internal/hooks.d.ts +14 -15
- package/browser/internal/hooks.js +37 -39
- package/browser/internal/logic/elementUtils.d.ts +3 -0
- package/browser/internal/logic/elementUtils.js +3 -0
- package/browser/internal/logic/setInputValueInForm.js +9 -52
- package/browser/internal/setFieldValue.d.ts +0 -0
- package/browser/internal/setFieldValue.js +0 -0
- package/browser/internal/setFormValues.d.ts +0 -0
- package/browser/internal/setFormValues.js +0 -0
- package/browser/internal/state.d.ts +339 -238
- package/browser/internal/state.js +59 -72
- package/browser/internal/watch.d.ts +18 -0
- package/browser/internal/watch.js +122 -0
- package/browser/unreleased/formStateHooks.d.ts +39 -0
- package/browser/unreleased/formStateHooks.js +54 -0
- package/browser/userFacingFormContext.js +9 -23
- package/build/ValidatedForm.js +30 -35
- package/build/hooks.js +11 -14
- package/build/internal/getInputProps.js +4 -14
- package/build/internal/hooks.d.ts +14 -15
- package/build/internal/hooks.js +39 -41
- package/build/internal/logic/elementUtils.d.ts +3 -0
- package/build/internal/logic/elementUtils.js +9 -0
- package/build/internal/logic/setInputValueInForm.js +12 -55
- package/build/internal/setFormValues.d.ts +0 -0
- package/build/internal/setFormValues.js +0 -0
- package/build/internal/state/controlledFields.js +11 -2
- package/build/internal/state.d.ts +339 -238
- package/build/internal/state.js +61 -77
- package/build/internal/watch.d.ts +20 -0
- package/build/internal/watch.js +126 -0
- package/build/unreleased/formStateHooks.d.ts +39 -0
- package/build/unreleased/formStateHooks.js +59 -0
- package/build/userFacingFormContext.js +9 -23
- package/package.json +1 -2
- package/src/ValidatedForm.tsx +48 -52
- package/src/hooks.ts +15 -26
- package/src/internal/getInputProps.ts +4 -14
- package/src/internal/hooks.ts +60 -72
- package/src/internal/hydratable.ts +28 -0
- package/src/internal/logic/getCheckboxChecked.ts +10 -0
- package/src/internal/logic/getRadioChecked.ts +7 -0
- package/src/internal/state/atomUtils.ts +13 -0
- package/src/internal/state.ts +99 -177
- package/src/unreleased/formStateHooks.ts +113 -0
- package/src/userFacingFormContext.ts +14 -53
@@ -1,82 +1,69 @@
|
|
1
1
|
import { atom } from "jotai";
|
2
|
-
import { atomWithImmer } from "jotai/immer";
|
3
2
|
import { atomFamily, selectAtom } from "jotai/utils";
|
4
|
-
import
|
3
|
+
import omit from "lodash/omit";
|
4
|
+
import { fieldAtomFamily, formAtomFamily, } from "./state/atomUtils";
|
5
5
|
export const ATOM_SCOPE = Symbol("remix-validated-form-scope");
|
6
|
-
export const
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
formId: typeof formId === "string" ? formId : undefined,
|
13
|
-
// Will change upon hydration -- these will never actually be used
|
6
|
+
export const isHydratedAtom = formAtomFamily(false);
|
7
|
+
export const isSubmittingAtom = formAtomFamily(false);
|
8
|
+
export const hasBeenSubmittedAtom = formAtomFamily(false);
|
9
|
+
export const fieldErrorsAtom = formAtomFamily({});
|
10
|
+
export const touchedFieldsAtom = formAtomFamily({});
|
11
|
+
export const formPropsAtom = formAtomFamily({
|
14
12
|
validateField: () => Promise.resolve(null),
|
15
13
|
registerReceiveFocus: () => () => { },
|
14
|
+
defaultValues: {},
|
15
|
+
});
|
16
|
+
export const formElementAtom = formAtomFamily(null);
|
17
|
+
//// Everything below is derived from the above
|
18
|
+
export const cleanupFormState = (formId) => {
|
19
|
+
[
|
20
|
+
isHydratedAtom,
|
21
|
+
isSubmittingAtom,
|
22
|
+
hasBeenSubmittedAtom,
|
23
|
+
fieldErrorsAtom,
|
24
|
+
touchedFieldsAtom,
|
25
|
+
formPropsAtom,
|
26
|
+
].forEach((formAtom) => formAtom.remove(formId));
|
27
|
+
};
|
28
|
+
export const isValidAtom = atomFamily((formId) => atom((get) => Object.keys(get(fieldErrorsAtom(formId))).length === 0));
|
29
|
+
export const resetAtom = atomFamily((formId) => atom(null, (_get, set) => {
|
30
|
+
set(fieldErrorsAtom(formId), {});
|
31
|
+
set(touchedFieldsAtom(formId), {});
|
32
|
+
set(hasBeenSubmittedAtom(formId), false);
|
16
33
|
}));
|
17
|
-
export const
|
18
|
-
|
19
|
-
|
20
|
-
// Selector atoms
|
21
|
-
export const formSelectorAtom = (selector) => (formAtom) => selectAtom(formAtom, selector);
|
22
|
-
export const fieldErrorsAtom = formSelectorAtom((state) => state.fieldErrors);
|
23
|
-
export const touchedFieldsAtom = formSelectorAtom((state) => state.touchedFields);
|
24
|
-
export const actionAtom = formSelectorAtom((state) => state.action);
|
25
|
-
export const hasBeenSubmittedAtom = formSelectorAtom((state) => state.hasBeenSubmitted);
|
26
|
-
export const validateFieldAtom = formSelectorAtom((state) => state.validateField);
|
27
|
-
export const registerReceiveFocusAtom = formSelectorAtom((state) => state.registerReceiveFocus);
|
28
|
-
export const isSubmittingAtom = formSelectorAtom((state) => state.isSubmitting);
|
29
|
-
export const defaultValuesAtom = formSelectorAtom((state) => state.defaultValues);
|
30
|
-
export const isValidAtom = formSelectorAtom((state) => { var _a; return Object.keys((_a = state.fieldErrors) !== null && _a !== void 0 ? _a : {}).length === 0; });
|
31
|
-
export const isHydratedAtom = formSelectorAtom((state) => state.hydrated);
|
32
|
-
export const clearErrorAtom = atom(null, (get, set, { name, formAtom }) => set(formAtom, (state) => {
|
33
|
-
var _a;
|
34
|
-
(_a = state.fieldErrors) === null || _a === void 0 ? true : delete _a[name];
|
35
|
-
return state;
|
34
|
+
export const startSubmitAtom = atomFamily((formId) => atom(null, (_get, set) => {
|
35
|
+
set(isSubmittingAtom(formId), true);
|
36
|
+
set(hasBeenSubmittedAtom(formId), true);
|
36
37
|
}));
|
37
|
-
export const
|
38
|
-
|
39
|
-
state.fieldErrors = {};
|
40
|
-
state.fieldErrors[name] = error;
|
41
|
-
return state;
|
38
|
+
export const endSubmitAtom = atomFamily((formId) => atom(null, (_get, set) => {
|
39
|
+
set(isSubmittingAtom(formId), false);
|
42
40
|
}));
|
43
|
-
export const
|
44
|
-
|
45
|
-
|
41
|
+
export const setTouchedAtom = atomFamily((formId) => atom(null, (get, set, { field, touched }) => {
|
42
|
+
const prev = get(touchedFieldsAtom(formId));
|
43
|
+
if (prev[field] !== touched) {
|
44
|
+
set(touchedFieldsAtom(formId), {
|
45
|
+
...prev,
|
46
|
+
[field]: touched,
|
47
|
+
});
|
48
|
+
}
|
46
49
|
}));
|
47
|
-
export const
|
48
|
-
|
49
|
-
|
50
|
+
export const setFieldErrorAtom = atomFamily((formId) => atom(null, (get, set, { field, error }) => {
|
51
|
+
const prev = get(fieldErrorsAtom(formId));
|
52
|
+
if (error === undefined && field in prev) {
|
53
|
+
set(fieldErrorsAtom(formId), omit(prev, field));
|
54
|
+
}
|
55
|
+
if (error !== undefined && prev[field] !== error) {
|
56
|
+
set(fieldErrorsAtom(formId), {
|
57
|
+
...get(fieldErrorsAtom(formId)),
|
58
|
+
[field]: error,
|
59
|
+
});
|
60
|
+
}
|
50
61
|
}));
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
});
|
59
|
-
export const startSubmitAtom = atom(null, (get, set, { formAtom }) => {
|
60
|
-
set(formAtom, (state) => {
|
61
|
-
state.hasBeenSubmitted = true;
|
62
|
-
state.isSubmitting = true;
|
63
|
-
return state;
|
64
|
-
});
|
65
|
-
});
|
66
|
-
export const endSubmitAtom = atom(null, (get, set, { formAtom }) => {
|
67
|
-
set(formAtom, (state) => {
|
68
|
-
state.isSubmitting = false;
|
69
|
-
return state;
|
70
|
-
});
|
71
|
-
});
|
72
|
-
export const syncFormContextAtom = atom(null, (get, set, { defaultValues, action, subaction, formAtom, validateField, registerReceiveFocus, }) => {
|
73
|
-
set(formAtom, (state) => {
|
74
|
-
state.defaultValues = defaultValues;
|
75
|
-
state.action = action;
|
76
|
-
state.subaction = subaction;
|
77
|
-
state.registerReceiveFocus = registerReceiveFocus;
|
78
|
-
state.validateField = validateField;
|
79
|
-
state.hydrated = true;
|
80
|
-
return state;
|
81
|
-
});
|
82
|
-
});
|
62
|
+
//// Field specific
|
63
|
+
export const fieldTouchedAtom = fieldAtomFamily(({ formId, field }) => atom((get) => get(touchedFieldsAtom(formId))[field], (_get, set, touched) => {
|
64
|
+
set(setTouchedAtom(formId), { field, touched });
|
65
|
+
}));
|
66
|
+
export const fieldErrorAtom = fieldAtomFamily(({ formId, field }) => atom((get) => get(fieldErrorsAtom(formId))[field], (_get, set, error) => {
|
67
|
+
set(setFieldErrorAtom(formId), { field, error });
|
68
|
+
}));
|
69
|
+
export const fieldDefaultValueAtom = fieldAtomFamily(({ formId, field }) => selectAtom(formPropsAtom(formId), (state) => state.defaultValues[field]));
|
@@ -0,0 +1,18 @@
|
|
1
|
+
declare type ParseInfo = {
|
2
|
+
value: unknown;
|
3
|
+
type: string;
|
4
|
+
isRepeated: boolean;
|
5
|
+
checked?: boolean;
|
6
|
+
};
|
7
|
+
declare type BaseWatchOptions<T> = {
|
8
|
+
formId?: string;
|
9
|
+
parse?: (info: ParseInfo) => T;
|
10
|
+
};
|
11
|
+
declare type UseWatchType = {
|
12
|
+
<T, U extends boolean | undefined>(name: string, options?: BaseWatchOptions<T> & {
|
13
|
+
repeatable: U | undefined;
|
14
|
+
}): U extends true ? T[] : T;
|
15
|
+
<T>(name: string, options?: BaseWatchOptions<T>): T;
|
16
|
+
};
|
17
|
+
export declare const useWatch: UseWatchType;
|
18
|
+
export {};
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import { atom } from "jotai";
|
2
|
+
import { selectAtom } from "jotai/utils";
|
3
|
+
import isNaN from "lodash/isNaN";
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
5
|
+
import invariant from "tiny-invariant";
|
6
|
+
import { useInternalFormContext, useFieldDefaultValue, useFormAtomValue, } from "./hooks";
|
7
|
+
import { isCheckbox, isMultiselect, isRadio } from "./logic/elementUtils";
|
8
|
+
import { formElementAtom } from "./state";
|
9
|
+
import { fieldAtomFamily } from "./state/atomUtils";
|
10
|
+
import { controlledFieldsAtom } from "./state/controlledFields";
|
11
|
+
const emptyAtom = atom(undefined); // Always empty -- just used as a default value
|
12
|
+
const watchControlledFieldAtom = fieldAtomFamily(({ field, formId }) => selectAtom(controlledFieldsAtom(formId), (fields) => {
|
13
|
+
var _a;
|
14
|
+
return ({
|
15
|
+
present: field in fields,
|
16
|
+
valueAtom: (_a = fields[field]) !== null && _a !== void 0 ? _a : emptyAtom,
|
17
|
+
});
|
18
|
+
}, (a, b) => a.present === b.present && a.valueAtom === b.valueAtom));
|
19
|
+
const defaultParse = ({ value, type, isRepeated, checked, }) => {
|
20
|
+
if (type === "number") {
|
21
|
+
if (value === "")
|
22
|
+
return undefined;
|
23
|
+
const result = Number(value);
|
24
|
+
if (isNaN(result))
|
25
|
+
throw new Error("Value is not a number");
|
26
|
+
return result;
|
27
|
+
}
|
28
|
+
if (type === "checkbox" && !isRepeated) {
|
29
|
+
return checked;
|
30
|
+
}
|
31
|
+
return value;
|
32
|
+
};
|
33
|
+
const getInputValues = (node, multipleInputs = false) => {
|
34
|
+
if (!node)
|
35
|
+
return [];
|
36
|
+
if (node instanceof RadioNodeList) {
|
37
|
+
return [...node].flatMap((el) => getInputValues(el, true));
|
38
|
+
}
|
39
|
+
if (isMultiselect(node)) {
|
40
|
+
return [
|
41
|
+
{
|
42
|
+
type: "select",
|
43
|
+
value: [...node.options]
|
44
|
+
.filter((opt) => opt.selected)
|
45
|
+
.map((opt) => opt.value),
|
46
|
+
},
|
47
|
+
];
|
48
|
+
}
|
49
|
+
if (isCheckbox(node)) {
|
50
|
+
if (node.checked || !multipleInputs)
|
51
|
+
return [{ type: "checkbox", value: node.value, checked: node.checked }];
|
52
|
+
return [];
|
53
|
+
}
|
54
|
+
if (isRadio(node)) {
|
55
|
+
if (node.checked)
|
56
|
+
return [{ type: "radio", value: node.value, checked: node.checked }];
|
57
|
+
return [];
|
58
|
+
}
|
59
|
+
const input = node;
|
60
|
+
return [{ type: input.type, value: input.value, checked: input.checked }];
|
61
|
+
};
|
62
|
+
export const useWatch = (field, options) => {
|
63
|
+
const { formId, parse = defaultParse, repeatable = false, } = options !== null && options !== void 0 ? options : {};
|
64
|
+
const context = useInternalFormContext(formId, "useWatch");
|
65
|
+
const defaultValue = useFieldDefaultValue(field, context);
|
66
|
+
const hasSynced = useRef(false);
|
67
|
+
const [inputValues, setValue] = useState([]);
|
68
|
+
const formElement = useFormAtomValue(formElementAtom(context.formId));
|
69
|
+
const controlledField = useFormAtomValue(watchControlledFieldAtom({ formId: context.formId, field }));
|
70
|
+
const controlledValue = useFormAtomValue(controlledField.valueAtom);
|
71
|
+
const shouldSyncNativeInputValue = !controlledField.present && formElement;
|
72
|
+
const syncFieldValue = useCallback(() => {
|
73
|
+
invariant(formElement, `Unable to find form element for form. Watching field ${field}`);
|
74
|
+
hasSynced.current = true;
|
75
|
+
// We pull the values out using `form.elements` instead of `FormData`
|
76
|
+
// so that we can access the `type` of the input.
|
77
|
+
setValue(getInputValues(formElement.elements.namedItem(field)));
|
78
|
+
}, [field, formElement]);
|
79
|
+
// Should set the field values after the initial render
|
80
|
+
useEffect(() => {
|
81
|
+
if (shouldSyncNativeInputValue)
|
82
|
+
syncFieldValue();
|
83
|
+
}, [
|
84
|
+
controlledField.present,
|
85
|
+
formElement,
|
86
|
+
shouldSyncNativeInputValue,
|
87
|
+
syncFieldValue,
|
88
|
+
]);
|
89
|
+
useEffect(() => {
|
90
|
+
if (shouldSyncNativeInputValue) {
|
91
|
+
const listener = async (event) => {
|
92
|
+
if (!(event.target instanceof HTMLElement))
|
93
|
+
return;
|
94
|
+
const target = event.target;
|
95
|
+
const { form: targetForm, name: targetName } = target;
|
96
|
+
if (targetForm === formElement && targetName === field) {
|
97
|
+
syncFieldValue();
|
98
|
+
}
|
99
|
+
};
|
100
|
+
window.addEventListener("change", listener);
|
101
|
+
window.addEventListener("input", listener);
|
102
|
+
return () => {
|
103
|
+
window.removeEventListener("change", listener);
|
104
|
+
window.removeEventListener("input", listener);
|
105
|
+
};
|
106
|
+
}
|
107
|
+
}, [field, formElement, shouldSyncNativeInputValue, syncFieldValue]);
|
108
|
+
const parsedValue = useMemo(() => {
|
109
|
+
const parsed = inputValues.map(({ type, value, checked }) => parse({
|
110
|
+
value,
|
111
|
+
type,
|
112
|
+
isRepeated: inputValues.length > 1 || repeatable,
|
113
|
+
checked,
|
114
|
+
}));
|
115
|
+
return parsed.length > 1 || repeatable ? parsed : parsed[0];
|
116
|
+
}, [parse, repeatable, inputValues]);
|
117
|
+
if (controlledField.present)
|
118
|
+
return controlledValue;
|
119
|
+
if (!hasSynced.current)
|
120
|
+
return defaultValue !== null && defaultValue !== void 0 ? defaultValue : (repeatable ? [] : undefined);
|
121
|
+
return parsedValue;
|
122
|
+
};
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { FieldErrors, TouchedFields } from "../validation/types";
|
2
|
+
export declare type FormState = {
|
3
|
+
fieldErrors: FieldErrors;
|
4
|
+
isSubmitting: boolean;
|
5
|
+
hasBeenSubmitted: boolean;
|
6
|
+
touchedFields: TouchedFields;
|
7
|
+
defaultValues: {
|
8
|
+
[fieldName: string]: any;
|
9
|
+
};
|
10
|
+
action?: string;
|
11
|
+
subaction?: string;
|
12
|
+
isValid: boolean;
|
13
|
+
};
|
14
|
+
/**
|
15
|
+
* Returns information about the form.
|
16
|
+
*
|
17
|
+
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
18
|
+
*/
|
19
|
+
export declare const useFormState: (formId?: string | undefined) => FormState;
|
20
|
+
export declare type FormHelpers = {
|
21
|
+
/**
|
22
|
+
* Clear the error of the specified field.
|
23
|
+
*/
|
24
|
+
clearError: (fieldName: string) => void;
|
25
|
+
/**
|
26
|
+
* Validate the specified field.
|
27
|
+
*/
|
28
|
+
validateField: (fieldName: string) => Promise<string | null>;
|
29
|
+
/**
|
30
|
+
* Change the touched state of the specified field.
|
31
|
+
*/
|
32
|
+
setTouched: (fieldName: string, touched: boolean) => void;
|
33
|
+
};
|
34
|
+
/**
|
35
|
+
* Returns helpers that can be used to update the form state.
|
36
|
+
*
|
37
|
+
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
38
|
+
*/
|
39
|
+
export declare const useFormHelpers: (formId?: string | undefined) => FormHelpers;
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import { useMemo } from "react";
|
2
|
+
import { useInternalFormContext, useClearError, useSetTouched, useDefaultValuesForForm, useFieldErrorsForForm, useFormAtomValue, } from "../internal/hooks";
|
3
|
+
import { fieldErrorsAtom, formPropsAtom, hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, touchedFieldsAtom, } from "../internal/state";
|
4
|
+
/**
|
5
|
+
* Returns information about the form.
|
6
|
+
*
|
7
|
+
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
8
|
+
*/
|
9
|
+
export const useFormState = (formId) => {
|
10
|
+
const formContext = useInternalFormContext(formId, "useIsValid");
|
11
|
+
const formProps = useFormAtomValue(formPropsAtom(formContext.formId));
|
12
|
+
const isSubmitting = useFormAtomValue(isSubmittingAtom(formContext.formId));
|
13
|
+
const hasBeenSubmitted = useFormAtomValue(hasBeenSubmittedAtom(formContext.formId));
|
14
|
+
const touchedFields = useFormAtomValue(touchedFieldsAtom(formContext.formId));
|
15
|
+
const isValid = useFormAtomValue(isValidAtom(formContext.formId));
|
16
|
+
const defaultValuesToUse = useDefaultValuesForForm(formContext);
|
17
|
+
const hydratedDefaultValues = defaultValuesToUse.hydrateTo(formProps.defaultValues);
|
18
|
+
const fieldErrorsFromState = useFormAtomValue(fieldErrorsAtom(formContext.formId));
|
19
|
+
const fieldErrorsToUse = useFieldErrorsForForm(formContext);
|
20
|
+
const hydratedFieldErrors = fieldErrorsToUse.hydrateTo(fieldErrorsFromState);
|
21
|
+
return useMemo(() => ({
|
22
|
+
...formProps,
|
23
|
+
defaultValues: hydratedDefaultValues,
|
24
|
+
fieldErrors: hydratedFieldErrors !== null && hydratedFieldErrors !== void 0 ? hydratedFieldErrors : {},
|
25
|
+
hasBeenSubmitted,
|
26
|
+
isSubmitting,
|
27
|
+
touchedFields,
|
28
|
+
isValid,
|
29
|
+
}), [
|
30
|
+
formProps,
|
31
|
+
hasBeenSubmitted,
|
32
|
+
hydratedDefaultValues,
|
33
|
+
hydratedFieldErrors,
|
34
|
+
isSubmitting,
|
35
|
+
isValid,
|
36
|
+
touchedFields,
|
37
|
+
]);
|
38
|
+
};
|
39
|
+
/**
|
40
|
+
* Returns helpers that can be used to update the form state.
|
41
|
+
*
|
42
|
+
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
43
|
+
*/
|
44
|
+
export const useFormHelpers = (formId) => {
|
45
|
+
const formContext = useInternalFormContext(formId, "useFormHelpers");
|
46
|
+
const setTouched = useSetTouched(formContext);
|
47
|
+
const { validateField } = useFormAtomValue(formPropsAtom(formContext.formId));
|
48
|
+
const clearError = useClearError(formContext);
|
49
|
+
return useMemo(() => ({
|
50
|
+
setTouched,
|
51
|
+
validateField,
|
52
|
+
clearError,
|
53
|
+
}), [clearError, setTouched, validateField]);
|
54
|
+
};
|
@@ -1,40 +1,26 @@
|
|
1
1
|
import { useCallback } from "react";
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
import {
|
2
|
+
import { useFormAtomValue, useInternalFormContext } from "./internal/hooks";
|
3
|
+
import { formPropsAtom } from "./internal/state";
|
4
|
+
import { useFormHelpers, useFormState } from "./unreleased/formStateHooks";
|
5
5
|
/**
|
6
6
|
* Provides access to some of the internal state of the form.
|
7
7
|
*/
|
8
8
|
export const useFormContext = (formId) => {
|
9
9
|
// Try to access context so we get our error specific to this hook if it's not there
|
10
10
|
const context = useInternalFormContext(formId, "useFormContext");
|
11
|
-
const
|
12
|
-
const
|
13
|
-
const
|
14
|
-
const isValid = useIsValid(formId);
|
15
|
-
const defaultValues = useHydratableSelector(context, defaultValuesAtom, useDefaultValuesForForm(context));
|
16
|
-
const fieldErrors = useHydratableSelector(context, fieldErrorsAtom, useFieldErrorsForForm(context));
|
17
|
-
const setFieldTouched = useSetTouched(context);
|
18
|
-
const touchedFields = useContextSelectAtom(context.formId, touchedFieldsAtom);
|
19
|
-
const validateField = useContextSelectAtom(context.formId, validateFieldAtom);
|
20
|
-
const registerReceiveFocus = useContextSelectAtom(context.formId, registerReceiveFocusAtom);
|
21
|
-
const internalClearError = useClearError(context);
|
11
|
+
const state = useFormState(formId);
|
12
|
+
const { clearError: internalClearError, setTouched, validateField, } = useFormHelpers(formId);
|
13
|
+
const { registerReceiveFocus } = useFormAtomValue(formPropsAtom(context.formId));
|
22
14
|
const clearError = useCallback((...names) => {
|
23
15
|
names.forEach((name) => {
|
24
16
|
internalClearError(name);
|
25
17
|
});
|
26
18
|
}, [internalClearError]);
|
27
19
|
return {
|
28
|
-
|
29
|
-
|
30
|
-
isValid,
|
31
|
-
defaultValues,
|
32
|
-
clearError,
|
33
|
-
fieldErrors: fieldErrors !== null && fieldErrors !== void 0 ? fieldErrors : {},
|
34
|
-
action,
|
35
|
-
setFieldTouched,
|
36
|
-
touchedFields,
|
20
|
+
...state,
|
21
|
+
setFieldTouched: setTouched,
|
37
22
|
validateField,
|
23
|
+
clearError,
|
38
24
|
registerReceiveFocus,
|
39
25
|
};
|
40
26
|
};
|
package/build/ValidatedForm.js
CHANGED
@@ -116,18 +116,12 @@ function formEventProxy(event) {
|
|
116
116
|
},
|
117
117
|
});
|
118
118
|
}
|
119
|
-
const useFormAtom = (formId) => {
|
120
|
-
const formAtom = (0, state_1.formRegistry)(formId);
|
121
|
-
(0, react_2.useEffect)(() => () => state_1.formRegistry.remove(formId), [formId]);
|
122
|
-
return formAtom;
|
123
|
-
};
|
124
119
|
/**
|
125
120
|
* The primary form component of `remix-validated-form`.
|
126
121
|
*/
|
127
122
|
function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues: unMemoizedDefaults, formRef: formRefProp, onReset, subaction, resetAfterSubmit = false, disableFocusOnError, method, replace, id, ...rest }) {
|
128
123
|
var _a;
|
129
124
|
const formId = useFormId(id);
|
130
|
-
const formAtom = useFormAtom(formId);
|
131
125
|
const providedDefaultValues = (0, util_1.useDeepEqualsMemo)(unMemoizedDefaults);
|
132
126
|
const contextValue = (0, react_2.useMemo)(() => ({
|
133
127
|
formId,
|
@@ -142,25 +136,30 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
142
136
|
const formRef = (0, react_2.useRef)(null);
|
143
137
|
const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : react_1.Form;
|
144
138
|
const submit = (0, react_1.useSubmit)();
|
145
|
-
const
|
146
|
-
const
|
147
|
-
const
|
148
|
-
const
|
149
|
-
const
|
150
|
-
const
|
151
|
-
const
|
152
|
-
const
|
139
|
+
const setFieldErrors = (0, hooks_2.useFormUpdateAtom)((0, state_1.fieldErrorsAtom)(formId));
|
140
|
+
const setFieldError = (0, hooks_2.useFormUpdateAtom)((0, state_1.setFieldErrorAtom)(formId));
|
141
|
+
const reset = (0, hooks_2.useFormUpdateAtom)((0, state_1.resetAtom)(formId));
|
142
|
+
const startSubmit = (0, hooks_2.useFormUpdateAtom)((0, state_1.startSubmitAtom)(formId));
|
143
|
+
const endSubmit = (0, hooks_2.useFormUpdateAtom)((0, state_1.endSubmitAtom)(formId));
|
144
|
+
const syncFormProps = (0, hooks_2.useFormUpdateAtom)((0, state_1.formPropsAtom)(formId));
|
145
|
+
const setHydrated = (0, hooks_2.useFormUpdateAtom)((0, state_1.isHydratedAtom)(formId));
|
146
|
+
const setFormElementInState = (0, hooks_2.useFormUpdateAtom)((0, state_1.formElementAtom)(formId));
|
147
|
+
(0, react_2.useEffect)(() => {
|
148
|
+
setHydrated(true);
|
149
|
+
return () => (0, state_1.cleanupFormState)(formId);
|
150
|
+
}, [formId, setHydrated]);
|
151
|
+
const validateField = (0, react_2.useCallback)(async (field) => {
|
153
152
|
(0, tiny_invariant_1.default)(formRef.current, "Cannot find reference to form");
|
154
|
-
const { error } = await validator.validateField(getDataFromForm(formRef.current),
|
153
|
+
const { error } = await validator.validateField(getDataFromForm(formRef.current), field);
|
155
154
|
if (error) {
|
156
|
-
|
155
|
+
setFieldError({ field, error });
|
157
156
|
return error;
|
158
157
|
}
|
159
158
|
else {
|
160
|
-
|
159
|
+
setFieldError({ field, error: undefined });
|
161
160
|
return null;
|
162
161
|
}
|
163
|
-
}, [
|
162
|
+
}, [setFieldError, validator]);
|
164
163
|
const customFocusHandlers = (0, MultiValueMap_1.useMultiValueMap)();
|
165
164
|
const registerReceiveFocus = (0, react_2.useCallback)((fieldName, handler) => {
|
166
165
|
customFocusHandlers().add(fieldName, handler);
|
@@ -169,33 +168,29 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
169
168
|
};
|
170
169
|
}, [customFocusHandlers]);
|
171
170
|
(0, util_1.useIsomorphicLayoutEffect)(() => {
|
172
|
-
|
173
|
-
|
171
|
+
var _a;
|
172
|
+
syncFormProps({
|
174
173
|
action,
|
175
|
-
defaultValues: providedDefaultValues !== null && providedDefaultValues !== void 0 ? providedDefaultValues : backendDefaultValues,
|
174
|
+
defaultValues: (_a = providedDefaultValues !== null && providedDefaultValues !== void 0 ? providedDefaultValues : backendDefaultValues) !== null && _a !== void 0 ? _a : {},
|
176
175
|
subaction,
|
177
176
|
validateField,
|
178
177
|
registerReceiveFocus,
|
179
178
|
});
|
180
179
|
}, [
|
181
180
|
action,
|
182
|
-
formAtom,
|
183
181
|
providedDefaultValues,
|
184
182
|
registerReceiveFocus,
|
185
183
|
subaction,
|
186
|
-
|
184
|
+
syncFormProps,
|
187
185
|
validateField,
|
188
186
|
backendDefaultValues,
|
189
187
|
]);
|
190
188
|
(0, react_2.useEffect)(() => {
|
191
189
|
var _a;
|
192
|
-
setFieldErrors({
|
193
|
-
|
194
|
-
formAtom,
|
195
|
-
});
|
196
|
-
}, [backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors, formAtom, setFieldErrors]);
|
190
|
+
setFieldErrors((_a = backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors) !== null && _a !== void 0 ? _a : {});
|
191
|
+
}, [backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors, setFieldErrors, setFieldError]);
|
197
192
|
(0, submissionCallbacks_1.useSubmitComplete)(hasActiveSubmission, () => {
|
198
|
-
endSubmit(
|
193
|
+
endSubmit();
|
199
194
|
});
|
200
195
|
let clickedButtonRef = react_2.default.useRef();
|
201
196
|
(0, react_2.useEffect)(() => {
|
@@ -218,11 +213,11 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
218
213
|
};
|
219
214
|
}, []);
|
220
215
|
const handleSubmit = async (e) => {
|
221
|
-
startSubmit(
|
216
|
+
startSubmit();
|
222
217
|
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
223
218
|
if (result.error) {
|
224
|
-
endSubmit(
|
225
|
-
setFieldErrors(
|
219
|
+
endSubmit();
|
220
|
+
setFieldErrors(result.error.fieldErrors);
|
226
221
|
if (!disableFocusOnError) {
|
227
222
|
focusFirstInvalidInput(result.error.fieldErrors, customFocusHandlers(), formRef.current);
|
228
223
|
}
|
@@ -231,7 +226,7 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
231
226
|
const eventProxy = formEventProxy(e);
|
232
227
|
await (onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, eventProxy));
|
233
228
|
if (eventProxy.defaultPrevented) {
|
234
|
-
endSubmit(
|
229
|
+
endSubmit();
|
235
230
|
return;
|
236
231
|
}
|
237
232
|
// We deviate from the remix code here a bit because of our async submit.
|
@@ -249,14 +244,14 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
249
244
|
clickedButtonRef.current = null;
|
250
245
|
}
|
251
246
|
};
|
252
|
-
return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
|
247
|
+
return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp, setFormElementInState]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
|
253
248
|
e.preventDefault();
|
254
249
|
handleSubmit(e);
|
255
250
|
}, onReset: (event) => {
|
256
251
|
onReset === null || onReset === void 0 ? void 0 : onReset(event);
|
257
252
|
if (event.defaultPrevented)
|
258
253
|
return;
|
259
|
-
reset(
|
254
|
+
reset();
|
260
255
|
}, children: (0, jsx_runtime_1.jsxs)(formContext_1.InternalFormContext.Provider, { value: contextValue, children: [(0, jsx_runtime_1.jsx)(FormResetter, { formRef: formRef, resetAfterSubmit: resetAfterSubmit }, void 0), subaction && ((0, jsx_runtime_1.jsx)("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), id && (0, jsx_runtime_1.jsx)("input", { type: "hidden", value: id, name: constants_1.FORM_ID_FIELD }, void 0), children] }, void 0) }, void 0));
|
261
256
|
}
|
262
257
|
exports.ValidatedForm = ValidatedForm;
|
package/build/hooks.js
CHANGED
@@ -14,7 +14,7 @@ const state_1 = require("./internal/state");
|
|
14
14
|
*/
|
15
15
|
const useIsSubmitting = (formId) => {
|
16
16
|
const formContext = (0, hooks_1.useInternalFormContext)(formId, "useIsSubmitting");
|
17
|
-
return (0, hooks_1.
|
17
|
+
return (0, hooks_1.useFormAtomValue)((0, state_1.isSubmittingAtom)(formContext.formId));
|
18
18
|
};
|
19
19
|
exports.useIsSubmitting = useIsSubmitting;
|
20
20
|
/**
|
@@ -24,23 +24,20 @@ exports.useIsSubmitting = useIsSubmitting;
|
|
24
24
|
*/
|
25
25
|
const useIsValid = (formId) => {
|
26
26
|
const formContext = (0, hooks_1.useInternalFormContext)(formId, "useIsValid");
|
27
|
-
return (0, hooks_1.
|
27
|
+
return (0, hooks_1.useFormAtomValue)((0, state_1.isValidAtom)(formContext.formId));
|
28
28
|
};
|
29
29
|
exports.useIsValid = useIsValid;
|
30
30
|
/**
|
31
31
|
* Provides the data and helpers necessary to set up a field.
|
32
32
|
*/
|
33
33
|
const useField = (name, options) => {
|
34
|
-
const {
|
34
|
+
const { formId: providedFormId, handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
|
35
35
|
const formContext = (0, hooks_1.useInternalFormContext)(providedFormId, "useField");
|
36
36
|
const defaultValue = (0, hooks_1.useFieldDefaultValue)(name, formContext);
|
37
|
-
const touched = (0, hooks_1.useFieldTouched)(name, formContext);
|
38
|
-
const error = (0, hooks_1.useFieldError)(name, formContext);
|
39
|
-
const
|
40
|
-
const
|
41
|
-
const hasBeenSubmitted = (0, hooks_1.useContextSelectAtom)(formContext.formId, state_1.hasBeenSubmittedAtom);
|
42
|
-
const validateField = (0, hooks_1.useContextSelectAtom)(formContext.formId, state_1.validateFieldAtom);
|
43
|
-
const registerReceiveFocus = (0, hooks_1.useContextSelectAtom)(formContext.formId, state_1.registerReceiveFocusAtom);
|
37
|
+
const [touched, setTouched] = (0, hooks_1.useFieldTouched)(name, formContext);
|
38
|
+
const [error, setError] = (0, hooks_1.useFieldError)(name, formContext);
|
39
|
+
const hasBeenSubmitted = (0, hooks_1.useFormAtomValue)((0, state_1.hasBeenSubmittedAtom)(formContext.formId));
|
40
|
+
const { validateField, registerReceiveFocus } = (0, hooks_1.useFormAtomValue)((0, state_1.formPropsAtom)(formContext.formId));
|
44
41
|
(0, react_1.useEffect)(() => {
|
45
42
|
if (handleReceiveFocus)
|
46
43
|
return registerReceiveFocus(name, handleReceiveFocus);
|
@@ -48,13 +45,13 @@ const useField = (name, options) => {
|
|
48
45
|
const field = (0, react_1.useMemo)(() => {
|
49
46
|
const helpers = {
|
50
47
|
error,
|
51
|
-
clearError: () =>
|
48
|
+
clearError: () => setError(undefined),
|
52
49
|
validate: () => {
|
53
50
|
validateField(name);
|
54
51
|
},
|
55
52
|
defaultValue,
|
56
53
|
touched,
|
57
|
-
setTouched
|
54
|
+
setTouched,
|
58
55
|
};
|
59
56
|
const getInputProps = (0, getInputProps_1.createGetInputProps)({
|
60
57
|
...helpers,
|
@@ -70,12 +67,12 @@ const useField = (name, options) => {
|
|
70
67
|
error,
|
71
68
|
defaultValue,
|
72
69
|
touched,
|
70
|
+
setTouched,
|
73
71
|
name,
|
74
72
|
hasBeenSubmitted,
|
75
73
|
options === null || options === void 0 ? void 0 : options.validationBehavior,
|
76
|
-
|
74
|
+
setError,
|
77
75
|
validateField,
|
78
|
-
setTouched,
|
79
76
|
]);
|
80
77
|
return field;
|
81
78
|
};
|