remix-validated-form 4.4.1 → 4.5.0-beta.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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +8 -5
  2. package/browser/ValidatedForm.js +21 -15
  3. package/browser/hooks.d.ts +1 -1
  4. package/browser/internal/hooks.d.ts +1 -1
  5. package/browser/internal/state/controlledFieldStore.d.ts +23 -21
  6. package/browser/internal/state/controlledFieldStore.js +32 -19
  7. package/browser/internal/state/controlledFields.d.ts +3 -3
  8. package/browser/internal/state/controlledFields.js +19 -21
  9. package/browser/internal/state/createFormStore.d.ts +13 -6
  10. package/browser/internal/state/createFormStore.js +48 -5
  11. package/browser/internal/state/storeHooks.d.ts +1 -3
  12. package/browser/internal/state/storeHooks.js +2 -8
  13. package/browser/internal/state/types.d.ts +1 -0
  14. package/browser/internal/state/types.js +1 -0
  15. package/browser/userFacingFormContext.d.ts +7 -0
  16. package/browser/userFacingFormContext.js +14 -4
  17. package/dist/remix-validated-form.cjs.js +4 -3
  18. package/dist/remix-validated-form.cjs.js.map +1 -0
  19. package/dist/remix-validated-form.es.js +136 -86
  20. package/dist/remix-validated-form.es.js.map +1 -0
  21. package/dist/remix-validated-form.umd.js +4 -3
  22. package/dist/remix-validated-form.umd.js.map +1 -0
  23. package/dist/types/hooks.d.ts +1 -1
  24. package/dist/types/internal/hooks.d.ts +1 -1
  25. package/dist/types/internal/state/controlledFieldStore.d.ts +23 -21
  26. package/dist/types/internal/state/controlledFields.d.ts +3 -3
  27. package/dist/types/internal/state/createFormStore.d.ts +13 -6
  28. package/dist/types/internal/state/storeHooks.d.ts +1 -3
  29. package/dist/types/internal/state/types.d.ts +1 -0
  30. package/package.json +4 -4
  31. package/src/ValidatedForm.tsx +34 -23
  32. package/src/internal/hooks.ts +1 -1
  33. package/src/internal/state/controlledFieldStore.ts +95 -74
  34. package/src/internal/state/controlledFields.ts +38 -26
  35. package/src/internal/state/createFormStore.ts +174 -113
  36. package/src/internal/state/storeHooks.ts +3 -16
  37. package/src/internal/state/types.ts +1 -0
  38. package/src/userFacingFormContext.ts +23 -11
  39. package/dist/types/internal/state/cleanup.d.ts +0 -2
  40. package/dist/types/internal/state/storeFamily.d.ts +0 -9
  41. package/src/internal/state/cleanup.ts +0 -8
  42. package/src/internal/state/storeFamily.ts +0 -24
@@ -1,9 +1,10 @@
1
+ import { WritableDraft } from "immer/dist/internal";
1
2
  import invariant from "tiny-invariant";
2
- import create from "zustand";
3
+ import create, { GetState } from "zustand";
3
4
  import { immer } from "zustand/middleware/immer";
4
5
  import { FieldErrors, TouchedFields, Validator } from "../../validation/types";
5
- import { controlledFieldStore } from "./controlledFieldStore";
6
- import { storeFamily } from "./storeFamily";
6
+ import { useControlledFieldStore } from "./controlledFieldStore";
7
+ import { InternalFormId } from "./types";
7
8
 
