remix-validated-form 4.1.4 → 4.1.5

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 +36 -38
  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 +38 -40
  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 +59 -71
  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
@@ -1,9 +1,9 @@
1
1
  $ npm run build:browser && npm run build:main
2
2
 
3
- > remix-validated-form@4.1.2 build:browser
3
+ > remix-validated-form@4.1.4 build:browser
4
4
  > tsc --module ESNext --outDir ./browser
5
5
 
6
6
 
7
- > remix-validated-form@4.1.2 build:main
7
+ > remix-validated-form@4.1.4 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
@@ -8,7 +8,7 @@ import { FORM_ID_FIELD } from "./internal/constants";
8
8
  import { InternalFormContext, } from "./internal/formContext";
9
9
  import { useDefaultValuesFromLoader, useErrorResponseForForm, useFormUpdateAtom, useHasActiveFormSubmit, } from "./internal/hooks";
10
10
  import { useMultiValueMap } from "./internal/MultiValueMap";
11
- import { addErrorAtom, clearErrorAtom, endSubmitAtom, formRegistry, resetAtom, setFieldErrorsAtom, startSubmitAtom, syncFormContextAtom, } from "./internal/state";
11
+ import { cleanupFormState, endSubmitAtom, fieldErrorsAtom, formElementAtom, formPropsAtom, isHydratedAtom, resetAtom, setFieldErrorAtom, startSubmitAtom, } from "./internal/state";
12
12
  import { useSubmitComplete } from "./internal/submissionCallbacks";
13
13
  import { mergeRefs, useDeepEqualsMemo, useIsomorphicLayoutEffect as useLayoutEffect, } from "./internal/util";
14
14
  const getDataFromForm = (el) => new FormData(el);
@@ -91,18 +91,12 @@ function formEventProxy(event) {
91
91
  },
92
92
  });
93
93
  }
94
- const useFormAtom = (formId) => {
95
- const formAtom = formRegistry(formId);
96
- useEffect(() => () => formRegistry.remove(formId), [formId]);
97
- return formAtom;
98
- };
99
94
  /**
100
95
  * The primary form component of `remix-validated-form`.
101
96
  */
102
97
  export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues: unMemoizedDefaults, formRef: formRefProp, onReset, subaction, resetAfterSubmit = false, disableFocusOnError, method, replace, id, ...rest }) {
103
98
  var _a;
104
99
  const formId = useFormId(id);
105
- const formAtom = useFormAtom(formId);
106
100
  const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults);
