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.
- package/.turbo/turbo-build.log +8 -5
- package/browser/ValidatedForm.js +21 -15
- package/browser/hooks.d.ts +1 -1
- package/browser/internal/hooks.d.ts +1 -1
- package/browser/internal/state/controlledFieldStore.d.ts +23 -21
- package/browser/internal/state/controlledFieldStore.js +32 -19
- package/browser/internal/state/controlledFields.d.ts +3 -3
- package/browser/internal/state/controlledFields.js +19 -21
- package/browser/internal/state/createFormStore.d.ts +13 -6
- package/browser/internal/state/createFormStore.js +48 -5
- package/browser/internal/state/storeHooks.d.ts +1 -3
- package/browser/internal/state/storeHooks.js +2 -8
- package/browser/internal/state/types.d.ts +1 -0
- package/browser/internal/state/types.js +1 -0
- package/browser/userFacingFormContext.d.ts +7 -0
- package/browser/userFacingFormContext.js +14 -4
- package/dist/remix-validated-form.cjs.js +4 -3
- package/dist/remix-validated-form.cjs.js.map +1 -0
- package/dist/remix-validated-form.es.js +136 -86
- package/dist/remix-validated-form.es.js.map +1 -0
- package/dist/remix-validated-form.umd.js +4 -3
- 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 +1 -1
- package/dist/types/internal/state/controlledFieldStore.d.ts +23 -21
- package/dist/types/internal/state/controlledFields.d.ts +3 -3
- package/dist/types/internal/state/createFormStore.d.ts +13 -6
- package/dist/types/internal/state/storeHooks.d.ts +1 -3
- package/dist/types/internal/state/types.d.ts +1 -0
- package/package.json +4 -4
- package/src/ValidatedForm.tsx +34 -23
- package/src/internal/hooks.ts +1 -1
- package/src/internal/state/controlledFieldStore.ts +95 -74
- package/src/internal/state/controlledFields.ts +38 -26
- package/src/internal/state/createFormStore.ts +174 -113
- package/src/internal/state/storeHooks.ts +3 -16
- package/src/internal/state/types.ts +1 -0
- package/src/userFacingFormContext.ts +23 -11
- package/dist/types/internal/state/cleanup.d.ts +0 -2
- package/dist/types/internal/state/storeFamily.d.ts +0 -9
- package/src/internal/state/cleanup.ts +0 -8
- 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 {
|
6
|
-
import {
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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,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
|
-
};
|