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.
- 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 +36 -38
- 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 +38 -40
- 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 +59 -71
- 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/.turbo/turbo-build.log
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
[2K[1G[2m$ npm run build:browser && npm run build:main[22m
|
2
2
|
|
3
|
-
> remix-validated-form@4.1.
|
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.
|
7
|
+
> remix-validated-form@4.1.4 build:main
|
8
8
|
> tsc --module CommonJS --outDir ./build
|
9
9
|
|
package/browser/ValidatedForm.js
CHANGED
@@ -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 {
|
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
|
121
|
-
const
|
122
|
-
const
|
123
|
-
const
|
124
|
-
const
|
125
|
-
const
|
126
|
-
const
|
127
|
-
const
|
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),
|
128
|
+
const { error } = await validator.validateField(getDataFromForm(formRef.current), field);
|
130
129
|
if (error) {
|
131
|
-
|
130
|
+
setFieldError({ field, error });
|
132
131
|
return error;
|
133
132
|
}
|
134
133
|
else {
|
135
|
-
|
134
|
+
setFieldError({ field, error: undefined });
|
136
135
|
return null;
|
137
136
|
}
|
138
|
-
}, [
|
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
|
-
|
148
|
-
|
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
|
-
|
159
|
+
syncFormProps,
|
162
160
|
validateField,
|
163
161
|
backendDefaultValues,
|
164
162
|
]);
|
165
163
|
useEffect(() => {
|
166
164
|
var _a;
|
167
|
-
setFieldErrors({
|
168
|
-
|
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(
|
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(
|
191
|
+
startSubmit();
|
197
192
|
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
198
193
|
if (result.error) {
|
199
|
-
endSubmit(
|
200
|
-
setFieldErrors(
|
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(
|
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(
|
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,
|
4
|
-
import { hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom,
|
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
|
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
|
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 {
|
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
|
35
|
-
const
|
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: () =>
|
43
|
+
clearError: () => setError(undefined),
|
47
44
|
validate: () => {
|
48
45
|
validateField(name);
|
49
46
|
},
|
50
47
|
defaultValue,
|
51
48
|
touched,
|
52
|
-
setTouched
|
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
|
-
|
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
|
-
|
49
|
-
inputProps.defaultChecked = getCheckboxDefaultChecked(value, defaultValue);
|
40
|
+
inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue);
|
50
41
|
}
|
51
42
|
else if (props.type === "radio") {
|
52
|
-
|
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 {
|
6
|
-
declare
|
7
|
-
declare const
|
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) =>
|
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) =>
|
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: (
|
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
|
20
|
-
export declare const
|
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
|
5
|
-
import
|
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 {
|
9
|
-
|
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 =
|
48
|
-
return
|
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 =
|
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
|
75
|
-
if (errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.repopulateFields)
|
76
|
-
|
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 = (
|
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
|
-
|
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
|
-
|
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
|
99
|
-
|
100
|
-
|
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 = (
|
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 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";
|