107
101
  const contextValue = useMemo(() => ({
108
102
  formId,
@@ -117,25 +111,30 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
117
111
  const formRef = useRef(null);
118
112
  const Form = (_a = fetcher === null || fetcher === void 0 ? void 0 : fetcher.Form) !== null && _a !== void 0 ? _a : RemixForm;
119
113
  const submit = useSubmit();
120
- const clearError = useFormUpdateAtom(clearErrorAtom);
121
- const addError = useFormUpdateAtom(addErrorAtom);
122
- const setFieldErrors = useFormUpdateAtom(setFieldErrorsAtom);
123
- const reset = useFormUpdateAtom(resetAtom);
124
- const startSubmit = useFormUpdateAtom(startSubmitAtom);
125
- const endSubmit = useFormUpdateAtom(endSubmitAtom);
126
- const syncFormContext = useFormUpdateAtom(syncFormContextAtom);
127
- const validateField = useCallback(async (fieldName) => {
114
+ const setFieldErrors = useFormUpdateAtom(fieldErrorsAtom(formId));
115
+ const setFieldError = useFormUpdateAtom(setFieldErrorAtom(formId));
116
+ const reset = useFormUpdateAtom(resetAtom(formId));
117
+ const startSubmit = useFormUpdateAtom(startSubmitAtom(formId));
118
+ const endSubmit = useFormUpdateAtom(endSubmitAtom(formId));
119
+ const syncFormProps = useFormUpdateAtom(formPropsAtom(formId));
120
+ const setHydrated = useFormUpdateAtom(isHydratedAtom(formId));
121
+ const setFormElementInState = useFormUpdateAtom(formElementAtom(formId));
122
+ useEffect(() => {
123
+ setHydrated(true);
124
+ return () => cleanupFormState(formId);
125
+ }, [formId, setHydrated]);
126
+ const validateField = useCallback(async (field) => {
128
127
  invariant(formRef.current, "Cannot find reference to form");
129
- const { error } = await validator.validateField(getDataFromForm(formRef.current), fieldName);
128
+ const { error } = await validator.validateField(getDataFromForm(formRef.current), field);
130
129
  if (error) {
131
- addError({ formAtom, name: fieldName, error });
130
+ setFieldError({ field, error });
132
131
  return error;
133
132
  }
134
133
  else {
135
- clearError({ name: fieldName, formAtom });
134
+ setFieldError({ field, error: undefined });
136
135
  return null;
137
136
  }
138
- }, [addError, clearError, formAtom, validator]);
137
+ }, [setFieldError, validator]);
139
138
  const customFocusHandlers = useMultiValueMap();
140
139
  const registerReceiveFocus = useCallback((fieldName, handler) => {
141
140
  customFocusHandlers().add(fieldName, handler);
@@ -144,33 +143,29 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
144
143
  };
145
144
  }, [customFocusHandlers]);
146
145
  useLayoutEffect(() => {
147
- syncFormContext({
148
- formAtom,
146
+ var _a;
147
+ syncFormProps({
149
148
  action,
150
- defaultValues: providedDefaultValues !== null && providedDefaultValues !== void 0 ? providedDefaultValues : backendDefaultValues,
149
+ defaultValues: (_a = providedDefaultValues !== null && providedDefaultValues !== void 0 ? providedDefaultValues : backendDefaultValues) !== null && _a !== void 0 ? _a : {},
151
150
  subaction,
152
151
  validateField,
153
152
  registerReceiveFocus,
154
153
  });
155
154
  }, [
156
155
  action,
157
- formAtom,
158
156
  providedDefaultValues,
159
157
  registerReceiveFocus,
160
158
  subaction,
161
- syncFormContext,
159
+ syncFormProps,
162
160
  validateField,
163
161
  backendDefaultValues,
164
162
  ]);
165
163
  useEffect(() => {
166
164
  var _a;
167
- setFieldErrors({
168
- fieldErrors: (_a = backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors) !== null && _a !== void 0 ? _a : {},
169
- formAtom,
170
- });
171
- }, [backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors, formAtom, setFieldErrors]);
165
+ setFieldErrors((_a = backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors) !== null && _a !== void 0 ? _a : {});
166
+ }, [backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors, setFieldErrors, setFieldError]);
172
167
  useSubmitComplete(hasActiveSubmission, () => {
173
- endSubmit({ formAtom });
168
+ endSubmit();
174
169
  });
175
170
  let clickedButtonRef = React.useRef();
176
171
  useEffect(() => {
@@ -193,11 +188,11 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
193
188
  };
194
189
  }, []);
195
190
  const handleSubmit = async (e) => {
196
- startSubmit({ formAtom });
191
+ startSubmit();
197
192
  const result = await validator.validate(getDataFromForm(e.currentTarget));
198
193
  if (result.error) {
199
- endSubmit({ formAtom });
200
- setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
194
+ endSubmit();
195
+ setFieldErrors(result.error.fieldErrors);
201
196
  if (!disableFocusOnError) {
202
197
  focusFirstInvalidInput(result.error.fieldErrors, customFocusHandlers(), formRef.current);
203
198
  }
@@ -206,7 +201,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
206
201
  const eventProxy = formEventProxy(e);
207
202
  await (onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, eventProxy));
208
203
  if (eventProxy.defaultPrevented) {
209
- endSubmit({ formAtom });
204
+ endSubmit();
210
205
  return;
211
206
  }
212
207
  // We deviate from the remix code here a bit because of our async submit.
@@ -224,13 +219,13 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
224
219
  clickedButtonRef.current = null;
225
220
  }
226
221
  };
227
- return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
222
+ return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp, setFormElementInState]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
228
223
  e.preventDefault();
229
224
  handleSubmit(e);
230
225
  }, onReset: (event) => {
231
226
  onReset === null || onReset === void 0 ? void 0 : onReset(event);
232
227
  if (event.defaultPrevented)
233
228
  return;
234
- reset({ formAtom });
229
+ reset();
235
230
  }, children: _jsxs(InternalFormContext.Provider, { value: contextValue, children: [_jsx(FormResetter, { formRef: formRef, resetAfterSubmit: resetAfterSubmit }, void 0), subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), id && _jsx("input", { type: "hidden", value: id, name: FORM_ID_FIELD }, void 0), children] }, void 0) }, void 0));
