remix-validated-form 4.3.1-beta.0 → 4.4.0
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 +10 -10
- package/browser/ValidatedForm.js +16 -30
- package/browser/hooks.d.ts +1 -1
- package/browser/hooks.js +10 -9
- package/browser/internal/hooks.d.ts +20 -9
- package/browser/internal/hooks.js +32 -23
- package/browser/internal/logic/getRadioChecked.js +1 -1
- package/browser/internal/state/cleanup.d.ts +2 -0
- package/browser/internal/state/cleanup.js +6 -0
- package/browser/internal/state/controlledFieldStore.d.ts +24 -0
- package/browser/internal/state/controlledFieldStore.js +57 -0
- package/browser/internal/state/controlledFields.d.ts +3 -116
- package/browser/internal/state/controlledFields.js +25 -68
- package/browser/internal/state/createFormStore.d.ts +40 -0
- package/browser/internal/state/createFormStore.js +83 -0
- package/browser/internal/state/storeFamily.d.ts +9 -0
- package/browser/internal/state/storeFamily.js +18 -0
- package/browser/internal/state/storeHooks.d.ts +5 -0
- package/browser/internal/state/storeHooks.js +10 -0
- package/browser/unreleased/formStateHooks.d.ts +15 -0
- package/browser/unreleased/formStateHooks.js +23 -14
- package/browser/userFacingFormContext.d.ts +8 -0
- package/browser/userFacingFormContext.js +5 -4
- package/dist/remix-validated-form.cjs.js +17 -1
- package/dist/remix-validated-form.es.js +1033 -1724
- package/dist/remix-validated-form.umd.js +17 -1
- package/dist/types/hooks.d.ts +1 -1
- package/dist/types/internal/hooks.d.ts +20 -9
- package/dist/types/internal/state/cleanup.d.ts +2 -0
- package/dist/types/internal/state/controlledFieldStore.d.ts +24 -0
- package/dist/types/internal/state/controlledFields.d.ts +3 -116
- package/dist/types/internal/state/createFormStore.d.ts +40 -0
- package/dist/types/internal/state/storeFamily.d.ts +9 -0
- package/dist/types/internal/state/storeHooks.d.ts +5 -0
- package/dist/types/unreleased/formStateHooks.d.ts +15 -0
- package/dist/types/userFacingFormContext.d.ts +8 -0
- package/package.json +4 -3
- package/src/ValidatedForm.tsx +25 -47
- package/src/hooks.ts +15 -18
- package/src/internal/hooks.ts +69 -45
- package/src/internal/logic/getRadioChecked.ts +1 -1
- package/src/internal/state/cleanup.ts +8 -0
- package/src/internal/state/controlledFieldStore.ts +91 -0
- package/src/internal/state/controlledFields.ts +31 -123
- package/src/internal/state/createFormStore.ts +152 -0
- package/src/internal/state/storeFamily.ts +24 -0
- package/src/internal/state/storeHooks.ts +22 -0
- package/src/unreleased/formStateHooks.ts +50 -27
- package/src/userFacingFormContext.ts +17 -5
- package/dist/types/internal/reset.d.ts +0 -28
- package/dist/types/internal/state/atomUtils.d.ts +0 -38
- package/dist/types/internal/state.d.ts +0 -343
- package/src/internal/reset.ts +0 -26
- package/src/internal/state/atomUtils.ts +0 -13
- package/src/internal/state.ts +0 -124
package/src/ValidatedForm.tsx
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
|
2
|
-
import { useAtomCallback } from "jotai/utils";
|
3
2
|
import uniq from "lodash/uniq";
|
4
3
|
import React, {
|
5
4
|
ComponentProps,
|
@@ -11,7 +10,6 @@ import React, {
|
|
11
10
|
useRef,
|
12
11
|
useState,
|
13
12
|
} from "react";
|
14
|
-
import invariant from "tiny-invariant";
|
15
13
|
import { useIsSubmitting, useIsValid } from "./hooks";
|
16
14
|
import { FORM_ID_FIELD } from "./internal/constants";
|
17
15
|
import {
|
@@ -21,23 +19,16 @@ import {
|
|
21
19
|
import {
|
22
20
|
useDefaultValuesFromLoader,
|
23
21
|
useErrorResponseForForm,
|
24
|
-
useFormUpdateAtom,
|
25
22
|
useHasActiveFormSubmit,
|
23
|
+
useSetFieldErrors,
|
26
24
|
} from "./internal/hooks";
|
27
25
|
import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
|
28
|
-
import {
|
26
|
+
import { cleanupFormState } from "./internal/state/cleanup";
|
27
|
+
import { SyncedFormProps } from "./internal/state/createFormStore";
|
29
28
|
import {
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
formElementAtom,
|
34
|
-
formPropsAtom,
|
35
|
-
isHydratedAtom,
|
36
|
-
setFieldErrorAtom,
|
37
|
-
startSubmitAtom,
|
38
|
-
SyncedFormProps,
|
39
|
-
} from "./internal/state";
|
40
|
-
import { useAwaitValue } from "./internal/state/controlledFields";
|
29
|
+
useControlledFieldStore,
|
30
|
+
useFormStore,
|
31
|
+
} from "./internal/state/storeHooks";
|
41
32
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
42
33
|
import {
|
43
34
|
mergeRefs,
|
@@ -234,41 +225,27 @@ export function ValidatedForm<DataType>({
|
|
234
225
|
const Form = fetcher?.Form ?? RemixForm;
|
235
226
|
|
236
227
|
const submit = useSubmit();
|
237
|
-
const setFieldErrors =
|
238
|
-
const setFieldError =
|
239
|
-
const reset =
|
240
|
-
const
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
const
|
228
|
+
const setFieldErrors = useSetFieldErrors(formId);
|
229
|
+
const setFieldError = useFormStore(formId, (state) => state.setFieldError);
|
230
|
+
const reset = useFormStore(formId, (state) => state.reset);
|
231
|
+
const resetControlledFields = useControlledFieldStore(
|
232
|
+
formId,
|
233
|
+
(state) => state.reset
|
234
|
+
);
|
235
|
+
const startSubmit = useFormStore(formId, (state) => state.startSubmit);
|
236
|
+
const endSubmit = useFormStore(formId, (state) => state.endSubmit);
|
237
|
+
const syncFormProps = useFormStore(formId, (state) => state.syncFormProps);
|
238
|
+
const setHydrated = useFormStore(formId, (state) => state.setHydrated);
|
239
|
+
const setFormElementInState = useFormStore(
|
240
|
+
formId,
|
241
|
+
(state) => state.setFormElement
|
242
|
+
);
|
245
243
|
|
246
244
|
useEffect(() => {
|
247
|
-
setHydrated(
|
245
|
+
setHydrated();
|
248
246
|
return () => cleanupFormState(formId);
|
249
247
|
}, [formId, setHydrated]);
|
250
248
|
|
251
|
-
const awaitValue = useAwaitValue(formId);
|
252
|
-
const validateField: SyncedFormProps["validateField"] = useCallback(
|
253
|
-
async (field) => {
|
254
|
-
invariant(formRef.current, "Cannot find reference to form");
|
255
|
-
await awaitValue(field);
|
256
|
-
const { error } = await validator.validateField(
|
257
|
-
getDataFromForm(formRef.current),
|
258
|
-
field
|
259
|
-
);
|
260
|
-
|
261
|
-
if (error) {
|
262
|
-
setFieldError({ field, error });
|
263
|
-
return error;
|
264
|
-
} else {
|
265
|
-
setFieldError({ field, error: undefined });
|
266
|
-
return null;
|
267
|
-
}
|
268
|
-
},
|
269
|
-
[awaitValue, setFieldError, validator]
|
270
|
-
);
|
271
|
-
|
272
249
|
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
273
250
|
const registerReceiveFocus: SyncedFormProps["registerReceiveFocus"] =
|
274
251
|
useCallback(
|
@@ -286,8 +263,8 @@ export function ValidatedForm<DataType>({
|
|
286
263
|
action,
|
287
264
|
defaultValues: providedDefaultValues ?? backendDefaultValues ?? {},
|
288
265
|
subaction,
|
289
|
-
validateField,
|
290
266
|
registerReceiveFocus,
|
267
|
+
validator,
|
291
268
|
});
|
292
269
|
}, [
|
293
270
|
action,
|
@@ -295,8 +272,8 @@ export function ValidatedForm<DataType>({
|
|
295
272
|
registerReceiveFocus,
|
296
273
|
subaction,
|
297
274
|
syncFormProps,
|
298
|
-
validateField,
|
299
275
|
backendDefaultValues,
|
276
|
+
validator,
|
300
277
|
]);
|
301
278
|
|
302
279
|
useEffect(() => {
|
@@ -386,6 +363,7 @@ export function ValidatedForm<DataType>({
|
|
386
363
|
onReset?.(event);
|
387
364
|
if (event.defaultPrevented) return;
|
388
365
|
reset();
|
366
|
+
resetControlledFields();
|
389
367
|
}}
|
390
368
|
>
|
391
369
|
<InternalFormContext.Provider value={contextValue}>
|
package/src/hooks.ts
CHANGED
@@ -8,15 +8,14 @@ import {
|
|
8
8
|
useInternalFormContext,
|
9
9
|
useFieldTouched,
|
10
10
|
useFieldError,
|
11
|
-
useFormAtomValue,
|
12
11
|
useFieldDefaultValue,
|
12
|
+
useClearError,
|
13
|
+
useInternalIsSubmitting,
|
14
|
+
useInternalIsValid,
|
15
|
+
useInternalHasBeenSubmitted,
|
16
|
+
useValidateField,
|
17
|
+
useRegisterReceiveFocus,
|
13
18
|
} from "./internal/hooks";
|
14
|
-
import {
|
15
|
-
formPropsAtom,
|
16
|
-
hasBeenSubmittedAtom,
|
17
|
-
isSubmittingAtom,
|
18
|
-
isValidAtom,
|
19
|
-
} from "./internal/state";
|
20
19
|
import {
|
21
20
|
useControllableValue,
|
22
21
|
useUpdateControllableValue,
|
@@ -31,7 +30,7 @@ import {
|
|
31
30
|
*/
|
32
31
|
export const useIsSubmitting = (formId?: string) => {
|
33
32
|
const formContext = useInternalFormContext(formId, "useIsSubmitting");
|
34
|
-
return
|
33
|
+
return useInternalIsSubmitting(formContext.formId);
|
35
34
|
};
|
36
35
|
|
37
36
|
/**
|
@@ -41,7 +40,7 @@ export const useIsSubmitting = (formId?: string) => {
|
|
41
40
|
*/
|
42
41
|
export const useIsValid = (formId?: string) => {
|
43
42
|
const formContext = useInternalFormContext(formId, "useIsValid");
|
44
|
-
return
|
43
|
+
return useInternalIsValid(formContext.formId);
|
45
44
|
};
|
46
45
|
|
47
46
|
export type FieldProps = {
|
@@ -103,14 +102,12 @@ export const useField = (
|
|
103
102
|
|
104
103
|
const defaultValue = useFieldDefaultValue(name, formContext);
|
105
104
|
const [touched, setTouched] = useFieldTouched(name, formContext);
|
106
|
-
const
|
105
|
+
const error = useFieldError(name, formContext);
|
106
|
+
const clearError = useClearError(formContext);
|
107
107
|
|
108
|
-
const hasBeenSubmitted =
|
109
|
-
|
110
|
-
);
|
111
|
-
const { validateField, registerReceiveFocus } = useFormAtomValue(
|
112
|
-
formPropsAtom(formContext.formId)
|
113
|
-
);
|
108
|
+
const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId);
|
109
|
+
const validateField = useValidateField(formContext.formId);
|
110
|
+
const registerReceiveFocus = useRegisterReceiveFocus(formContext.formId);
|
114
111
|
|
115
112
|
useEffect(() => {
|
116
113
|
if (handleReceiveFocus)
|
@@ -120,7 +117,7 @@ export const useField = (
|
|
120
117
|
const field = useMemo<FieldProps>(() => {
|
121
118
|
const helpers = {
|
122
119
|
error,
|
123
|
-
clearError: () =>
|
120
|
+
clearError: () => clearError(name),
|
124
121
|
validate: () => {
|
125
122
|
validateField(name);
|
126
123
|
},
|
@@ -140,13 +137,13 @@ export const useField = (
|
|
140
137
|
};
|
141
138
|
}, [
|
142
139
|
error,
|
140
|
+
clearError,
|
143
141
|
defaultValue,
|
144
142
|
touched,
|
145
143
|
setTouched,
|
146
144
|
name,
|
147
145
|
hasBeenSubmitted,
|
148
146
|
options?.validationBehavior,
|
149
|
-
setError,
|
150
147
|
validateField,
|
151
148
|
]);
|
152
149
|
|
package/src/internal/hooks.ts
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
import { useActionData, useMatches, useTransition } from "@remix-run/react";
|
2
|
-
import { Atom, useAtom, WritableAtom } from "jotai";
|
3
|
-
import { useAtomValue, useUpdateAtom } from "jotai/utils";
|
4
2
|
import lodashGet from "lodash/get";
|
5
3
|
import { useCallback, useContext } from "react";
|
6
4
|
import invariant from "tiny-invariant";
|
@@ -8,25 +6,8 @@ import { FieldErrors, ValidationErrorResponseData } from "..";
|
|
8
6
|
import { formDefaultValuesKey } from "./constants";
|
9
7
|
import { InternalFormContext, InternalFormContextValue } from "./formContext";
|
10
8
|
import { Hydratable, hydratable } from "./hydratable";
|
11
|
-
import {
|
12
|
-
|
13
|
-
fieldErrorAtom,
|
14
|
-
fieldTouchedAtom,
|
15
|
-
formPropsAtom,
|
16
|
-
isHydratedAtom,
|
17
|
-
setFieldErrorAtom,
|
18
|
-
setTouchedAtom,
|
19
|
-
} from "./state";
|
20
|
-
|
21
|
-
export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
|
22
|
-
useUpdateAtom(atom, ATOM_SCOPE);
|
23
|
-
|
24
|
-
export const useFormAtom = <Value, Update, Result extends void | Promise<void>>(
|
25
|
-
anAtom: WritableAtom<Value, Update, Result>
|
26
|
-
) => useAtom(anAtom, ATOM_SCOPE);
|
27
|
-
|
28
|
-
export const useFormAtomValue = <Value>(anAtom: Atom<Value>) =>
|
29
|
-
useAtomValue(anAtom, ATOM_SCOPE);
|
9
|
+
import { InternalFormId } from "./state/storeFamily";
|
10
|
+
import { useFormStore } from "./state/storeHooks";
|
30
11
|
|
31
12
|
export const useInternalFormContext = (
|
32
13
|
formId?: string | symbol,
|
@@ -72,7 +53,7 @@ export const useFieldErrorsForForm = (
|
|
72
53
|
context: InternalFormContextValue
|
73
54
|
): Hydratable<FieldErrors | undefined> => {
|
74
55
|
const response = useErrorResponseForForm(context);
|
75
|
-
const hydrated =
|
56
|
+
const hydrated = useFormStore(context.formId, (state) => state.isHydrated);
|
76
57
|
return hydratable.from(response?.fieldErrors, hydrated);
|
77
58
|
};
|
78
59
|
|
@@ -97,7 +78,7 @@ export const useDefaultValuesForForm = (
|
|
97
78
|
context: InternalFormContextValue
|
98
79
|
): Hydratable<{ [fieldName: string]: any }> => {
|
99
80
|
const { formId, defaultValuesProp } = context;
|
100
|
-
const hydrated =
|
81
|
+
const hydrated = useFormStore(formId, (state) => state.isHydrated);
|
101
82
|
const errorResponse = useErrorResponseForForm(context);
|
102
83
|
const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
|
103
84
|
|
@@ -133,20 +114,31 @@ export const useHasActiveFormSubmit = ({
|
|
133
114
|
export const useFieldTouched = (
|
134
115
|
field: string,
|
135
116
|
{ formId }: InternalFormContextValue
|
136
|
-
) =>
|
117
|
+
) => {
|
118
|
+
const touched = useFormStore(formId, (state) => state.touchedFields[field]);
|
119
|
+
const setFieldTouched = useFormStore(formId, (state) => state.setTouched);
|
120
|
+
const setTouched = useCallback(
|
121
|
+
(touched: boolean) => setFieldTouched(field, touched),
|
122
|
+
[field, setFieldTouched]
|
123
|
+
);
|
124
|
+
return [touched, setTouched] as const;
|
125
|
+
};
|
137
126
|
|
138
127
|
export const useFieldError = (
|
139
128
|
name: string,
|
140
129
|
context: InternalFormContextValue
|
141
130
|
) => {
|
142
131
|
const fieldErrors = useFieldErrorsForForm(context);
|
143
|
-
const
|
144
|
-
|
132
|
+
const state = useFormStore(
|
133
|
+
context.formId,
|
134
|
+
(state) => state.fieldErrors[name]
|
145
135
|
);
|
146
|
-
return [
|
147
|
-
|
148
|
-
|
149
|
-
|
136
|
+
return fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state);
|
137
|
+
};
|
138
|
+
|
139
|
+
export const useClearError = (context: InternalFormContextValue) => {
|
140
|
+
const { formId } = context;
|
141
|
+
return useFormStore(formId, (state) => state.clearFieldError);
|
150
142
|
};
|
151
143
|
|
152
144
|
export const useFieldDefaultValue = (
|
@@ -154,26 +146,58 @@ export const useFieldDefaultValue = (
|
|
154
146
|
context: InternalFormContextValue
|
155
147
|
) => {
|
156
148
|
const defaultValues = useDefaultValuesForForm(context);
|
157
|
-
const
|
158
|
-
formPropsAtom(context.formId)
|
159
|
-
);
|
149
|
+
const state = useSyncedDefaultValues(context.formId);
|
160
150
|
return defaultValues
|
161
151
|
.map((val) => lodashGet(val, name))
|
162
152
|
.hydrateTo(lodashGet(state, name));
|
163
153
|
};
|
164
154
|
|
165
|
-
export const
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
155
|
+
export const useInternalIsSubmitting = (formId: InternalFormId) =>
|
156
|
+
useFormStore(formId, (state) => state.isSubmitting);
|
157
|
+
|
158
|
+
export const useInternalIsValid = (formId: InternalFormId) =>
|
159
|
+
useFormStore(formId, (state) => state.isValid());
|
160
|
+
|
161
|
+
export const useInternalHasBeenSubmitted = (formId: InternalFormId) =>
|
162
|
+
useFormStore(formId, (state) => state.hasBeenSubmitted);
|
163
|
+
|
164
|
+
export const useValidateField = (formId: InternalFormId) =>
|
165
|
+
useFormStore(formId, (state) => state.validateField);
|
166
|
+
|
167
|
+
export const useValidate = (formId: InternalFormId) =>
|
168
|
+
useFormStore(formId, (state) => state.validate);
|
169
|
+
|
170
|
+
const noOpReceiver = () => () => {};
|
171
|
+
export const useRegisterReceiveFocus = (formId: InternalFormId) =>
|
172
|
+
useFormStore(
|
173
|
+
formId,
|
174
|
+
(state) => state.formProps?.registerReceiveFocus ?? noOpReceiver
|
170
175
|
);
|
171
|
-
};
|
172
176
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
177
|
+
const defaultDefaultValues = {};
|
178
|
+
export const useSyncedDefaultValues = (formId: InternalFormId) =>
|
179
|
+
useFormStore(
|
180
|
+
formId,
|
181
|
+
(state) => state.formProps?.defaultValues ?? defaultDefaultValues
|
178
182
|
);
|
179
|
-
|
183
|
+
|
184
|
+
export const useSetTouched = ({ formId }: InternalFormContextValue) =>
|
185
|
+
useFormStore(formId, (state) => state.setTouched);
|
186
|
+
|
187
|
+
export const useTouchedFields = (formId: InternalFormId) =>
|
188
|
+
useFormStore(formId, (state) => state.touchedFields);
|
189
|
+
|
190
|
+
export const useFieldErrors = (formId: InternalFormId) =>
|
191
|
+
useFormStore(formId, (state) => state.fieldErrors);
|
192
|
+
|
193
|
+
export const useSetFieldErrors = (formId: InternalFormId) =>
|
194
|
+
useFormStore(formId, (state) => state.setFieldErrors);
|
195
|
+
|
196
|
+
export const useResetFormElement = (formId: InternalFormId) =>
|
197
|
+
useFormStore(formId, (state) => state.resetFormElement);
|
198
|
+
|
199
|
+
export const useFormActionProp = (formId: InternalFormId) =>
|
200
|
+
useFormStore(formId, (state) => state.formProps?.action);
|
201
|
+
|
202
|
+
export const useFormSubactionProp = (formId: InternalFormId) =>
|
203
|
+
useFormStore(formId, (state) => state.formProps?.subaction);
|
@@ -8,7 +8,7 @@ export const getRadioChecked = (
|
|
8
8
|
|
9
9
|
if (import.meta.vitest) {
|
10
10
|
const { it, expect } = import.meta.vitest;
|
11
|
-
it("
|
11
|
+
it("getRadioChecked", () => {
|
12
12
|
expect(getRadioChecked("on", "on")).toBe(true);
|
13
13
|
expect(getRadioChecked("on", undefined)).toBe(undefined);
|
14
14
|
expect(getRadioChecked("trueValue", undefined)).toBe(undefined);
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { controlledFieldStore } from "./controlledFieldStore";
|
2
|
+
import { formStore } from "./createFormStore";
|
3
|
+
import { InternalFormId } from "./storeFamily";
|
4
|
+
|
5
|
+
export const cleanupFormState = (formId: InternalFormId) => {
|
6
|
+
formStore.remove(formId);
|
7
|
+
controlledFieldStore.remove(formId);
|
8
|
+
};
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import invariant from "tiny-invariant";
|
2
|
+
import create from "zustand";
|
3
|
+
import { immer } from "zustand/middleware/immer";
|
4
|
+
import { storeFamily } from "./storeFamily";
|
5
|
+
|
6
|
+
export type ControlledFieldState = {
|
7
|
+
fields: {
|
8
|
+
[fieldName: string]:
|
9
|
+
| {
|
10
|
+
refCount: number;
|
11
|
+
value: unknown;
|
12
|
+
defaultValue?: unknown;
|
13
|
+
hydrated: boolean;
|
14
|
+
valueUpdatePromise: Promise<void> | undefined;
|
15
|
+
resolveValueUpdate: (() => void) | undefined;
|
16
|
+
}
|
17
|
+
| undefined;
|
18
|
+
};
|
19
|
+
register: (fieldName: string) => void;
|
20
|
+
unregister: (fieldName: string) => void;
|
21
|
+
setValue: (fieldName: string, value: unknown) => void;
|
22
|
+
hydrateWithDefault: (fieldName: string, defaultValue: unknown) => void;
|
23
|
+
awaitValueUpdate: (fieldName: string) => Promise<void>;
|
24
|
+
reset: () => void;
|
25
|
+
};
|
26
|
+
|
27
|
+
export const controlledFieldStore = storeFamily(() =>
|
28
|
+
create<ControlledFieldState>()(
|
29
|
+
immer((set, get, api) => ({
|
30
|
+
fields: {},
|
31
|
+
|
32
|
+
register: (field) =>
|
33
|
+
set((state) => {
|
34
|
+
if (state.fields[field]) {
|
35
|
+
state.fields[field]!.refCount++;
|
36
|
+
} else {
|
37
|
+
state.fields[field] = {
|
38
|
+
refCount: 1,
|
39
|
+
value: undefined,
|
40
|
+
hydrated: false,
|
41
|
+
valueUpdatePromise: undefined,
|
42
|
+
resolveValueUpdate: undefined,
|
43
|
+
};
|
44
|
+
}
|
45
|
+
}),
|
46
|
+
|
47
|
+
unregister: (field) =>
|
48
|
+
set((state) => {
|
49
|
+
const fieldState = state.fields[field];
|
50
|
+
if (!fieldState) return;
|
51
|
+
|
52
|
+
fieldState.refCount--;
|
53
|
+
if (fieldState.refCount === 0) delete state.fields[field];
|
54
|
+
}),
|
55
|
+
|
56
|
+
setValue: (field, value) =>
|
57
|
+
set((state) => {
|
58
|
+
const fieldState = state.fields[field];
|
59
|
+
if (!fieldState) return;
|
60
|
+
|
61
|
+
fieldState.value = value;
|
62
|
+
const promise = new Promise<void>((resolve) => {
|
63
|
+
fieldState.resolveValueUpdate = resolve;
|
64
|
+
});
|
65
|
+
fieldState.valueUpdatePromise = promise;
|
66
|
+
}),
|
67
|
+
|
68
|
+
hydrateWithDefault: (field, defaultValue) =>
|
69
|
+
set((state) => {
|
70
|
+
const fieldState = state.fields[field];
|
71
|
+
if (!fieldState) return;
|
72
|
+
|
73
|
+
fieldState.value = defaultValue;
|
74
|
+
fieldState.defaultValue = defaultValue;
|
75
|
+
fieldState.hydrated = true;
|
76
|
+
}),
|
77
|
+
|
78
|
+
awaitValueUpdate: async (field) => {
|
79
|
+
await get().fields[field]?.valueUpdatePromise;
|
80
|
+
},
|
81
|
+
|
82
|
+
reset: () =>
|
83
|
+
set((state) => {
|
84
|
+
Object.values(state.fields).forEach((field) => {
|
85
|
+
if (!field) return;
|
86
|
+
field.value = field.defaultValue;
|
87
|
+
});
|
88
|
+
}),
|
89
|
+
}))
|
90
|
+
)
|
91
|
+
);
|
@@ -1,115 +1,36 @@
|
|
1
|
-
import { atom, PrimitiveAtom } from "jotai";
|
2
|
-
import { useAtomCallback } from "jotai/utils";
|
3
|
-
import omit from "lodash/omit";
|
4
1
|
import { useCallback, useEffect } from "react";
|
5
2
|
import { InternalFormContextValue } from "../formContext";
|
6
|
-
import {
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
useFormUpdateAtom,
|
11
|
-
} from "../hooks";
|
12
|
-
import { isHydratedAtom } from "../state";
|
13
|
-
import {
|
14
|
-
fieldAtomFamily,
|
15
|
-
FieldAtomKey,
|
16
|
-
formAtomFamily,
|
17
|
-
InternalFormId,
|
18
|
-
} from "./atomUtils";
|
19
|
-
|
20
|
-
export const controlledFieldsAtom = formAtomFamily<
|
21
|
-
Record<string, PrimitiveAtom<unknown>>
|
22
|
-
>({});
|
23
|
-
const refCountAtom = fieldAtomFamily(() => atom(0));
|
24
|
-
const fieldValueAtom = fieldAtomFamily(() => atom<unknown>(undefined));
|
25
|
-
const fieldValueHydratedAtom = fieldAtomFamily(() => atom(false));
|
26
|
-
|
27
|
-
export const valueUpdatePromiseAtom = fieldAtomFamily(() =>
|
28
|
-
atom<Promise<void> | undefined>(undefined)
|
29
|
-
);
|
30
|
-
export const resolveValueUpdateAtom = fieldAtomFamily(() =>
|
31
|
-
atom<(() => void) | undefined>(undefined)
|
32
|
-
);
|
33
|
-
|
34
|
-
const registerAtom = atom(null, (get, set, { formId, field }: FieldAtomKey) => {
|
35
|
-
set(refCountAtom({ formId, field }), (prev) => prev + 1);
|
36
|
-
const newRefCount = get(refCountAtom({ formId, field }));
|
37
|
-
// We don't set hydrated here because it gets set when we know
|
38
|
-
// we have the right default values
|
39
|
-
if (newRefCount === 1) {
|
40
|
-
set(controlledFieldsAtom(formId), (prev) => ({
|
41
|
-
...prev,
|
42
|
-
[field]: fieldValueAtom({ formId, field }),
|
43
|
-
}));
|
44
|
-
}
|
45
|
-
});
|
46
|
-
|
47
|
-
const unregisterAtom = atom(
|
48
|
-
null,
|
49
|
-
(get, set, { formId, field }: FieldAtomKey) => {
|
50
|
-
set(refCountAtom({ formId, field }), (prev) => prev - 1);
|
51
|
-
const newRefCount = get(refCountAtom({ formId, field }));
|
52
|
-
if (newRefCount === 0) {
|
53
|
-
set(controlledFieldsAtom(formId), (prev) => omit(prev, field));
|
54
|
-
fieldValueAtom.remove({ formId, field });
|
55
|
-
resolveValueUpdateAtom.remove({ formId, field });
|
56
|
-
fieldValueHydratedAtom.remove({ formId, field });
|
57
|
-
}
|
58
|
-
}
|
59
|
-
);
|
60
|
-
|
61
|
-
export const setControlledFieldValueAtom = atom(
|
62
|
-
null,
|
63
|
-
(
|
64
|
-
_get,
|
65
|
-
set,
|
66
|
-
{
|
67
|
-
formId,
|
68
|
-
field,
|
69
|
-
value,
|
70
|
-
}: { formId: InternalFormId; field: string; value: unknown }
|
71
|
-
) => {
|
72
|
-
set(fieldValueAtom({ formId, field }), value);
|
73
|
-
const resolveAtom = resolveValueUpdateAtom({ formId, field });
|
74
|
-
const promiseAtom = valueUpdatePromiseAtom({ formId, field });
|
75
|
-
|
76
|
-
const promise = new Promise<void>((resolve) =>
|
77
|
-
set(resolveAtom, () => {
|
78
|
-
resolve();
|
79
|
-
set(resolveAtom, undefined);
|
80
|
-
set(promiseAtom, undefined);
|
81
|
-
})
|
82
|
-
);
|
83
|
-
set(promiseAtom, promise);
|
84
|
-
}
|
85
|
-
);
|
3
|
+
import { useFieldDefaultValue } from "../hooks";
|
4
|
+
import { controlledFieldStore } from "./controlledFieldStore";
|
5
|
+
import { formStore } from "./createFormStore";
|
6
|
+
import { InternalFormId } from "./storeFamily";
|
86
7
|
|
87
8
|
export const useControlledFieldValue = (
|
88
9
|
context: InternalFormContextValue,
|
89
10
|
field: string
|
90
11
|
) => {
|
91
|
-
const
|
92
|
-
const
|
12
|
+
const useValueStore = controlledFieldStore(context.formId);
|
13
|
+
const value = useValueStore((state) => state.fields[field]?.value);
|
93
14
|
|
15
|
+
const useFormStore = formStore(context.formId);
|
16
|
+
const isFormHydrated = useFormStore((state) => state.isHydrated);
|
94
17
|
const defaultValue = useFieldDefaultValue(field, context);
|
95
|
-
|
96
|
-
const
|
97
|
-
|
18
|
+
|
19
|
+
const isFieldHydrated = useValueStore(
|
20
|
+
(state) => state.fields[field]?.hydrated ?? false
|
98
21
|
);
|
22
|
+
const hydrateWithDefault = useValueStore((state) => state.hydrateWithDefault);
|
99
23
|
|
100
24
|
useEffect(() => {
|
101
|
-
if (
|
102
|
-
|
103
|
-
setIsFieldHydrated(true);
|
25
|
+
if (isFormHydrated && !isFieldHydrated) {
|
26
|
+
hydrateWithDefault(field, defaultValue);
|
104
27
|
}
|
105
28
|
}, [
|
106
29
|
defaultValue,
|
107
30
|
field,
|
108
|
-
|
31
|
+
hydrateWithDefault,
|
109
32
|
isFieldHydrated,
|
110
|
-
|
111
|
-
setIsFieldHydrated,
|
112
|
-
setValue,
|
33
|
+
isFormHydrated,
|
113
34
|
]);
|
114
35
|
|
115
36
|
return isFieldHydrated ? value : defaultValue;
|
@@ -119,27 +40,26 @@ export const useControllableValue = (
|
|
119
40
|
context: InternalFormContextValue,
|
120
41
|
field: string
|
121
42
|
) => {
|
122
|
-
const
|
123
|
-
|
43
|
+
const useValueStore = controlledFieldStore(context.formId);
|
44
|
+
|
45
|
+
const resolveUpdate = useValueStore(
|
46
|
+
(state) => state.fields[field]?.resolveValueUpdate
|
124
47
|
);
|
125
48
|
useEffect(() => {
|
126
49
|
resolveUpdate?.();
|
127
50
|
}, [resolveUpdate]);
|
128
51
|
|
129
|
-
const register =
|
130
|
-
const unregister =
|
52
|
+
const register = useValueStore((state) => state.register);
|
53
|
+
const unregister = useValueStore((state) => state.unregister);
|
131
54
|
useEffect(() => {
|
132
|
-
register(
|
133
|
-
return () => unregister(
|
55
|
+
register(field);
|
56
|
+
return () => unregister(field);
|
134
57
|
}, [context.formId, field, register, unregister]);
|
135
58
|
|
136
|
-
const setControlledFieldValue =
|
137
|
-
setControlledFieldValueAtom
|
138
|
-
);
|
59
|
+
const setControlledFieldValue = useValueStore((state) => state.setValue);
|
139
60
|
const setValue = useCallback(
|
140
|
-
(value: unknown) =>
|
141
|
-
|
142
|
-
[field, context.formId, setControlledFieldValue]
|
61
|
+
(value: unknown) => setControlledFieldValue(field, value),
|
62
|
+
[field, setControlledFieldValue]
|
143
63
|
);
|
144
64
|
|
145
65
|
const value = useControlledFieldValue(context, field);
|
@@ -148,23 +68,11 @@ export const useControllableValue = (
|
|
148
68
|
};
|
149
69
|
|
150
70
|
export const useUpdateControllableValue = (formId: InternalFormId) => {
|
151
|
-
const
|
152
|
-
|
153
|
-
);
|
154
|
-
return useCallback(
|
155
|
-
(field: string, value: unknown) =>
|
156
|
-
setControlledFieldValue({ formId, field, value }),
|
157
|
-
[formId, setControlledFieldValue]
|
158
|
-
);
|
71
|
+
const useValueStore = controlledFieldStore(formId);
|
72
|
+
return useValueStore((state) => state.setValue);
|
159
73
|
};
|
160
74
|
|
161
75
|
export const useAwaitValue = (formId: InternalFormId) => {
|
162
|
-
|
163
|
-
|
164
|
-
async (get, _set, field: string) => {
|
165
|
-
await get(valueUpdatePromiseAtom({ formId, field }));
|
166
|
-
},
|
167
|
-
[formId]
|
168
|
-
)
|
169
|
-
);
|
76
|
+
const useValueStore = controlledFieldStore(formId);
|
77
|
+
return useValueStore((state) => state.awaitValueUpdate);
|
170
78
|
};
|