8
9
  export type SyncedFormProps = {
9
10
  formId?: string;
@@ -14,6 +15,13 @@ export type SyncedFormProps = {
14
15
  validator: Validator<unknown>;
15
16
  };
16
17
 
18
+ export type FormStoreState = {
19
+ forms: { [formId: InternalFormId]: FormState };
20
+ form: (formId: InternalFormId) => FormState;
21
+ registerForm: (formId: InternalFormId) => void;
22
+ cleanupForm: (formId: InternalFormId) => void;
23
+ };
24
+
17
25
  export type FormState = {
18
26
  isHydrated: boolean;
19
27
  isSubmitting: boolean;
@@ -39,114 +47,167 @@ export type FormState = {
39
47
  resetFormElement: () => void;
40
48
  };
41
49
 
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
- )
50
+ const noOp = () => {};
51
+ const defaultFormState: FormState = {
52
+ isHydrated: false,
53
+ isSubmitting: false,
54
+ hasBeenSubmitted: false,
55
+ touchedFields: {},
56
+ fieldErrors: {},
57
+ formElement: null,
58
+ isValid: () => true,
59
+ startSubmit: noOp,
60
+ endSubmit: noOp,
61
+ setTouched: noOp,
62
+ setFieldError: noOp,
63
+ setFieldErrors: noOp,
64
+ clearFieldError: noOp,
65
+
66
+ reset: () => noOp,
67
+ syncFormProps: noOp,
68
+ setHydrated: noOp,
69
+ setFormElement: noOp,
70
+ validateField: async () => null,
71
+
72
+ validate: async () => {},
73
+
74
+ resetFormElement: noOp,
75
+ };
76
+
77
+ const createFormState = (
78
+ formId: InternalFormId,
79
+ set: (setter: (draft: WritableDraft<FormState>) => void) => void,
80
+ get: GetState<FormState>
81
+ ): FormState => ({
82
+ // It's not "hydrated" until the form props are synced
83
+ isHydrated: false,
84
+ isSubmitting: false,
85
+ hasBeenSubmitted: false,
86
+ touchedFields: {},
87
+ fieldErrors: {},
88
+ formElement: null,
89
+
90
+ isValid: () => Object.keys(get().fieldErrors).length === 0,
91
+ startSubmit: () =>
92
+ set((state) => {
93
+ state.isSubmitting = true;
94
+ state.hasBeenSubmitted = true;
95
+ }),
96
+ endSubmit: () =>
97
+ set((state) => {
98
+ state.isSubmitting = false;
99
+ }),
100
+ setTouched: (fieldName, touched) =>
101
+ set((state) => {
102
+ state.touchedFields[fieldName] = touched;
103
+ }),
104
+ setFieldError: (fieldName: string, error: string) =>
105
+ set((state) => {
106
+ state.fieldErrors[fieldName] = error;
107
+ }),
108
+ setFieldErrors: (errors: FieldErrors) =>
109
+ set((state) => {
110
+ state.fieldErrors = errors;
111
+ }),
112
+ clearFieldError: (fieldName: string) =>
113
+ set((state) => {
114
+ delete state.fieldErrors[fieldName];
115
+ }),
116
+
117
+ reset: () =>
118
+ set((state) => {
119
+ state.fieldErrors = {};
120
+ state.touchedFields = {};
121
+ state.hasBeenSubmitted = false;
122
+ }),
123
+ syncFormProps: (props: SyncedFormProps) =>
124
+ set((state) => {
125
+ state.formProps = props;
126
+ state.isHydrated = true;
127
+ }),
128
+ setHydrated: () =>
129
+ set((state) => {
130
+ state.isHydrated = true;
131
+ }),
132
+ setFormElement: (formElement: HTMLFormElement | null) => {
133
+ // This gets called frequently, so we want to avoid calling set() every time
134
+ // Or else we wind up with an infinite loop
135
+ if (get().formElement === formElement) return;
136
+ set((state) => {
137
+ // weird type issue here
138
+ // seems to be because formElement is a writable draft
139
+ state.formElement = formElement as any;
140
+ });
141
+ },
142
+ validateField: async (field: string) => {
143
+ const formElement = get().formElement;
144
+ invariant(
145
+ formElement,
146
+ "Cannot find reference to form. This is probably a bug in remix-validated-form."
147
+ );
148
+
149
+ const validator = get().formProps?.validator;
150
+ invariant(
151
+ validator,
152
+ "Cannot validator. This is probably a bug in remix-validated-form."
153
+ );
154
+
155
+ await useControlledFieldStore.getState().awaitValueUpdate?.(formId, field);
156
+
157
+ const { error } = await validator.validateField(
158
+ new FormData(formElement),
159
+ field
160
+ );
161
+
162
+ if (error) {
163
+ get().setFieldError(field, error);
164
+ return error;
165
+ } else {
166
+ get().clearFieldError(field);
167
+ return null;
168
+ }
169
+ },
170
+
171
+ validate: async () => {
172
+ const formElement = get().formElement;
173
+ invariant(
174
+ formElement,
175
+ "Cannot find reference to form. This is probably a bug in remix-validated-form."
176
+ );
177
+
178
+ const validator = get().formProps?.validator;
179
+ invariant(
180
+ validator,
181
+ "Cannot validator. This is probably a bug in remix-validated-form."
182
+ );
183
+
184
+ const { error } = await validator.validate(new FormData(formElement));
185
+ if (error) get().setFieldErrors(error.fieldErrors);
186
+ },
187
+
188
+ resetFormElement: () => get().formElement?.reset(),
189
+ });
190
+
191
+ export const useRootFormStore = create<FormStoreState>()(
192
+ immer((set, get) => ({
193
+ forms: {},
194
+ form: (formId) => {
195
+ return get().forms[formId] ?? defaultFormState;
196
+ },
197
+ cleanupForm: (formId: InternalFormId) => {
198
+ set((state) => {
199
+ delete state.forms[formId];
200
+ });
201
+ },
202
+ registerForm: (formId: InternalFormId) => {
203
+ if (get().forms[formId]) return;
204
+ set((state) => {
205
+ state.forms[formId] = createFormState(
206
+ formId,
207
+ (setter) => set((state) => setter(state.forms[formId])),
208
+ () => get().forms[formId]
209
+ ) as WritableDraft<FormState>;
210
+ });
211
+ },
212
+ }))
152
213
  );
@@ -1,22 +1,9 @@
1
- import {
2
- ControlledFieldState,
3
- controlledFieldStore,
4
- } from "./controlledFieldStore";
5
- import { FormState, formStore } from "./createFormStore";
6
- import { InternalFormId } from "./storeFamily";
1
+ import { FormState, useRootFormStore } from "./createFormStore";
2
+ import { InternalFormId } from "./types";
7
3
 
8
4
  export const useFormStore = <T>(
9
5
  formId: InternalFormId,
10
6
  selector: (state: FormState) => T
11
7
  ) => {
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);
8
+ return useRootFormStore((state) => selector(state.form(formId)));
22
9
  };
