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.
Files changed (58) hide show
  1. package/.turbo/turbo-build.log +5 -5
  2. package/browser/ValidatedForm.js +20 -35
  3. package/browser/hooks.d.ts +1 -1
  4. package/browser/hooks.js +10 -9
  5. package/browser/internal/hooks.d.ts +20 -9
  6. package/browser/internal/hooks.js +32 -23
  7. package/browser/internal/logic/getRadioChecked.js +1 -1
  8. package/browser/internal/state/cleanup.d.ts +2 -0
  9. package/browser/internal/state/cleanup.js +6 -0
  10. package/browser/internal/state/controlledFieldStore.d.ts +24 -0
  11. package/browser/internal/state/controlledFieldStore.js +57 -0
  12. package/browser/internal/state/controlledFields.d.ts +3 -116
  13. package/browser/internal/state/controlledFields.js +25 -68
  14. package/browser/internal/state/createFormStore.d.ts +40 -0
  15. package/browser/internal/state/createFormStore.js +83 -0
  16. package/browser/internal/state/storeFamily.d.ts +9 -0
  17. package/browser/internal/state/storeFamily.js +18 -0
  18. package/browser/internal/state/storeHooks.d.ts +5 -0
  19. package/browser/internal/state/storeHooks.js +10 -0
  20. package/browser/unreleased/formStateHooks.d.ts +15 -0
  21. package/browser/unreleased/formStateHooks.js +23 -14
  22. package/browser/userFacingFormContext.d.ts +15 -0
  23. package/browser/userFacingFormContext.js +6 -4
  24. package/dist/remix-validated-form.cjs.js +18 -1
  25. package/dist/remix-validated-form.cjs.js.map +1 -0
  26. package/dist/remix-validated-form.es.js +1039 -1729
  27. package/dist/remix-validated-form.es.js.map +1 -0
  28. package/dist/remix-validated-form.umd.js +18 -1
  29. package/dist/remix-validated-form.umd.js.map +1 -0
  30. package/dist/types/hooks.d.ts +1 -1
  31. package/dist/types/internal/hooks.d.ts +20 -9
  32. package/dist/types/internal/state/cleanup.d.ts +2 -0
  33. package/dist/types/internal/state/controlledFieldStore.d.ts +24 -0
  34. package/dist/types/internal/state/controlledFields.d.ts +3 -116
  35. package/dist/types/internal/state/createFormStore.d.ts +40 -0
  36. package/dist/types/internal/state/storeFamily.d.ts +9 -0
  37. package/dist/types/internal/state/storeHooks.d.ts +5 -0
  38. package/dist/types/unreleased/formStateHooks.d.ts +15 -0
  39. package/dist/types/userFacingFormContext.d.ts +15 -0
  40. package/package.json +4 -3
  41. package/src/ValidatedForm.tsx +38 -53
  42. package/src/hooks.ts +15 -18
  43. package/src/internal/hooks.ts +69 -45
  44. package/src/internal/logic/getRadioChecked.ts +1 -1
  45. package/src/internal/state/cleanup.ts +8 -0
  46. package/src/internal/state/controlledFieldStore.ts +91 -0
  47. package/src/internal/state/controlledFields.ts +31 -123
  48. package/src/internal/state/createFormStore.ts +152 -0
  49. package/src/internal/state/storeFamily.ts +24 -0
  50. package/src/internal/state/storeHooks.ts +22 -0
  51. package/src/unreleased/formStateHooks.ts +50 -27
  52. package/src/userFacingFormContext.ts +26 -5
  53. package/dist/types/internal/reset.d.ts +0 -28
  54. package/dist/types/internal/state/atomUtils.d.ts +0 -38
  55. package/dist/types/internal/state.d.ts +0 -343
  56. package/src/internal/reset.ts +0 -26
  57. package/src/internal/state/atomUtils.ts +0 -13
  58. package/src/internal/state.ts +0 -124
@@ -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
- ATOM_SCOPE,
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 = useFormAtomValue(isHydratedAtom(context.formId));
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 = useFormAtomValue(isHydratedAtom(formId));
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
- ) => useFormAtom(fieldTouchedAtom({ formId, field }));
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 [state, set] = useFormAtom(
144
- fieldErrorAtom({ formId: context.formId, field: name })
132
+ const state = useFormStore(
133
+ context.formId,
134
+ (state) => state.fieldErrors[name]
145
135
  );
146
- return [
147
- fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state),
148
- set,
149
- ] as const;
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 { defaultValues: state } = useFormAtomValue(
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 useClearError = ({ formId }: InternalFormContextValue) => {
166
- const updateError = useFormUpdateAtom(setFieldErrorAtom(formId));
167
- return useCallback(
168
- (name: string) => updateError({ field: name, error: undefined }),
169
- [updateError]
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
- export const useSetTouched = ({ formId }: InternalFormContextValue) => {
174
- const setTouched = useFormUpdateAtom(setTouchedAtom(formId));
175
- return useCallback(
176
- (name: string, touched: boolean) => setTouched({ field: name, touched }),
177
- [setTouched]
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("add", () => {
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
- useFieldDefaultValue,
8
- useFormAtomValue,
9
- useFormAtom,
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 fieldAtom = fieldValueAtom({ formId: context.formId, field });
92
- const [value, setValue] = useFormAtom(fieldAtom);
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
- const isHydrated = useFormAtomValue(isHydratedAtom(context.formId));
96
- const [isFieldHydrated, setIsFieldHydrated] = useFormAtom(
97
- fieldValueHydratedAtom({ formId: context.formId, field })
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 (isHydrated && !isFieldHydrated) {
102
- setValue(defaultValue);
103
- setIsFieldHydrated(true);
25
+ if (isFormHydrated && !isFieldHydrated) {
26
+ hydrateWithDefault(field, defaultValue);
104
27
  }
105
28
  }, [
106
29
  defaultValue,
107
30
  field,
108
- context.formId,
31
+ hydrateWithDefault,
109
32
  isFieldHydrated,
110
- isHydrated,
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 resolveUpdate = useFormAtomValue(
123
- resolveValueUpdateAtom({ formId: context.formId, field })
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 = useFormUpdateAtom(registerAtom);
130
- const unregister = useFormUpdateAtom(unregisterAtom);
52
+ const register = useValueStore((state) => state.register);
53
+ const unregister = useValueStore((state) => state.unregister);
131
54
  useEffect(() => {
132
- register({ formId: context.formId, field });
133
- return () => unregister({ formId: context.formId, field });
55
+ register(field);
56
+ return () => unregister(field);
134
57
  }, [context.formId, field, register, unregister]);
135
58
 
136
- const setControlledFieldValue = useFormUpdateAtom(
137
- setControlledFieldValueAtom
138
- );
59
+ const setControlledFieldValue = useValueStore((state) => state.setValue);
139
60
  const setValue = useCallback(
140
- (value: unknown) =>
141
- setControlledFieldValue({ formId: context.formId, field, value }),
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 setControlledFieldValue = useFormUpdateAtom(
152
- setControlledFieldValueAtom
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
- return useAtomCallback(
163
- useCallback(
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
+ };