236
231
  }
package/browser/hooks.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo } from "react";
2
2
  import { createGetInputProps, } from "./internal/getInputProps";
3
- import { useInternalFormContext, useFieldTouched, useFieldError, useFieldDefaultValue, useContextSelectAtom, useClearError, useSetTouched, } from "./internal/hooks";
4
- import { hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, registerReceiveFocusAtom, validateFieldAtom, } from "./internal/state";
3
+ import { useInternalFormContext, useFieldTouched, useFieldError, useFormAtomValue, useFieldDefaultValue, } from "./internal/hooks";
4
+ import { formPropsAtom, hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, } from "./internal/state";
5
5
  /**
6
6
  * Returns whether or not the parent form is currently being submitted.
7
7
  * This is different from remix's `useTransition().submission` in that it
@@ -11,7 +11,7 @@ import { hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, registerReceiveFoc
11
11
  */
12
12
  export const useIsSubmitting = (formId) => {
13
13
  const formContext = useInternalFormContext(formId, "useIsSubmitting");
14
- return useContextSelectAtom(formContext.formId, isSubmittingAtom);
14
+ return useFormAtomValue(isSubmittingAtom(formContext.formId));
15
15
  };
16
16
  /**
17
17
  * Returns whether or not the current form is valid.
@@ -20,22 +20,19 @@ export const useIsSubmitting = (formId) => {
20
20
  */
21
21
  export const useIsValid = (formId) => {
22
22
  const formContext = useInternalFormContext(formId, "useIsValid");
23
- return useContextSelectAtom(formContext.formId, isValidAtom);
23
+ return useFormAtomValue(isValidAtom(formContext.formId));
24
24
  };
25
25
  /**
26
26
  * Provides the data and helpers necessary to set up a field.
27
27
  */