@@ -0,0 +1 @@
1
+ export type InternalFormId = string | symbol;
@@ -1,4 +1,4 @@
1
- import { useCallback } from "react";
1
+ import { useCallback, useMemo } from "react";
2
2
  import {
3
3
  useInternalFormContext,
4
4
  useRegisterReceiveFocus,
@@ -102,14 +102,26 @@ export const useFormContext = (formId?: string): FormContextValue => {
102
102
  [internalClearError]
103
103
  );
104
104
 
105
- return {
106
- ...state,
107
- setFieldTouched: setTouched,
108
- validateField,
109
- clearError,
110
- registerReceiveFocus,
111
- clearAllErrors,
112
- validate,
113
- reset,
114
- };
105
+ return useMemo(
106
+ () => ({
107
+ ...state,
108
+ setFieldTouched: setTouched,
109
+ validateField,
110
+ clearError,
111
+ registerReceiveFocus,
112
+ clearAllErrors,
113
+ validate,
114
+ reset,
115
+ }),
116
+ [
117
+ clearAllErrors,
118
+ clearError,
119
+ registerReceiveFocus,
120
+ reset,
121
+ setTouched,
122
+ state,
123
+ validate,
124
+ validateField,
125
+ ]
126
+ );
115
127
  };
@@ -1,2 +0,0 @@
1
- import { InternalFormId } from "./storeFamily";
2
- export declare const cleanupFormState: (formId: InternalFormId) => void;
@@ -1,9 +0,0 @@
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
- export declare type InternalFormId = string | symbol;
6
- export declare const storeFamily: <T>(create: (formId: InternalFormId) => T) => {
7
- (formId: InternalFormId): T;
8
- remove(formId: InternalFormId): void;
9
- };
@@ -1,8 +0,0 @@
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
- };
@@ -1,24 +0,0 @@
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
- };