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.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/browser/ValidatedForm.js +31 -36
  3. package/browser/hooks.js +13 -16
  4. package/browser/internal/customState.d.ts +105 -0
  5. package/browser/internal/customState.js +46 -0
  6. package/browser/internal/getInputProps.js +4 -14
  7. package/browser/internal/hooks.d.ts +14 -15
  8. package/browser/internal/hooks.js +37 -39
  9. package/browser/internal/logic/elementUtils.d.ts +3 -0
  10. package/browser/internal/logic/elementUtils.js +3 -0
  11. package/browser/internal/logic/setInputValueInForm.js +9 -52
  12. package/browser/internal/setFieldValue.d.ts +0 -0
  13. package/browser/internal/setFieldValue.js +0 -0
  14. package/browser/internal/setFormValues.d.ts +0 -0
  15. package/browser/internal/setFormValues.js +0 -0
  16. package/browser/internal/state.d.ts +339 -238
  17. package/browser/internal/state.js +59 -72
  18. package/browser/internal/watch.d.ts +18 -0
  19. package/browser/internal/watch.js +122 -0
  20. package/browser/unreleased/formStateHooks.d.ts +39 -0
  21. package/browser/unreleased/formStateHooks.js +54 -0
  22. package/browser/userFacingFormContext.js +9 -23
  23. package/build/ValidatedForm.js +30 -35
  24. package/build/hooks.js +11 -14
  25. package/build/internal/getInputProps.js +4 -14
  26. package/build/internal/hooks.d.ts +14 -15
  27. package/build/internal/hooks.js +39 -41
  28. package/build/internal/logic/elementUtils.d.ts +3 -0
  29. package/build/internal/logic/elementUtils.js +9 -0
  30. package/build/internal/logic/setInputValueInForm.js +12 -55
  31. package/build/internal/setFormValues.d.ts +0 -0
  32. package/build/internal/setFormValues.js +0 -0
  33. package/build/internal/state/controlledFields.js +11 -2
  34. package/build/internal/state.d.ts +339 -238
  35. package/build/internal/state.js +61 -77
  36. package/build/internal/watch.d.ts +20 -0
  37. package/build/internal/watch.js +126 -0
  38. package/build/unreleased/formStateHooks.d.ts +39 -0
  39. package/build/unreleased/formStateHooks.js +59 -0
  40. package/build/userFacingFormContext.js +9 -23
  41. package/package.json +1 -2
  42. package/src/ValidatedForm.tsx +48 -52
  43. package/src/hooks.ts +15 -26
  44. package/src/internal/getInputProps.ts +4 -14
  45. package/src/internal/hooks.ts +60 -72
  46. package/src/internal/hydratable.ts +28 -0
  47. package/src/internal/logic/getCheckboxChecked.ts +10 -0
  48. package/src/internal/logic/getRadioChecked.ts +7 -0
  49. package/src/internal/state/atomUtils.ts +13 -0
  50. package/src/internal/state.ts +99 -177
  51. package/src/unreleased/formStateHooks.ts +113 -0
  52. package/src/userFacingFormContext.ts +14 -53