28
28
  export const useField = (name, options) => {
29
- const { handleReceiveFocus, formId: providedFormId } = options !== null && options !== void 0 ? options : {};
29
+ const { formId: providedFormId, handleReceiveFocus } = options !== null && options !== void 0 ? options : {};
30
30
  const formContext = useInternalFormContext(providedFormId, "useField");
31
31
  const defaultValue = useFieldDefaultValue(name, formContext);
32
- const touched = useFieldTouched(name, formContext);
33
- const error = useFieldError(name, formContext);
34
- const clearError = useClearError(formContext);
35
- const setTouched = useSetTouched(formContext);
36
- const hasBeenSubmitted = useContextSelectAtom(formContext.formId, hasBeenSubmittedAtom);
37
- const validateField = useContextSelectAtom(formContext.formId, validateFieldAtom);
38
- const registerReceiveFocus = useContextSelectAtom(formContext.formId, registerReceiveFocusAtom);
32
+ const [touched, setTouched] = useFieldTouched(name, formContext);
33
+ const [error, setError] = useFieldError(name, formContext);
34
+ const hasBeenSubmitted = useFormAtomValue(hasBeenSubmittedAtom(formContext.formId));
35
+ const { validateField, registerReceiveFocus } = useFormAtomValue(formPropsAtom(formContext.formId));
39
36
  useEffect(() => {
40
37
  if (handleReceiveFocus)
41
38
  return registerReceiveFocus(name, handleReceiveFocus);
@@ -43,13 +40,13 @@ export const useField = (name, options) => {
43
40
  const field = useMemo(() => {
44
41
  const helpers = {
45
42
  error,
46
- clearError: () => clearError(name),
43
+ clearError: () => setError(undefined),
47
44
  validate: () => {
48
45
  validateField(name);
49
46
  },
50
47
  defaultValue,
51
48
  touched,
52
- setTouched: (touched) => setTouched(name, touched),
49
+ setTouched,
53
50
  };
54
51
  const getInputProps = createGetInputProps({
55
52
  ...helpers,
@@ -65,12 +62,12 @@ export const useField = (name, options) => {
65
62
  error,
66
63
  defaultValue,
67
64
  touched,
65
+ setTouched,
68
66
  name,
69
67
  hasBeenSubmitted,
70
68
  options === null || options === void 0 ? void 0 : options.validationBehavior,
71
- clearError,
69
+ setError,
72
70
  validateField,
73
- setTouched,
74
71
  ]);
75
72
  return field;
76
73
  };
@@ -0,0 +1,105 @@
1
+ import { Atom } from "jotai";
2
+ import { InternalFormId } from "./state/atomUtils";
3
+ export declare const unregisterAtomsFamily: {
4
+ (param: InternalFormId): Atom<Atom<any>[]> & {
5
+ write: (get: {
6
+ <Value>(atom: Atom<Value | Promise<Value>>): Value;
7
+ <Value_1>(atom: Atom<Promise<Value_1>>): Value_1;
8
+ <Value_2>(atom: Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
9
+ } & {
10
+ <Value_3>(atom: Atom<Value_3 | Promise<Value_3>>, options: {
11
+ unstable_promise: true;
12
+ }): Value_3 | Promise<Value_3>;
13
+ <Value_4>(atom: Atom<Promise<Value_4>>, options: {
14
+ unstable_promise: true;
15
+ }): Value_4 | Promise<Value_4>;
16
+ <Value_5>(atom: Atom<Value_5>, options: {
17
+ unstable_promise: true;
18
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
19
+ }, set: {
20
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
21
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
22
+ }, update: Atom<any>[] | ((prev: Atom<any>[]) => Atom<any>[])) => void;
23
+ onMount?: (<S extends (update: Atom<any>[] | ((prev: Atom<any>[]) => Atom<any>[])) => void>(setAtom: S) => void | (() => void)) | undefined;
24
+ } & {
25
+ init: Atom<any>[];
26
+ };
27
+ remove(param: InternalFormId): void;
28
+ setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
29
+ };
30
+ export declare const createCustomFormState: <T>(defaultValue: T) => {
31
+ __atomFamily: {
32
+ (param: InternalFormId): Atom<T> & {
33
+ write: (get: {
34
+ <Value>(atom: Atom<Value | Promise<Value>>): Value;
35
+ <Value_1>(atom: Atom<Promise<Value_1>>): Value_1;
36
+ <Value_2>(atom: Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
37
+ } & {
38
+ <Value_3>(atom: Atom<Value_3 | Promise<Value_3>>, options: {
39
+ unstable_promise: true;
40
+ }): Value_3 | Promise<Value_3>;
41
+ <Value_4>(atom: Atom<Promise<Value_4>>, options: {
42
+ unstable_promise: true;
43
+ }): Value_4 | Promise<Value_4>;
44
+ <Value_5>(atom: Atom<Value_5>, options: {
45
+ unstable_promise: true;
46
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
47
+ }, set: {
48
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
49
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
50
+ }, update: T | ((prev: T) => T)) => void;
51
+ onMount?: (<S extends import("jotai/core/atom").SetAtom<T | ((prev: T) => T), void>>(setAtom: S) => void | (() => void)) | undefined;
52
+ } & {
53
+ init: T;
54
+ };
55
+ remove(param: InternalFormId): void;
56
+ setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
57
+ };
58
+ __registerAtom: Atom<null> & {
59
+ write: (get: {
60
+ <Value>(atom: Atom<Value | Promise<Value>>): Value;
61
+ <Value_1>(atom: Atom<Promise<Value_1>>): Value_1;
62
+ <Value_2>(atom: Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
63
+ } & {
64
+ <Value_3>(atom: Atom<Value_3 | Promise<Value_3>>, options: {
65
+ unstable_promise: true;
66
+ }): Value_3 | Promise<Value_3>;
67
+ <Value_4>(atom: Atom<Promise<Value_4>>, options: {
68
+ unstable_promise: true;
69
+ }): Value_4 | Promise<Value_4>;
70
+ <Value_5>(atom: Atom<Value_5>, options: {
71
+ unstable_promise: true;
72
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
73
+ }, set: {
74
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
75
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
76
+ }, update: InternalFormId) => void;
77
+ onMount?: (<S_1 extends (update: InternalFormId) => void>(setAtom: S_1) => void | (() => void)) | undefined;
78
+ } & {
79
+ init: null;
80
+ };
81
+ __unregisterAtom: Atom<null> & {
82
+ write: (get: {
83
+ <Value>(atom: Atom<Value | Promise<Value>>): Value;
84
+ <Value_1>(atom: Atom<Promise<Value_1>>): Value_1;
85
+ <Value_2>(atom: Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
86
+ } & {
87
+ <Value_3>(atom: Atom<Value_3 | Promise<Value_3>>, options: {
88
+ unstable_promise: true;
89
+ }): Value_3 | Promise<Value_3>;
90
+ <Value_4>(atom: Atom<Promise<Value_4>>, options: {
91
+ unstable_promise: true;
92
+ }): Value_4 | Promise<Value_4>;
93
+ <Value_5>(atom: Atom<Value_5>, options: {
94
+ unstable_promise: true;
95
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
96
+ }, set: {
97
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
98
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
99
+ }, update: InternalFormId) => void;
100
+ onMount?: (<S_1 extends (update: InternalFormId) => void>(setAtom: S_1) => void | (() => void)) | undefined;
101
+ } & {
102
+ init: null;
103
+ };
104
+ };
105
+ export declare const useCustomFormState: <T>(state: ReturnType<typeof createCustomFormState>, formId: InternalFormId) => readonly [T, (value: T) => void];
@@ -0,0 +1,46 @@
1
+ import { atom } from "jotai";
2
+ import { useAtomCallback } from "jotai/utils";
3
+ import { useCallback, useEffect } from "react";
4
+ import { useFormAtom } from "./hooks";
5
+ import { formAtomFamily } from "./state/atomUtils";
6
+ export const unregisterAtomsFamily = formAtomFamily([]);
7
+ export const createCustomFormState = (defaultValue) => {
8
+ const atomFamily = formAtomFamily(defaultValue);
9
+ const refCountFamily = formAtomFamily(0);
10
+ const registerAtom = atom(null, (get, set, formId) => {
11
+ const refCountAtom = refCountFamily(formId);
12
+ set(refCountAtom, (prev) => prev + 1);
13
+ const newRefCount = get(refCountAtom);
14
+ if (newRefCount === 1) {
15
+ set(unregisterAtomsFamily(formId), (prev) => [...prev, unregisterAtom]);
16
+ }
17
+ });
18
+ const unregisterAtom = atom(null, (get, set, formId) => {
19
+ const refCountAtom = refCountFamily(formId);
20
+ set(refCountAtom, (prev) => prev - 1);
21
+ const newRefCount = get(refCountAtom);
22
+ if (newRefCount === 0) {
23
+ set(unregisterAtomsFamily(formId), (prev) => prev.filter((item) => item !== unregisterAtom));
24
+ atomFamily.remove(formId);
25
+ refCountFamily.remove(formId);
26
+ }
27
+ });
28
+ return {
29
+ __atomFamily: atomFamily,
30
+ __registerAtom: registerAtom,
31
+ __unregisterAtom: unregisterAtom,
32
+ };
33
+ };
34
+ export const useCustomFormState = (state, formId) => {
35
+ const atom = state.__atomFamily(formId);
36
+ const [value, setValue] = useFormAtom(atom);
37
+ const register = useAtomCallback(useCallback((_get, set) => set(state.__registerAtom, formId), [formId, state.__registerAtom]));
38
+ const unregister = useAtomCallback(useCallback((_get, set) => set(state.__unregisterAtom, formId), [formId, state.__unregisterAtom]));
39
+ useEffect(() => {
40
+ register();
41
+ return () => {
42
+ unregister();
43
+ };
44
+ }, [register, unregister]);
45
+ return [value, setValue];
46
+ };
@@ -1,25 +1,17 @@
1
1
  import omitBy from "lodash/omitBy";
2
+ import { getCheckboxChecked } from "./logic/getCheckboxChecked";
3
+ import { getRadioChecked } from "./logic/getRadioChecked";
2
4
  const defaultValidationBehavior = {
3
5
  initial: "onBlur",
4
6
  whenTouched: "onChange",
5
7
  whenSubmitted: "onChange",
6
8
  };
7
- const getCheckboxDefaultChecked = (value, defaultValue) => {
8
- if (Array.isArray(defaultValue))
9
- return defaultValue.includes(value);
10
- if (typeof defaultValue === "boolean")
11
- return defaultValue;
12
- if (typeof defaultValue === "string")
13
- return defaultValue === value;
14
- return undefined;
15
- };
16
9
  export const createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name, }) => {
17
10
  const validationBehaviors = {
18
11
  ...defaultValidationBehavior,
19
12
  ...validationBehavior,
20
13
  };
21
14
  return (props = {}) => {
22
- var _a, _b;
23
15
  const behavior = hasBeenSubmitted
24
16
  ? validationBehaviors.whenSubmitted
25
17
  : touched
@@ -45,12 +37,10 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
45
37
  name,
46
38
  };
47
39
  if (props.type === "checkbox") {
48
- const value = (_a = props.value) !== null && _a !== void 0 ? _a : "on";
49
- inputProps.defaultChecked = getCheckboxDefaultChecked(value, defaultValue);
40
+ inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue);
50
41
  }
51
42
  else if (props.type === "radio") {
52
- const value = (_b = props.value) !== null && _b !== void 0 ? _b : "on";
53
- inputProps.defaultChecked = defaultValue === value;
43
+ inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
54
44
  }
55
45
  else {
56
46
  inputProps.defaultValue = defaultValue;
@@ -1,22 +1,21 @@
1
- import { Atom } from "jotai";
1
+ import { Atom, WritableAtom } from "jotai";
2
2
  import { useUpdateAtom } from "jotai/utils";
3
- import { ValidationErrorResponseData } from "..";
3
+ import { FieldErrors, ValidationErrorResponseData } from "..";
4
4
  import { InternalFormContextValue } from "./formContext";
5
- import { FormAtom } from "./state";
6
- declare type FormSelectorAtomCreator<T> = (formState: FormAtom) => Atom<T>;
7
- declare const USE_HYDRATED_STATE: unique symbol;
5
+ import { Hydratable } from "./hydratable";
6
+ export declare const useFormUpdateAtom: typeof useUpdateAtom;
7
+ export declare const useFormAtom: <Value, Update, Result extends void | Promise<void>>(anAtom: WritableAtom<Value, Update, Result>) => [Value extends Promise<infer V> ? V : Value, import("jotai/core/atom").SetAtom<Update, Result>];
8
+ export declare const useFormAtomValue: <Value>(anAtom: Atom<Value>) => Value extends Promise<infer V> ? V : Value;
8
9
  export declare const useInternalFormContext: (formId?: string | symbol | undefined, hookName?: string | undefined) => InternalFormContextValue;
9
- export declare const useContextSelectAtom: <T>(formId: string | symbol, selectorAtomCreator: FormSelectorAtomCreator<T>) => T extends Promise<infer V> ? V : T;
10
- export declare const useHydratableSelector: <T, U>({ formId }: InternalFormContextValue, atomCreator: FormSelectorAtomCreator<T>, dataToUse: U | typeof USE_HYDRATED_STATE, selector?: (data: U) => T) => T | (T extends Promise<infer V> ? V : T);
11
10
  export declare function useErrorResponseForForm({ fetcher, subaction, formId, }: InternalFormContextValue): ValidationErrorResponseData | null;
12
- export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => import("..").FieldErrors | typeof USE_HYDRATED_STATE | undefined;
11
+ export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => Hydratable<FieldErrors | undefined>;
13
12
  export declare const useDefaultValuesFromLoader: ({ formId, }: InternalFormContextValue) => any;
14
- export declare const useDefaultValuesForForm: (context: InternalFormContextValue) => any;
13
+ export declare const useDefaultValuesForForm: (context: InternalFormContextValue) => Hydratable<{
14
+ [fieldName: string]: any;
15
+ }>;
15
16
  export declare const useHasActiveFormSubmit: ({ fetcher, }: InternalFormContextValue) => boolean;
16
- export declare const useFieldTouched: (name: string, { formId }: InternalFormContextValue) => boolean;
17
- export declare const useFieldError: (name: string, context: InternalFormContextValue) => string | undefined;
17
+ export declare const useFieldTouched: (field: string, { formId }: InternalFormContextValue) => [boolean, (update: boolean) => void];
18
+ export declare const useFieldError: (name: string, context: InternalFormContextValue) => readonly [string | undefined, (update?: string | undefined) => void];
18
19
  export declare const useFieldDefaultValue: (name: string, context: InternalFormContextValue) => any;
19
- export declare const useFormUpdateAtom: typeof useUpdateAtom;
20
- export declare const useClearError: (context: InternalFormContextValue) => (name: string) => void;
21
- export declare const useSetTouched: (context: InternalFormContextValue) => (name: string, touched: boolean) => void;
22
- export {};
20
+ export declare const useClearError: ({ formId }: InternalFormContextValue) => (name: string) => void;
21
+ export declare const useSetTouched: ({ formId }: InternalFormContextValue) => (name: string, touched: boolean) => void;
@@ -1,12 +1,16 @@
1
1
  import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
+ import { useAtom } from "jotai";
2
3
  import { useAtomValue, useUpdateAtom } from "jotai/utils";
3
4
  import lodashGet from "lodash/get";
4
- import identity from "lodash/identity";
5
- import { useCallback, useContext, useMemo } from "react";
5
+ import { useCallback, useContext } from "react";
6
+ import invariant from "tiny-invariant";
6
7
  import { formDefaultValuesKey } from "./constants";
7
8
  import { InternalFormContext } from "./formContext";
8
- import { ATOM_SCOPE, clearErrorAtom, fieldDefaultValueAtom, fieldErrorAtom, fieldTouchedAtom, formRegistry, isHydratedAtom, setTouchedAtom, } from "./state";
9
- const USE_HYDRATED_STATE = Symbol("USE_HYDRATED_STATE");
9
+ import { hydratable } from "./hydratable";
10
+ import { ATOM_SCOPE, fieldErrorAtom, fieldTouchedAtom, formPropsAtom, isHydratedAtom, setFieldErrorAtom, setTouchedAtom, } from "./state";
11
+ export const useFormUpdateAtom = (atom) => useUpdateAtom(atom, ATOM_SCOPE);
12
+ export const useFormAtom = (anAtom) => useAtom(anAtom, ATOM_SCOPE);
13
+ export const useFormAtomValue = (anAtom) => useAtomValue(anAtom, ATOM_SCOPE);
10
14
  export const useInternalFormContext = (formId, hookName) => {
11
15
  const formContext = useContext(InternalFormContext);
12
16
  if (formId)
@@ -15,15 +19,6 @@ export const useInternalFormContext = (formId, hookName) => {
15
19
  return formContext;
16
20
  throw new Error(`Unable to determine form for ${hookName}. Please use it inside a form or pass a 'formId'.`);
17
21
  };
18
- export const useContextSelectAtom = (formId, selectorAtomCreator) => {
19
- const formAtom = formRegistry(formId);
20
- const selectorAtom = useMemo(() => selectorAtomCreator(formAtom), [formAtom, selectorAtomCreator]);
21
- return useAtomValue(selectorAtom, ATOM_SCOPE);
22
- };
23
- export const useHydratableSelector = ({ formId }, atomCreator, dataToUse, selector = identity) => {
24
- const dataFromState = useContextSelectAtom(formId, atomCreator);
25
- return dataToUse === USE_HYDRATED_STATE ? dataFromState : selector(dataToUse);
26
- };
27
22
  export function useErrorResponseForForm({ fetcher, subaction, formId, }) {
28
23
  var _a;
29
24
  const actionData = useActionData();
@@ -44,8 +39,8 @@ export function useErrorResponseForForm({ fetcher, subaction, formId, }) {
44
39
  }
45
40
  export const useFieldErrorsForForm = (context) => {
46
41
  const response = useErrorResponseForForm(context);
47
- const hydrated = useContextSelectAtom(context.formId, isHydratedAtom);
48
- return hydrated ? USE_HYDRATED_STATE : response === null || response === void 0 ? void 0 : response.fieldErrors;
42
+ const hydrated = useFormAtomValue(isHydratedAtom(context.formId));
43
+ return hydratable.from(response === null || response === void 0 ? void 0 : response.fieldErrors, hydrated);
49
44
  };
50
45
  export const useDefaultValuesFromLoader = ({ formId, }) => {
51
46
  const matches = useMatches();
@@ -62,7 +57,7 @@ export const useDefaultValuesFromLoader = ({ formId, }) => {
62
57
  };
63
58
  export const useDefaultValuesForForm = (context) => {
64
59
  const { formId, defaultValuesProp } = context;
65
- const hydrated = useContextSelectAtom(formId, isHydratedAtom);
60
+ const hydrated = useFormAtomValue(isHydratedAtom(formId));
66
61
  const errorResponse = useErrorResponseForForm(context);
67
62
  const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
68
63
  // Typical flow is:
@@ -71,12 +66,14 @@ export const useDefaultValuesForForm = (context) => {
71
66
  // - State gets hydrated with default values
72
67
  // - After submit, we may need to use values from the error
73
68
  if (hydrated)
74
- return USE_HYDRATED_STATE;
75
- if (errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.repopulateFields)
76
- return errorResponse.repopulateFields;
69
+ return hydratable.hydratedData();
70
+ if (errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.repopulateFields) {
71
+ invariant(typeof errorResponse.repopulateFields === "object", "repopulateFields returned something other than an object");
72
+ return hydratable.serverData(errorResponse.repopulateFields);
73
+ }
77
74
  if (defaultValuesProp)
78
- return defaultValuesProp;
79
- return defaultValuesFromLoader;
75
+ return hydratable.serverData(defaultValuesProp);
76
+ return hydratable.serverData(defaultValuesFromLoader);
80
77
  };
81
78
  export const useHasActiveFormSubmit = ({ fetcher, }) => {
82
79
  const transition = useTransition();
@@ -85,26 +82,27 @@ export const useHasActiveFormSubmit = ({ fetcher, }) => {
85
82
  : !!transition.submission;
86
83
  return hasActiveSubmission;
87
84
  };
88
- export const useFieldTouched = (name, { formId }) => {
89
- const atomCreator = useMemo(() => fieldTouchedAtom(name), [name]);
90
- return useContextSelectAtom(formId, atomCreator);
91
- };
85
+ export const useFieldTouched = (field, { formId }) => useFormAtom(fieldTouchedAtom({ formId, field }));
92
86
  export const useFieldError = (name, context) => {
93
- return useHydratableSelector(context, useMemo(() => fieldErrorAtom(name), [name]), useFieldErrorsForForm(context), (fieldErrors) => fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors[name]);
87
+ const fieldErrors = useFieldErrorsForForm(context);
88
+ const [state, set] = useFormAtom(fieldErrorAtom({ formId: context.formId, field: name }));
89
+ return [
90
+ fieldErrors.map((fieldErrors) => fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors[name]).hydrateTo(state),
91
+ set,
92
+ ];
94
93
  };
95
94
  export const useFieldDefaultValue = (name, context) => {
96
- return useHydratableSelector(context, useMemo(() => fieldDefaultValueAtom(name), [name]), useDefaultValuesForForm(context), (val) => lodashGet(val, name));
95
+ const defaultValues = useDefaultValuesForForm(context);
96
+ const { defaultValues: state } = useFormAtomValue(formPropsAtom(context.formId));
97
+ return defaultValues
98
+ .map((val) => lodashGet(val, name))
99
+ .hydrateTo(state[name]);
97
100
  };
98
- export const useFormUpdateAtom = (atom) => useUpdateAtom(atom, ATOM_SCOPE);
99
- export const useClearError = (context) => {
100
- const clearError = useFormUpdateAtom(clearErrorAtom);
101
- return useCallback((name) => {
102
- clearError({ name, formAtom: formRegistry(context.formId) });
103
- }, [clearError, context.formId]);
101
+ export const useClearError = ({ formId }) => {
102
+ const updateError = useFormUpdateAtom(setFieldErrorAtom(formId));
103
+ return useCallback((name) => updateError({ field: name, error: undefined }), [updateError]);
104
104
  };
105
- export const useSetTouched = (context) => {
106
- const setTouched = useFormUpdateAtom(setTouchedAtom);
107
- return useCallback((name, touched) => {
108
- setTouched({ name, formAtom: formRegistry(context.formId), touched });
109
- }, [setTouched, context.formId]);
105
+ export const useSetTouched = ({ formId }) => {
106
+ const setTouched = useFormUpdateAtom(setTouchedAtom(formId));
107
+ return useCallback((name, touched) => setTouched({ field: name, touched }), [setTouched]);
110
108
  };
@@ -0,0 +1,3 @@
1
+ export declare const isMultiselect: (node: Node) => node is HTMLSelectElement;
2
+ export declare const isCheckbox: (node: Node | RadioNodeList) => node is HTMLInputElement;
3
+ export declare const isRadio: (node: Node) => node is HTMLInputElement;
@@ -0,0 +1,3 @@
1
+ export const isMultiselect = (node) => node instanceof HTMLSelectElement && node.multiple;
2
+ export const isCheckbox = (node) => node instanceof HTMLInputElement && node.type === "checkbox";
3
+ export const isRadio = (node) => node instanceof HTMLInputElement && node.type === "radio";