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
package/build/internal/state.js
CHANGED
@@ -3,90 +3,74 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
-
exports.
|
6
|
+
exports.fieldDefaultValueAtom = exports.fieldErrorAtom = exports.fieldTouchedAtom = exports.setFieldErrorAtom = exports.setTouchedAtom = exports.endSubmitAtom = exports.startSubmitAtom = exports.resetAtom = exports.isValidAtom = exports.cleanupFormState = exports.formElementAtom = exports.formPropsAtom = exports.touchedFieldsAtom = exports.fieldErrorsAtom = exports.hasBeenSubmittedAtom = exports.isSubmittingAtom = exports.isHydratedAtom = exports.ATOM_SCOPE = void 0;
|
7
7
|
const jotai_1 = require("jotai");
|
8
|
-
const immer_1 = require("jotai/immer");
|
9
8
|
const utils_1 = require("jotai/utils");
|
10
|
-
const
|
9
|
+
const omit_1 = __importDefault(require("lodash/omit"));
|
10
|
+
const atomUtils_1 = require("./state/atomUtils");
|
11
11
|
exports.ATOM_SCOPE = Symbol("remix-validated-form-scope");
|
12
|
-
exports.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
formId: typeof formId === "string" ? formId : undefined,
|
19
|
-
// Will change upon hydration -- these will never actually be used
|
12
|
+
exports.isHydratedAtom = (0, atomUtils_1.formAtomFamily)(false);
|
13
|
+
exports.isSubmittingAtom = (0, atomUtils_1.formAtomFamily)(false);
|
14
|
+
exports.hasBeenSubmittedAtom = (0, atomUtils_1.formAtomFamily)(false);
|
15
|
+
exports.fieldErrorsAtom = (0, atomUtils_1.formAtomFamily)({});
|
16
|
+
exports.touchedFieldsAtom = (0, atomUtils_1.formAtomFamily)({});
|
17
|
+
exports.formPropsAtom = (0, atomUtils_1.formAtomFamily)({
|
20
18
|
validateField: () => Promise.resolve(null),
|
21
19
|
registerReceiveFocus: () => () => { },
|
20
|
+
defaultValues: {},
|
21
|
+
});
|
22
|
+
exports.formElementAtom = (0, atomUtils_1.formAtomFamily)(null);
|
23
|
+
//// Everything below is derived from the above
|
24
|
+
const cleanupFormState = (formId) => {
|
25
|
+
[
|
26
|
+
exports.isHydratedAtom,
|
27
|
+
exports.isSubmittingAtom,
|
28
|
+
exports.hasBeenSubmittedAtom,
|
29
|
+
exports.fieldErrorsAtom,
|
30
|
+
exports.touchedFieldsAtom,
|
31
|
+
exports.formPropsAtom,
|
32
|
+
].forEach((formAtom) => formAtom.remove(formId));
|
33
|
+
};
|
34
|
+
exports.cleanupFormState = cleanupFormState;
|
35
|
+
exports.isValidAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)((get) => Object.keys(get((0, exports.fieldErrorsAtom)(formId))).length === 0));
|
36
|
+
exports.resetAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)(null, (_get, set) => {
|
37
|
+
set((0, exports.fieldErrorsAtom)(formId), {});
|
38
|
+
set((0, exports.touchedFieldsAtom)(formId), {});
|
39
|
+
set((0, exports.hasBeenSubmittedAtom)(formId), false);
|
22
40
|
}));
|
23
|
-
|
24
|
-
exports.
|
25
|
-
|
26
|
-
exports.fieldTouchedAtom = fieldTouchedAtom;
|
27
|
-
const fieldDefaultValueAtom = (name) => (formAtom) => (0, utils_1.selectAtom)(formAtom, (formState) => formState.defaultValues && (0, get_1.default)(formState.defaultValues, name));
|
28
|
-
exports.fieldDefaultValueAtom = fieldDefaultValueAtom;
|
29
|
-
// Selector atoms
|
30
|
-
const formSelectorAtom = (selector) => (formAtom) => (0, utils_1.selectAtom)(formAtom, selector);
|
31
|
-
exports.formSelectorAtom = formSelectorAtom;
|
32
|
-
exports.fieldErrorsAtom = (0, exports.formSelectorAtom)((state) => state.fieldErrors);
|
33
|
-
exports.touchedFieldsAtom = (0, exports.formSelectorAtom)((state) => state.touchedFields);
|
34
|
-
exports.actionAtom = (0, exports.formSelectorAtom)((state) => state.action);
|
35
|
-
exports.hasBeenSubmittedAtom = (0, exports.formSelectorAtom)((state) => state.hasBeenSubmitted);
|
36
|
-
exports.validateFieldAtom = (0, exports.formSelectorAtom)((state) => state.validateField);
|
37
|
-
exports.registerReceiveFocusAtom = (0, exports.formSelectorAtom)((state) => state.registerReceiveFocus);
|
38
|
-
exports.isSubmittingAtom = (0, exports.formSelectorAtom)((state) => state.isSubmitting);
|
39
|
-
exports.defaultValuesAtom = (0, exports.formSelectorAtom)((state) => state.defaultValues);
|
40
|
-
exports.isValidAtom = (0, exports.formSelectorAtom)((state) => { var _a; return Object.keys((_a = state.fieldErrors) !== null && _a !== void 0 ? _a : {}).length === 0; });
|
41
|
-
exports.isHydratedAtom = (0, exports.formSelectorAtom)((state) => state.hydrated);
|
42
|
-
exports.clearErrorAtom = (0, jotai_1.atom)(null, (get, set, { name, formAtom }) => set(formAtom, (state) => {
|
43
|
-
var _a;
|
44
|
-
(_a = state.fieldErrors) === null || _a === void 0 ? true : delete _a[name];
|
45
|
-
return state;
|
41
|
+
exports.startSubmitAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)(null, (_get, set) => {
|
42
|
+
set((0, exports.isSubmittingAtom)(formId), true);
|
43
|
+
set((0, exports.hasBeenSubmittedAtom)(formId), true);
|
46
44
|
}));
|
47
|
-
exports.
|
48
|
-
|
49
|
-
state.fieldErrors = {};
|
50
|
-
state.fieldErrors[name] = error;
|
51
|
-
return state;
|
45
|
+
exports.endSubmitAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)(null, (_get, set) => {
|
46
|
+
set((0, exports.isSubmittingAtom)(formId), false);
|
52
47
|
}));
|
53
|
-
exports.
|
54
|
-
|
55
|
-
|
48
|
+
exports.setTouchedAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)(null, (get, set, { field, touched }) => {
|
49
|
+
const prev = get((0, exports.touchedFieldsAtom)(formId));
|
50
|
+
if (prev[field] !== touched) {
|
51
|
+
set((0, exports.touchedFieldsAtom)(formId), {
|
52
|
+
...prev,
|
53
|
+
[field]: touched,
|
54
|
+
});
|
55
|
+
}
|
56
56
|
}));
|
57
|
-
exports.
|
58
|
-
|
59
|
-
|
57
|
+
exports.setFieldErrorAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)(null, (get, set, { field, error }) => {
|
58
|
+
const prev = get((0, exports.fieldErrorsAtom)(formId));
|
59
|
+
if (error === undefined && field in prev) {
|
60
|
+
set((0, exports.fieldErrorsAtom)(formId), (0, omit_1.default)(prev, field));
|
61
|
+
}
|
62
|
+
if (error !== undefined && prev[field] !== error) {
|
63
|
+
set((0, exports.fieldErrorsAtom)(formId), {
|
64
|
+
...get((0, exports.fieldErrorsAtom)(formId)),
|
65
|
+
[field]: error,
|
66
|
+
});
|
67
|
+
}
|
60
68
|
}));
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
});
|
69
|
-
exports.startSubmitAtom = (0, jotai_1.atom)(null, (get, set, { formAtom }) => {
|
70
|
-
set(formAtom, (state) => {
|
71
|
-
state.hasBeenSubmitted = true;
|
72
|
-
state.isSubmitting = true;
|
73
|
-
return state;
|
74
|
-
});
|
75
|
-
});
|
76
|
-
exports.endSubmitAtom = (0, jotai_1.atom)(null, (get, set, { formAtom }) => {
|
77
|
-
set(formAtom, (state) => {
|
78
|
-
state.isSubmitting = false;
|
79
|
-
return state;
|
80
|
-
});
|
81
|
-
});
|
82
|
-
exports.syncFormContextAtom = (0, jotai_1.atom)(null, (get, set, { defaultValues, action, subaction, formAtom, validateField, registerReceiveFocus, }) => {
|
83
|
-
set(formAtom, (state) => {
|
84
|
-
state.defaultValues = defaultValues;
|
85
|
-
state.action = action;
|
86
|
-
state.subaction = subaction;
|
87
|
-
state.registerReceiveFocus = registerReceiveFocus;
|
88
|
-
state.validateField = validateField;
|
89
|
-
state.hydrated = true;
|
90
|
-
return state;
|
91
|
-
});
|
92
|
-
});
|
69
|
+
//// Field specific
|
70
|
+
exports.fieldTouchedAtom = (0, atomUtils_1.fieldAtomFamily)(({ formId, field }) => (0, jotai_1.atom)((get) => get((0, exports.touchedFieldsAtom)(formId))[field], (_get, set, touched) => {
|
71
|
+
set((0, exports.setTouchedAtom)(formId), { field, touched });
|
72
|
+
}));
|
73
|
+
exports.fieldErrorAtom = (0, atomUtils_1.fieldAtomFamily)(({ formId, field }) => (0, jotai_1.atom)((get) => get((0, exports.fieldErrorsAtom)(formId))[field], (_get, set, error) => {
|
74
|
+
set((0, exports.setFieldErrorAtom)(formId), { field, error });
|
75
|
+
}));
|
76
|
+
exports.fieldDefaultValueAtom = (0, atomUtils_1.fieldAtomFamily)(({ formId, field }) => (0, utils_1.selectAtom)((0, exports.formPropsAtom)(formId), (state) => state.defaultValues[field]));
|
@@ -0,0 +1,20 @@
|
|
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>(name: string, options?: BaseWatchOptions<T> & {
|
13
|
+
repeatable?: false;
|
14
|
+
}): T;
|
15
|
+
<T>(name: string, options?: BaseWatchOptions<T> & {
|
16
|
+
repeatable: true;
|
17
|
+
}): T[];
|
18
|
+
};
|
19
|
+
export declare const useWatch: UseWatchType;
|
20
|
+
export {};
|
@@ -0,0 +1,126 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.useWatch = void 0;
|
7
|
+
const jotai_1 = require("jotai");
|
8
|
+
const utils_1 = require("jotai/utils");
|
9
|
+
const isNaN_1 = __importDefault(require("lodash/isNaN"));
|
10
|
+
const react_1 = require("react");
|
11
|
+
const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
|
12
|
+
const hooks_1 = require("./hooks");
|
13
|
+
const elementUtils_1 = require("./logic/elementUtils");
|
14
|
+
const state_1 = require("./state");
|
15
|
+
const atomUtils_1 = require("./state/atomUtils");
|
16
|
+
const controlledFields_1 = require("./state/controlledFields");
|
17
|
+
const emptyAtom = (0, jotai_1.atom)(undefined); // Always empty -- just used as a default value
|
18
|
+
const watchControlledFieldAtom = (0, atomUtils_1.fieldAtomFamily)(({ field, formId }) => (0, utils_1.selectAtom)((0, controlledFields_1.controlledFieldsAtom)(formId), (fields) => {
|
19
|
+
var _a;
|
20
|
+
return ({
|
21
|
+
present: field in fields,
|
22
|
+
valueAtom: (_a = fields[field]) !== null && _a !== void 0 ? _a : emptyAtom,
|
23
|
+
});
|
24
|
+
}));
|
25
|
+
const defaultParse = ({ value, type, isRepeated, checked, }) => {
|
26
|
+
if (type === "number") {
|
27
|
+
const result = Number(value);
|
28
|
+
if ((0, isNaN_1.default)(result))
|
29
|
+
throw new Error("Value is not a number");
|
30
|
+
return result;
|
31
|
+
}
|
32
|
+
if (type === "checkbox" && !isRepeated) {
|
33
|
+
return checked;
|
34
|
+
}
|
35
|
+
return value;
|
36
|
+
};
|
37
|
+
const getInputValues = (node, multipleInputs = false) => {
|
38
|
+
if (!node)
|
39
|
+
return [];
|
40
|
+
if (node instanceof RadioNodeList) {
|
41
|
+
return [...node].flatMap((el) => getInputValues(el, true));
|
42
|
+
}
|
43
|
+
if ((0, elementUtils_1.isMultiselect)(node)) {
|
44
|
+
return [
|
45
|
+
{
|
46
|
+
type: "select",
|
47
|
+
value: [...node.options]
|
48
|
+
.filter((opt) => opt.selected)
|
49
|
+
.map((opt) => opt.value),
|
50
|
+
},
|
51
|
+
];
|
52
|
+
}
|
53
|
+
if ((0, elementUtils_1.isCheckbox)(node)) {
|
54
|
+
if (node.checked || !multipleInputs)
|
55
|
+
return [{ type: "checkbox", value: node.value, checked: node.checked }];
|
56
|
+
return [];
|
57
|
+
}
|
58
|
+
if ((0, elementUtils_1.isRadio)(node)) {
|
59
|
+
if (node.checked)
|
60
|
+
return [{ type: "radio", value: node.value, checked: node.checked }];
|
61
|
+
return [];
|
62
|
+
}
|
63
|
+
const input = node;
|
64
|
+
return [{ type: input.type, value: input.value, checked: input.checked }];
|
65
|
+
};
|
66
|
+
const useWatch = (field, { formId, parse = defaultParse, repeatable = false, } = {}) => {
|
67
|
+
const context = (0, hooks_1.useInternalFormContext)(formId, "useWatch");
|
68
|
+
const defaultValue = (0, hooks_1.useFieldDefaultValue)(field, context);
|
69
|
+
const hasSynced = (0, react_1.useRef)(false);
|
70
|
+
const [inputValues, setValue] = (0, react_1.useState)([]);
|
71
|
+
const formElement = (0, hooks_1.useFormAtomValue)((0, state_1.formElementAtom)(context.formId));
|
72
|
+
const controlledField = (0, hooks_1.useFormAtomValue)(watchControlledFieldAtom({ formId: context.formId, field }));
|
73
|
+
const controlledValue = (0, hooks_1.useFormAtomValue)(controlledField.valueAtom);
|
74
|
+
const shouldSyncNativeInputValue = !controlledField.present && formElement;
|
75
|
+
const syncFieldValue = (0, react_1.useCallback)(() => {
|
76
|
+
(0, tiny_invariant_1.default)(formElement, `Unable to find form element for form. Watching field ${field}`);
|
77
|
+
hasSynced.current = true;
|
78
|
+
// We pull the values out using `form.elements` instead of `FormData`
|
79
|
+
// so that we can access the `type` of the input.
|
80
|
+
setValue(getInputValues(formElement.elements.namedItem(field)));
|
81
|
+
}, [field, formElement]);
|
82
|
+
// Should set the field values after the initial render
|
83
|
+
(0, react_1.useEffect)(() => {
|
84
|
+
if (shouldSyncNativeInputValue)
|
85
|
+
syncFieldValue();
|
86
|
+
}, [
|
87
|
+
controlledField.present,
|
88
|
+
formElement,
|
89
|
+
shouldSyncNativeInputValue,
|
90
|
+
syncFieldValue,
|
91
|
+
]);
|
92
|
+
(0, react_1.useEffect)(() => {
|
93
|
+
if (shouldSyncNativeInputValue) {
|
94
|
+
const listener = async (event) => {
|
95
|
+
if (!(event.target instanceof HTMLElement))
|
96
|
+
return;
|
97
|
+
const target = event.target;
|
98
|
+
const { form: targetForm, name: targetName } = target;
|
99
|
+
if (targetForm === formElement && targetName === field) {
|
100
|
+
syncFieldValue();
|
101
|
+
}
|
102
|
+
};
|
103
|
+
window.addEventListener("change", listener);
|
104
|
+
window.addEventListener("input", listener);
|
105
|
+
return () => {
|
106
|
+
window.removeEventListener("change", listener);
|
107
|
+
window.removeEventListener("input", listener);
|
108
|
+
};
|
109
|
+
}
|
110
|
+
}, [field, formElement, shouldSyncNativeInputValue, syncFieldValue]);
|
111
|
+
const parsedValue = (0, react_1.useMemo)(() => {
|
112
|
+
const parsed = inputValues.map(({ type, value, checked }) => parse({
|
113
|
+
value,
|
114
|
+
type,
|
115
|
+
isRepeated: inputValues.length > 1 || repeatable,
|
116
|
+
checked,
|
117
|
+
}));
|
118
|
+
return parsed.length > 1 || repeatable ? parsed : parsed[0];
|
119
|
+
}, [parse, repeatable, inputValues]);
|
120
|
+
if (controlledField.present)
|
121
|
+
return controlledValue;
|
122
|
+
if (!hasSynced.current)
|
123
|
+
return defaultValue !== null && defaultValue !== void 0 ? defaultValue : (repeatable ? [] : undefined);
|
124
|
+
return parsedValue;
|
125
|
+
};
|
126
|
+
exports.useWatch = useWatch;
|
@@ -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,59 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useFormHelpers = exports.useFormState = void 0;
|
4
|
+
const react_1 = require("react");
|
5
|
+
const hooks_1 = require("../internal/hooks");
|
6
|
+
const state_1 = require("../internal/state");
|
7
|
+
/**
|
8
|
+
* Returns information about the form.
|
9
|
+
*
|
10
|
+
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
11
|
+
*/
|
12
|
+
const useFormState = (formId) => {
|
13
|
+
const formContext = (0, hooks_1.useInternalFormContext)(formId, "useIsValid");
|
14
|
+
const formProps = (0, hooks_1.useFormAtomValue)((0, state_1.formPropsAtom)(formContext.formId));
|
15
|
+
const isSubmitting = (0, hooks_1.useFormAtomValue)((0, state_1.isSubmittingAtom)(formContext.formId));
|
16
|
+
const hasBeenSubmitted = (0, hooks_1.useFormAtomValue)((0, state_1.hasBeenSubmittedAtom)(formContext.formId));
|
17
|
+
const touchedFields = (0, hooks_1.useFormAtomValue)((0, state_1.touchedFieldsAtom)(formContext.formId));
|
18
|
+
const isValid = (0, hooks_1.useFormAtomValue)((0, state_1.isValidAtom)(formContext.formId));
|
19
|
+
const defaultValuesToUse = (0, hooks_1.useDefaultValuesForForm)(formContext);
|
20
|
+
const hydratedDefaultValues = defaultValuesToUse.hydrateTo(formProps.defaultValues);
|
21
|
+
const fieldErrorsFromState = (0, hooks_1.useFormAtomValue)((0, state_1.fieldErrorsAtom)(formContext.formId));
|
22
|
+
const fieldErrorsToUse = (0, hooks_1.useFieldErrorsForForm)(formContext);
|
23
|
+
const hydratedFieldErrors = fieldErrorsToUse.hydrateTo(fieldErrorsFromState);
|
24
|
+
return (0, react_1.useMemo)(() => ({
|
25
|
+
...formProps,
|
26
|
+
defaultValues: hydratedDefaultValues,
|
27
|
+
fieldErrors: hydratedFieldErrors !== null && hydratedFieldErrors !== void 0 ? hydratedFieldErrors : {},
|
28
|
+
hasBeenSubmitted,
|
29
|
+
isSubmitting,
|
30
|
+
touchedFields,
|
31
|
+
isValid,
|
32
|
+
}), [
|
33
|
+
formProps,
|
34
|
+
hasBeenSubmitted,
|
35
|
+
hydratedDefaultValues,
|
36
|
+
hydratedFieldErrors,
|
37
|
+
isSubmitting,
|
38
|
+
isValid,
|
39
|
+
touchedFields,
|
40
|
+
]);
|
41
|
+
};
|
42
|
+
exports.useFormState = useFormState;
|
43
|
+
/**
|
44
|
+
* Returns helpers that can be used to update the form state.
|
45
|
+
*
|
46
|
+
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
47
|
+
*/
|
48
|
+
const useFormHelpers = (formId) => {
|
49
|
+
const formContext = (0, hooks_1.useInternalFormContext)(formId, "useFormHelpers");
|
50
|
+
const setTouched = (0, hooks_1.useSetTouched)(formContext);
|
51
|
+
const { validateField } = (0, hooks_1.useFormAtomValue)((0, state_1.formPropsAtom)(formContext.formId));
|
52
|
+
const clearError = (0, hooks_1.useClearError)(formContext);
|
53
|
+
return (0, react_1.useMemo)(() => ({
|
54
|
+
setTouched,
|
55
|
+
validateField,
|
56
|
+
clearError,
|
57
|
+
}), [clearError, setTouched, validateField]);
|
58
|
+
};
|
59
|
+
exports.useFormHelpers = useFormHelpers;
|
@@ -2,42 +2,28 @@
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.useFormContext = void 0;
|
4
4
|
const react_1 = require("react");
|
5
|
-
const hooks_1 = require("./hooks");
|
6
|
-
const hooks_2 = require("./internal/hooks");
|
5
|
+
const hooks_1 = require("./internal/hooks");
|
7
6
|
const state_1 = require("./internal/state");
|
7
|
+
const formStateHooks_1 = require("./unreleased/formStateHooks");
|
8
8
|
/**
|
9
9
|
* Provides access to some of the internal state of the form.
|
10
10
|
*/
|
11
11
|
const useFormContext = (formId) => {
|
12
12
|
// Try to access context so we get our error specific to this hook if it's not there
|
13
|
-
const context = (0,
|
14
|
-
const
|
15
|
-
const
|
16
|
-
const
|
17
|
-
const isValid = (0, hooks_1.useIsValid)(formId);
|
18
|
-
const defaultValues = (0, hooks_2.useHydratableSelector)(context, state_1.defaultValuesAtom, (0, hooks_2.useDefaultValuesForForm)(context));
|
19
|
-
const fieldErrors = (0, hooks_2.useHydratableSelector)(context, state_1.fieldErrorsAtom, (0, hooks_2.useFieldErrorsForForm)(context));
|
20
|
-
const setFieldTouched = (0, hooks_2.useSetTouched)(context);
|
21
|
-
const touchedFields = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.touchedFieldsAtom);
|
22
|
-
const validateField = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.validateFieldAtom);
|
23
|
-
const registerReceiveFocus = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.registerReceiveFocusAtom);
|
24
|
-
const internalClearError = (0, hooks_2.useClearError)(context);
|
13
|
+
const context = (0, hooks_1.useInternalFormContext)(formId, "useFormContext");
|
14
|
+
const state = (0, formStateHooks_1.useFormState)(formId);
|
15
|
+
const { clearError: internalClearError, setTouched, validateField, } = (0, formStateHooks_1.useFormHelpers)(formId);
|
16
|
+
const { registerReceiveFocus } = (0, hooks_1.useFormAtomValue)((0, state_1.formPropsAtom)(context.formId));
|
25
17
|
const clearError = (0, react_1.useCallback)((...names) => {
|
26
18
|
names.forEach((name) => {
|
27
19
|
internalClearError(name);
|
28
20
|
});
|
29
21
|
}, [internalClearError]);
|
30
22
|
return {
|
31
|
-
|
32
|
-
|
33
|
-
isValid,
|
34
|
-
defaultValues,
|
35
|
-
clearError,
|
36
|
-
fieldErrors: fieldErrors !== null && fieldErrors !== void 0 ? fieldErrors : {},
|
37
|
-
action,
|
38
|
-
setFieldTouched,
|
39
|
-
touchedFields,
|
23
|
+
...state,
|
24
|
+
setFieldTouched: setTouched,
|
40
25
|
validateField,
|
26
|
+
clearError,
|
41
27
|
registerReceiveFocus,
|
42
28
|
};
|
43
29
|
};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "remix-validated-form",
|
3
|
-
"version": "4.1.
|
3
|
+
"version": "4.1.6",
|
4
4
|
"description": "Form component and utils for easy form validation in remix",
|
5
5
|
"browser": "./browser/index.js",
|
6
6
|
"main": "./build/index.js",
|
@@ -48,7 +48,6 @@
|
|
48
48
|
"typescript": "^4.5.3"
|
49
49
|
},
|
50
50
|
"dependencies": {
|
51
|
-
"immer": "^9.0.12",
|
52
51
|
"jotai": "^1.5.3",
|
53
52
|
"lodash": "^4.17.21",
|
54
53
|
"tiny-invariant": "^1.2.0"
|
package/src/ValidatedForm.tsx
CHANGED
@@ -25,15 +25,16 @@ import {
|
|
25
25
|
} from "./internal/hooks";
|
26
26
|
import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
|
27
27
|
import {
|
28
|
-
|
29
|
-
clearErrorAtom,
|
28
|
+
cleanupFormState,
|
30
29
|
endSubmitAtom,
|
31
|
-
|
32
|
-
|
30
|
+
fieldErrorsAtom,
|
31
|
+
formElementAtom,
|
32
|
+
formPropsAtom,
|
33
|
+
isHydratedAtom,
|
33
34
|
resetAtom,
|
34
|
-
|
35
|
+
setFieldErrorAtom,
|
35
36
|
startSubmitAtom,
|
36
|
-
|
37
|
+
SyncedFormProps,
|
37
38
|
} from "./internal/state";
|
38
39
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
39
40
|
import {
|
@@ -192,12 +193,6 @@ function formEventProxy<T extends object>(event: T): T {
|
|
192
193
|
}) as T;
|
193
194
|
}
|
194
195
|
|
195
|
-
const useFormAtom = (formId: string | symbol) => {
|
196
|
-
const formAtom = formRegistry(formId);
|
197
|
-
useEffect(() => () => formRegistry.remove(formId), [formId]);
|
198
|
-
return formAtom;
|
199
|
-
};
|
200
|
-
|
201
196
|
/**
|
202
197
|
* The primary form component of `remix-validated-form`.
|
203
198
|
*/
|
@@ -219,7 +214,6 @@ export function ValidatedForm<DataType>({
|
|
219
214
|
...rest
|
220
215
|
}: FormProps<DataType>) {
|
221
216
|
const formId = useFormId(id);
|
222
|
-
const formAtom = useFormAtom(formId);
|
223
217
|
const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults);
|
224
218
|
const contextValue = useMemo<InternalFormContextValue>(
|
225
219
|
() => ({
|
@@ -238,73 +232,75 @@ export function ValidatedForm<DataType>({
|
|
238
232
|
const Form = fetcher?.Form ?? RemixForm;
|
239
233
|
|
240
234
|
const submit = useSubmit();
|
241
|
-
const
|
242
|
-
const
|
243
|
-
const
|
244
|
-
const
|
245
|
-
const
|
246
|
-
const
|
247
|
-
const
|
235
|
+
const setFieldErrors = useFormUpdateAtom(fieldErrorsAtom(formId));
|
236
|
+
const setFieldError = useFormUpdateAtom(setFieldErrorAtom(formId));
|
237
|
+
const reset = useFormUpdateAtom(resetAtom(formId));
|
238
|
+
const startSubmit = useFormUpdateAtom(startSubmitAtom(formId));
|
239
|
+
const endSubmit = useFormUpdateAtom(endSubmitAtom(formId));
|
240
|
+
const syncFormProps = useFormUpdateAtom(formPropsAtom(formId));
|
241
|
+
const setHydrated = useFormUpdateAtom(isHydratedAtom(formId));
|
242
|
+
const setFormElementInState = useFormUpdateAtom(formElementAtom(formId));
|
248
243
|
|
249
|
-
|
250
|
-
|
244
|
+
useEffect(() => {
|
245
|
+
setHydrated(true);
|
246
|
+
return () => cleanupFormState(formId);
|
247
|
+
}, [formId, setHydrated]);
|
248
|
+
|
249
|
+
const validateField: SyncedFormProps["validateField"] = useCallback(
|
250
|
+
async (field) => {
|
251
251
|
invariant(formRef.current, "Cannot find reference to form");
|
252
252
|
const { error } = await validator.validateField(
|
253
253
|
getDataFromForm(formRef.current),
|
254
|
-
|
254
|
+
field
|
255
255
|
);
|
256
256
|
|
257
257
|
if (error) {
|
258
|
-
|
258
|
+
setFieldError({ field, error });
|
259
259
|
return error;
|
260
260
|
} else {
|
261
|
-
|
261
|
+
setFieldError({ field, error: undefined });
|
262
262
|
return null;
|
263
263
|
}
|
264
264
|
},
|
265
|
-
[
|
265
|
+
[setFieldError, validator]
|
266
266
|
);
|
267
267
|
|
268
268
|
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
269
|
-
const registerReceiveFocus:
|
270
|
-
(
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
269
|
+
const registerReceiveFocus: SyncedFormProps["registerReceiveFocus"] =
|
270
|
+
useCallback(
|
271
|
+
(fieldName, handler) => {
|
272
|
+
customFocusHandlers().add(fieldName, handler);
|
273
|
+
return () => {
|
274
|
+
customFocusHandlers().remove(fieldName, handler);
|
275
|
+
};
|
276
|
+
},
|
277
|
+
[customFocusHandlers]
|
278
|
+
);
|
278
279
|
|
279
280
|
useLayoutEffect(() => {
|
280
|
-
|
281
|
-
formAtom,
|
281
|
+
syncFormProps({
|
282
282
|
action,
|
283
|
-
defaultValues: providedDefaultValues ?? backendDefaultValues,
|
283
|
+
defaultValues: providedDefaultValues ?? backendDefaultValues ?? {},
|
284
284
|
subaction,
|
285
285
|
validateField,
|
286
286
|
registerReceiveFocus,
|
287
287
|
});
|
288
288
|
}, [
|
289
289
|
action,
|
290
|
-
formAtom,
|
291
290
|
providedDefaultValues,
|
292
291
|
registerReceiveFocus,
|
293
292
|
subaction,
|
294
|
-
|
293
|
+
syncFormProps,
|
295
294
|
validateField,
|
296
295
|
backendDefaultValues,
|
297
296
|
]);
|
298
297
|
|
299
298
|
useEffect(() => {
|
300
|
-
setFieldErrors({
|
301
|
-
|
302
|
-
formAtom,
|
303
|
-
});
|
304
|
-
}, [backendError?.fieldErrors, formAtom, setFieldErrors]);
|
299
|
+
setFieldErrors(backendError?.fieldErrors ?? {});
|
300
|
+
}, [backendError?.fieldErrors, setFieldErrors, setFieldError]);
|
305
301
|
|
306
302
|
useSubmitComplete(hasActiveSubmission, () => {
|
307
|
-
endSubmit(
|
303
|
+
endSubmit();
|
308
304
|
});
|
309
305
|
|
310
306
|
let clickedButtonRef = React.useRef<any>();
|
@@ -334,11 +330,11 @@ export function ValidatedForm<DataType>({
|
|
334
330
|
}, []);
|
335
331
|
|
336
332
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
337
|
-
startSubmit(
|
333
|
+
startSubmit();
|
338
334
|
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
339
335
|
if (result.error) {
|
340
|
-
endSubmit(
|
341
|
-
setFieldErrors(
|
336
|
+
endSubmit();
|
337
|
+
setFieldErrors(result.error.fieldErrors);
|
342
338
|
if (!disableFocusOnError) {
|
343
339
|
focusFirstInvalidInput(
|
344
340
|
result.error.fieldErrors,
|
@@ -350,7 +346,7 @@ export function ValidatedForm<DataType>({
|
|
350
346
|
const eventProxy = formEventProxy(e);
|
351
347
|
await onSubmit?.(result.data, eventProxy);
|
352
348
|
if (eventProxy.defaultPrevented) {
|
353
|
-
endSubmit(
|
349
|
+
endSubmit();
|
354
350
|
return;
|
355
351
|
}
|
356
352
|
|
@@ -372,7 +368,7 @@ export function ValidatedForm<DataType>({
|
|
372
368
|
|
373
369
|
return (
|
374
370
|
<Form
|
375
|
-
ref={mergeRefs([formRef, formRefProp])}
|
371
|
+
ref={mergeRefs([formRef, formRefProp, setFormElementInState])}
|
376
372
|
{...rest}
|
377
373
|
id={id}
|
378
374
|
action={action}
|
@@ -385,7 +381,7 @@ export function ValidatedForm<DataType>({
|
|
385
381
|
onReset={(event) => {
|
386
382
|
onReset?.(event);
|
387
383
|
if (event.defaultPrevented) return;
|
388
|
-
reset(
|
384
|
+
reset();
|
389
385
|
}}
|
390
386
|
>
|
391
387
|
<InternalFormContext.Provider value={contextValue}>
|