@@ -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.syncFormContextAtom = exports.endSubmitAtom = exports.startSubmitAtom = exports.resetAtom = exports.setTouchedAtom = exports.setFieldErrorsAtom = exports.addErrorAtom = exports.clearErrorAtom = exports.isHydratedAtom = exports.isValidAtom = exports.defaultValuesAtom = exports.isSubmittingAtom = exports.registerReceiveFocusAtom = exports.validateFieldAtom = exports.hasBeenSubmittedAtom = exports.actionAtom = exports.touchedFieldsAtom = exports.fieldErrorsAtom = exports.formSelectorAtom = exports.fieldDefaultValueAtom = exports.fieldTouchedAtom = exports.fieldErrorAtom = exports.formRegistry = exports.ATOM_SCOPE = void 0;
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 get_1 = __importDefault(require("lodash/get"));
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.formRegistry = (0, utils_1.atomFamily)((formId) => (0, immer_1.atomWithImmer)({
13
- hydrated: false,
14
- isSubmitting: false,
15
- hasBeenSubmitted: false,
16
- touchedFields: {},
17
- // The symbol version is just to keep things straight with the `atomFamily`
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
- const fieldErrorAtom = (name) => (formAtom) => (0, utils_1.selectAtom)(formAtom, (formState) => { var _a; return (_a = formState.fieldErrors) === null || _a === void 0 ? void 0 : _a[name]; });
24
- exports.fieldErrorAtom = fieldErrorAtom;
25
- const fieldTouchedAtom = (name) => (formAtom) => (0, utils_1.selectAtom)(formAtom, (formState) => formState.touchedFields[name]);
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.addErrorAtom = (0, jotai_1.atom)(null, (get, set, { name, formAtom, error }) => set(formAtom, (state) => {
48
- if (!state.fieldErrors)
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.setFieldErrorsAtom = (0, jotai_1.atom)(null, (get, set, { formAtom, fieldErrors }) => set(formAtom, (state) => {
54
- state.fieldErrors = fieldErrors;
55
- return state;
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.setTouchedAtom = (0, jotai_1.atom)(null, (get, set, { name, formAtom, touched }) => set(formAtom, (state) => {
58
- state.touchedFields[name] = touched;
59
- return state;
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
- exports.resetAtom = (0, jotai_1.atom)(null, (get, set, { formAtom }) => {
62
- set(formAtom, (state) => {
63
- state.fieldErrors = {};
64
- state.touchedFields = {};
65
- state.hasBeenSubmitted = false;
66
- return state;
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, hooks_2.useInternalFormContext)(formId, "useFormContext");
14
- const action = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.actionAtom);
15
- const isSubmitting = (0, hooks_1.useIsSubmitting)(formId);
16
- const hasBeenSubmitted = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.hasBeenSubmittedAtom);
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
- isSubmitting,
32
- hasBeenSubmitted,
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.4-beta.0",
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"
@@ -25,15 +25,16 @@ import {
25
25
  } from "./internal/hooks";
26
26
  import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
27
27
  import {
28
- addErrorAtom,
29
- clearErrorAtom,
28
+ cleanupFormState,
30
29
  endSubmitAtom,
31
- formRegistry,
32
- FormState,
30
+ fieldErrorsAtom,
31
+ formElementAtom,
32
+ formPropsAtom,
33
+ isHydratedAtom,
33
34
  resetAtom,
34
- setFieldErrorsAtom,
35
+ setFieldErrorAtom,
35
36
  startSubmitAtom,
36
- syncFormContextAtom,
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 clearError = useFormUpdateAtom(clearErrorAtom);
242
- const addError = useFormUpdateAtom(addErrorAtom);
243
- const setFieldErrors = useFormUpdateAtom(setFieldErrorsAtom);
244
- const reset = useFormUpdateAtom(resetAtom);
245
- const startSubmit = useFormUpdateAtom(startSubmitAtom);
246
- const endSubmit = useFormUpdateAtom(endSubmitAtom);
247
- const syncFormContext = useFormUpdateAtom(syncFormContextAtom);
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
- const validateField: FormState["validateField"] = useCallback(
250
- async (fieldName) => {
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
- fieldName as any
254
+ field
255
255
  );
256
256
 
257
257
  if (error) {
258
- addError({ formAtom, name: fieldName, error });
258
+ setFieldError({ field, error });
259
259
  return error;
260
260
  } else {
261
- clearError({ name: fieldName, formAtom });
261
+ setFieldError({ field, error: undefined });
262
262
  return null;
263
263
  }
264
264
  },
265
- [addError, clearError, formAtom, validator]
265
+ [setFieldError, validator]
266
266
  );
267
267
 
268
268
  const customFocusHandlers = useMultiValueMap<string, () => void>();
269
- const registerReceiveFocus: FormState["registerReceiveFocus"] = useCallback(
270
- (fieldName, handler) => {
271
- customFocusHandlers().add(fieldName, handler);
272
- return () => {
273
- customFocusHandlers().remove(fieldName, handler);
274
- };
275
- },
276
- [customFocusHandlers]
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
- syncFormContext({
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
- syncFormContext,
293
+ syncFormProps,
295
294
  validateField,
296
295
  backendDefaultValues,
297
296
  ]);
298
297
 
299
298
  useEffect(() => {
300
- setFieldErrors({
301
- fieldErrors: backendError?.fieldErrors ?? {},
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({ formAtom });
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({ formAtom });
333
+ startSubmit();
338
334
  const result = await validator.validate(getDataFromForm(e.currentTarget));
339
335
  if (result.error) {
340
- endSubmit({ formAtom });
341
- setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
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({ formAtom });
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({ formAtom });
384
+ reset();
389
385
  }}
390
386
  >
391
387
  <InternalFormContext.Provider value={contextValue}>