remix-validated-form 4.3.1-beta.0 → 4.4.2
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 +5 -5
- package/browser/ValidatedForm.js +20 -35
- 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 +15 -0
- package/browser/userFacingFormContext.js +6 -4
- package/dist/remix-validated-form.cjs.js +18 -1
- package/dist/remix-validated-form.cjs.js.map +1 -0
- package/dist/remix-validated-form.es.js +1039 -1729
- package/dist/remix-validated-form.es.js.map +1 -0
- package/dist/remix-validated-form.umd.js +18 -1
- package/dist/remix-validated-form.umd.js.map +1 -0
- 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 +15 -0
- package/package.json +4 -3
- package/src/ValidatedForm.tsx +38 -53
- 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 +26 -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/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
|
};
|
@@ -0,0 +1,152 @@
|
|
1
|
+
import invariant from "tiny-invariant";
|
2
|
+
import create from "zustand";
|
3
|
+
import { immer } from "zustand/middleware/immer";
|
4
|
+
import { FieldErrors, TouchedFields, Validator } from "../../validation/types";
|
5
|
+
import { controlledFieldStore } from "./controlledFieldStore";
|
6
|
+
import { storeFamily } from "./storeFamily";
|
7
|
+
|
8
|
+
export type SyncedFormProps = {
|
9
|
+
formId?: string;
|
10
|
+
action?: string;
|
11
|
+
subaction?: string;
|
12
|
+
defaultValues: { [fieldName: string]: any };
|
13
|
+
registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
|
14
|
+
validator: Validator<unknown>;
|
15
|
+
};
|
16
|
+
|
17
|
+
export type FormState = {
|
18
|
+
isHydrated: boolean;
|
19
|
+
isSubmitting: boolean;
|
20
|
+
hasBeenSubmitted: boolean;
|
21
|
+
fieldErrors: FieldErrors;
|
22
|
+
touchedFields: TouchedFields;
|
23
|
+
formProps?: SyncedFormProps;
|
24
|
+
formElement: HTMLFormElement | null;
|
25
|
+
|
26
|
+
isValid: () => boolean;
|
27
|
+
startSubmit: () => void;
|
28
|
+
endSubmit: () => void;
|
29
|
+
setTouched: (field: string, touched: boolean) => void;
|
30
|
+
setFieldError: (field: string, error: string) => void;
|
31
|
+
setFieldErrors: (errors: FieldErrors) => void;
|
32
|
+
clearFieldError: (field: string) => void;
|
33
|
+
reset: () => void;
|
34
|
+
syncFormProps: (props: SyncedFormProps) => void;
|
35
|
+
setHydrated: () => void;
|
36
|
+
setFormElement: (formElement: HTMLFormElement | null) => void;
|
37
|
+
validateField: (fieldName: string) => Promise<string | null>;
|
38
|
+
validate: () => Promise<void>;
|
39
|
+
resetFormElement: () => void;
|
40
|
+
};
|
41
|
+
|
42
|
+
export const formStore = storeFamily((formId) =>
|
43
|
+
create<FormState>()(
|
44
|
+
immer((set, get, api) => ({
|
45
|
+
isHydrated: false,
|
46
|
+
isSubmitting: false,
|
47
|
+
hasBeenSubmitted: false,
|
48
|
+
touchedFields: {},
|
49
|
+
fieldErrors: {},
|
50
|
+
formElement: null,
|
51
|
+
|
52
|
+
isValid: () => Object.keys(get().fieldErrors).length === 0,
|
53
|
+
startSubmit: () =>
|
54
|
+
set((state) => {
|
55
|
+
state.isSubmitting = true;
|
56
|
+
state.hasBeenSubmitted = true;
|
57
|
+
}),
|
58
|
+
endSubmit: () =>
|
59
|
+
set((state) => {
|
60
|
+
state.isSubmitting = false;
|
61
|
+
}),
|
62
|
+
setTouched: (fieldName, touched) =>
|
63
|
+
set((state) => {
|
64
|
+
state.touchedFields[fieldName] = touched;
|
65
|
+
}),
|
66
|
+
setFieldError: (fieldName: string, error: string) =>
|
67
|
+
set((state) => {
|
68
|
+
state.fieldErrors[fieldName] = error;
|
69
|
+
}),
|
70
|
+
setFieldErrors: (errors: FieldErrors) =>
|
71
|
+
set((state) => {
|
72
|
+
state.fieldErrors = errors;
|
73
|
+
}),
|
74
|
+
clearFieldError: (fieldName: string) =>
|
75
|
+
set((state) => {
|
76
|
+
delete state.fieldErrors[fieldName];
|
77
|
+
}),
|
78
|
+
|
79
|
+
reset: () =>
|
80
|
+
set((state) => {
|
81
|
+
state.fieldErrors = {};
|
82
|
+
state.touchedFields = {};
|
83
|
+
state.hasBeenSubmitted = false;
|
84
|
+
}),
|
85
|
+
syncFormProps: (props: SyncedFormProps) =>
|
86
|
+
set((state) => {
|
87
|
+
state.formProps = props;
|
88
|
+
}),
|
89
|
+
setHydrated: () =>
|
90
|
+
set((state) => {
|
91
|
+
state.isHydrated = true;
|
92
|
+
}),
|
93
|
+
setFormElement: (formElement: HTMLFormElement | null) => {
|
94
|
+
// This gets called frequently, so we want to avoid calling set() every time
|
95
|
+
// Or else we wind up with an infinite loop
|
96
|
+
if (get().formElement === formElement) return;
|
97
|
+
set((state) => {
|
98
|
+
// weird type issue here
|
99
|
+
// seems to be because formElement is a writable draft
|
100
|
+
state.formElement = formElement as any;
|
101
|
+
});
|
102
|
+
},
|
103
|
+
validateField: async (field: string) => {
|
104
|
+
const formElement = get().formElement;
|
105
|
+
invariant(
|
106
|
+
formElement,
|
107
|
+
"Cannot find reference to form. This is probably a bug in remix-validated-form."
|
108
|
+
);
|
109
|
+
|
110
|
+
const validator = get().formProps?.validator;
|
111
|
+
invariant(
|
112
|
+
validator,
|
113
|
+
"Cannot validator. This is probably a bug in remix-validated-form."
|
114
|
+
);
|
115
|
+
|
116
|
+
await controlledFieldStore(formId).getState().awaitValueUpdate?.(field);
|
117
|
+
|
118
|
+
const { error } = await validator.validateField(
|
119
|
+
new FormData(formElement),
|
120
|
+
field
|
121
|
+
);
|
122
|
+
|
123
|
+
if (error) {
|
124
|
+
get().setFieldError(field, error);
|
125
|
+
return error;
|
126
|
+
} else {
|
127
|
+
get().clearFieldError(field);
|
128
|
+
return null;
|
129
|
+
}
|
130
|
+
},
|
131
|
+
|
132
|
+
validate: async () => {
|
133
|
+
const formElement = get().formElement;
|
134
|
+
invariant(
|
135
|
+
formElement,
|
136
|
+
"Cannot find reference to form. This is probably a bug in remix-validated-form."
|
137
|
+
);
|
138
|
+
|
139
|
+
const validator = get().formProps?.validator;
|
140
|
+
invariant(
|
141
|
+
validator,
|
142
|
+
"Cannot validator. This is probably a bug in remix-validated-form."
|
143
|
+
);
|
144
|
+
|
145
|
+
const { error } = await validator.validate(new FormData(formElement));
|
146
|
+
if (error) get().setFieldErrors(error.fieldErrors);
|
147
|
+
},
|
148
|
+
|
149
|
+
resetFormElement: () => get().formElement?.reset(),
|
150
|
+
}))
|
151
|
+
)
|
152
|
+
);
|
@@ -0,0 +1,24 @@
|
|
1
|
+
/**
|
2
|
+
* This is basically what `atomFamily` from jotai does,
|
3
|
+
* but it doesn't make sense to include the entire jotai library just for that api.
|
4
|
+
*/
|
5
|
+
|
6
|
+
export type InternalFormId = string | symbol;
|
7
|
+
|
8
|
+
export const storeFamily = <T>(create: (formId: InternalFormId) => T) => {
|
9
|
+
const stores: Map<InternalFormId, T> = new Map();
|
10
|
+
|
11
|
+
const family = (formId: InternalFormId) => {
|
12
|
+
if (stores.has(formId)) return stores.get(formId)!;
|
13
|
+
|
14
|
+
const store = create(formId);
|
15
|
+
stores.set(formId, store);
|
16
|
+
return store;
|
17
|
+
};
|
18
|
+
|
19
|
+
family.remove = (formId: InternalFormId) => {
|
20
|
+
stores.delete(formId);
|
21
|
+
};
|
22
|
+
|
23
|
+
return family;
|
24
|
+
};
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import {
|
2
|
+
ControlledFieldState,
|
3
|
+
controlledFieldStore,
|
4
|
+
} from "./controlledFieldStore";
|
5
|
+
import { FormState, formStore } from "./createFormStore";
|
6
|
+
import { InternalFormId } from "./storeFamily";
|
7
|
+
|
8
|
+
export const useFormStore = <T>(
|
9
|
+
formId: InternalFormId,
|
10
|
+
selector: (state: FormState) => T
|
11
|
+
) => {
|
12
|
+
const useStore = formStore(formId);
|
13
|
+
return useStore(selector);
|
14
|
+
};
|
15
|
+
|
16
|
+
export const useControlledFieldStore = <T>(
|
17
|
+
formId: InternalFormId,
|
18
|
+
selector: (state: ControlledFieldState) => T
|
19
|
+
) => {
|
20
|
+
const useStore = controlledFieldStore(formId);
|
21
|
+
return useStore(selector);
|
22
|
+
};
|