remix-validated-form 4.4.2 → 4.5.0-beta.1
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 +13 -10
- package/browser/ValidatedForm.js +24 -39
- package/browser/hooks.d.ts +1 -1
- package/browser/internal/hooks.d.ts +3 -2
- package/browser/internal/hooks.js +1 -0
- 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 +16 -8
- package/browser/internal/state/createFormStore.js +62 -8
- 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/unreleased/formStateHooks.d.ts +8 -2
- package/browser/unreleased/formStateHooks.js +12 -2
- package/browser/userFacingFormContext.d.ts +8 -2
- package/browser/userFacingFormContext.js +15 -4
- package/dist/remix-validated-form.cjs.js +3 -3
- package/dist/remix-validated-form.cjs.js.map +1 -1
- package/dist/remix-validated-form.es.js +169 -113
- package/dist/remix-validated-form.es.js.map +1 -1
- package/dist/remix-validated-form.umd.js +3 -3
- package/dist/remix-validated-form.umd.js.map +1 -1
- package/dist/types/hooks.d.ts +1 -1
- package/dist/types/internal/hooks.d.ts +3 -2
- 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 +16 -8
- package/dist/types/internal/state/storeHooks.d.ts +1 -3
- package/dist/types/internal/state/types.d.ts +1 -0
- package/dist/types/unreleased/formStateHooks.d.ts +8 -2
- package/dist/types/userFacingFormContext.d.ts +8 -2
- package/package.json +4 -4
- package/src/ValidatedForm.tsx +41 -56
- package/src/internal/hooks.ts +4 -1
- package/src/internal/state/controlledFieldStore.ts +95 -74
- package/src/internal/state/controlledFields.ts +38 -26
- package/src/internal/state/createFormStore.ts +199 -115
- package/src/internal/state/storeHooks.ts +3 -16
- package/src/internal/state/types.ts +1 -0
- package/src/unreleased/formStateHooks.ts +24 -3
- package/src/userFacingFormContext.ts +38 -13
- 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,31 +1,37 @@
|
|
1
1
|
import { useCallback, useEffect } from "react";
|
2
2
|
import { InternalFormContextValue } from "../formContext";
|
3
3
|
import { useFieldDefaultValue } from "../hooks";
|
4
|
-
import {
|
5
|
-
import {
|
6
|
-
import { InternalFormId } from "./
|
4
|
+
import { useControlledFieldStore } from "./controlledFieldStore";
|
5
|
+
import { useFormStore } from "./storeHooks";
|
6
|
+
import { InternalFormId } from "./types";
|
7
7
|
|
8
8
|
export const useControlledFieldValue = (
|
9
9
|
context: InternalFormContextValue,
|
10
10
|
field: string
|
11
11
|
) => {
|
12
|
-
const
|
13
|
-
|
12
|
+
const value = useControlledFieldStore(
|
13
|
+
(state) => state.getField(context.formId, field)?.value
|
14
|
+
);
|
14
15
|
|
15
|
-
const
|
16
|
-
|
16
|
+
const isFormHydrated = useFormStore(
|
17
|
+
context.formId,
|
18
|
+
(state) => state.isHydrated
|
19
|
+
);
|
17
20
|
const defaultValue = useFieldDefaultValue(field, context);
|
18
21
|
|
19
|
-
const isFieldHydrated =
|
20
|
-
(state) => state.
|
22
|
+
const isFieldHydrated = useControlledFieldStore(
|
23
|
+
(state) => state.getField(context.formId, field)?.hydrated ?? false
|
24
|
+
);
|
25
|
+
const hydrateWithDefault = useControlledFieldStore(
|
26
|
+
(state) => state.hydrateWithDefault
|
21
27
|
);
|
22
|
-
const hydrateWithDefault = useValueStore((state) => state.hydrateWithDefault);
|
23
28
|
|
24
29
|
useEffect(() => {
|
25
30
|
if (isFormHydrated && !isFieldHydrated) {
|
26
|
-
hydrateWithDefault(field, defaultValue);
|
31
|
+
hydrateWithDefault(context.formId, field, defaultValue);
|
27
32
|
}
|
28
33
|
}, [
|
34
|
+
context.formId,
|
29
35
|
defaultValue,
|
30
36
|
field,
|
31
37
|
hydrateWithDefault,
|
@@ -40,26 +46,26 @@ export const useControllableValue = (
|
|
40
46
|
context: InternalFormContextValue,
|
41
47
|
field: string
|
42
48
|
) => {
|
43
|
-
const
|
44
|
-
|
45
|
-
const resolveUpdate = useValueStore(
|
46
|
-
(state) => state.fields[field]?.resolveValueUpdate
|
49
|
+
const resolveUpdate = useControlledFieldStore(
|
50
|
+
(state) => state.getField(context.formId, field)?.resolveValueUpdate
|
47
51
|
);
|
48
52
|
useEffect(() => {
|
49
53
|
resolveUpdate?.();
|
50
54
|
}, [resolveUpdate]);
|
51
55
|
|
52
|
-
const register =
|
53
|
-
const unregister =
|
56
|
+
const register = useControlledFieldStore((state) => state.register);
|
57
|
+
const unregister = useControlledFieldStore((state) => state.unregister);
|
54
58
|
useEffect(() => {
|
55
|
-
register(field);
|
56
|
-
return () => unregister(field);
|
59
|
+
register(context.formId, field);
|
60
|
+
return () => unregister(context.formId, field);
|
57
61
|
}, [context.formId, field, register, unregister]);
|
58
62
|
|
59
|
-
const setControlledFieldValue =
|
63
|
+
const setControlledFieldValue = useControlledFieldStore(
|
64
|
+
(state) => state.setValue
|
65
|
+
);
|
60
66
|
const setValue = useCallback(
|
61
|
-
(value: unknown) => setControlledFieldValue(field, value),
|
62
|
-
[field, setControlledFieldValue]
|
67
|
+
(value: unknown) => setControlledFieldValue(context.formId, field, value),
|
68
|
+
[context.formId, field, setControlledFieldValue]
|
63
69
|
);
|
64
70
|
|
65
71
|
const value = useControlledFieldValue(context, field);
|
@@ -68,11 +74,17 @@ export const useControllableValue = (
|
|
68
74
|
};
|
69
75
|
|
70
76
|
export const useUpdateControllableValue = (formId: InternalFormId) => {
|
71
|
-
const
|
72
|
-
return
|
77
|
+
const setValue = useControlledFieldStore((state) => state.setValue);
|
78
|
+
return useCallback(
|
79
|
+
(field: string, value: unknown) => setValue(formId, field, value),
|
80
|
+
[formId, setValue]
|
81
|
+
);
|
73
82
|
};
|
74
83
|
|
75
84
|
export const useAwaitValue = (formId: InternalFormId) => {
|
76
|
-
const
|
77
|
-
return
|
85
|
+
const awaitValue = useControlledFieldStore((state) => state.awaitValueUpdate);
|
86
|
+
return useCallback(
|
87
|
+
(field: string) => awaitValue(formId, field),
|
88
|
+
[awaitValue, formId]
|
89
|
+
);
|
78
90
|
};
|
@@ -1,9 +1,15 @@
|
|
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
|
-
import {
|
5
|
-
|
6
|
-
|
5
|
+
import {
|
6
|
+
FieldErrors,
|
7
|
+
TouchedFields,
|
8
|
+
ValidationResult,
|
9
|
+
Validator,
|
10
|
+
} from "../../validation/types";
|
11
|
+
import { useControlledFieldStore } from "./controlledFieldStore";
|
12
|
+
import { InternalFormId } from "./types";
|
7
13
|
|
8
14
|
export type SyncedFormProps = {
|
9
15
|
formId?: string;
|
@@ -14,6 +20,13 @@ export type SyncedFormProps = {
|
|
14
20
|
validator: Validator<unknown>;
|
15
21
|
};
|
16
22
|
|
23
|
+
export type FormStoreState = {
|
24
|
+
forms: { [formId: InternalFormId]: FormState };
|
25
|
+
form: (formId: InternalFormId) => FormState;
|
26
|
+
registerForm: (formId: InternalFormId) => void;
|
27
|
+
cleanupForm: (formId: InternalFormId) => void;
|
28
|
+
};
|
29
|
+
|
17
30
|
export type FormState = {
|
18
31
|
isHydrated: boolean;
|
19
32
|
isSubmitting: boolean;
|
@@ -35,118 +48,189 @@ export type FormState = {
|
|
35
48
|
setHydrated: () => void;
|
36
49
|
setFormElement: (formElement: HTMLFormElement | null) => void;
|
37
50
|
validateField: (fieldName: string) => Promise<string | null>;
|
38
|
-
validate: () => Promise<
|
51
|
+
validate: () => Promise<ValidationResult<unknown>>;
|
39
52
|
resetFormElement: () => void;
|
53
|
+
submit: () => void;
|
40
54
|
};
|
41
55
|
|
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
|
-
|
56
|
+
const noOp = () => {};
|
57
|
+
const defaultFormState: FormState = {
|
58
|
+
isHydrated: false,
|
59
|
+
isSubmitting: false,
|
60
|
+
hasBeenSubmitted: false,
|
61
|
+
touchedFields: {},
|
62
|
+
fieldErrors: {},
|
63
|
+
formElement: null,
|
64
|
+
isValid: () => true,
|
65
|
+
startSubmit: noOp,
|
66
|
+
endSubmit: noOp,
|
67
|
+
setTouched: noOp,
|
68
|
+
setFieldError: noOp,
|
69
|
+
setFieldErrors: noOp,
|
70
|
+
clearFieldError: noOp,
|
71
|
+
|
72
|
+
reset: () => noOp,
|
73
|
+
syncFormProps: noOp,
|
74
|
+
setHydrated: noOp,
|
75
|
+
setFormElement: noOp,
|
76
|
+
validateField: async () => null,
|
77
|
+
|
78
|
+
validate: async () => {
|
79
|
+
throw new Error("Validate called before form was initialized.");
|
80
|
+
},
|
81
|
+
|
82
|
+
submit: async () => {
|
83
|
+
throw new Error("Submit called before form was initialized.");
|
84
|
+
},
|
85
|
+
|
86
|
+
resetFormElement: noOp,
|
87
|
+
};
|
88
|
+
|
89
|
+
const createFormState = (
|
90
|
+
formId: InternalFormId,
|
91
|
+
set: (setter: (draft: WritableDraft<FormState>) => void) => void,
|
92
|
+
get: GetState<FormState>
|
93
|
+
): FormState => ({
|
94
|
+
// It's not "hydrated" until the form props are synced
|
95
|
+
isHydrated: false,
|
96
|
+
isSubmitting: false,
|
97
|
+
hasBeenSubmitted: false,
|
98
|
+
touchedFields: {},
|
99
|
+
fieldErrors: {},
|
100
|
+
formElement: null,
|
101
|
+
|
102
|
+
isValid: () => Object.keys(get().fieldErrors).length === 0,
|
103
|
+
startSubmit: () =>
|
104
|
+
set((state) => {
|
105
|
+
state.isSubmitting = true;
|
106
|
+
state.hasBeenSubmitted = true;
|
107
|
+
}),
|
108
|
+
endSubmit: () =>
|
109
|
+
set((state) => {
|
110
|
+
state.isSubmitting = false;
|
111
|
+
}),
|
112
|
+
setTouched: (fieldName, touched) =>
|
113
|
+
set((state) => {
|
114
|
+
state.touchedFields[fieldName] = touched;
|
115
|
+
}),
|
116
|
+
setFieldError: (fieldName: string, error: string) =>
|
117
|
+
set((state) => {
|
118
|
+
state.fieldErrors[fieldName] = error;
|
119
|
+
}),
|
120
|
+
setFieldErrors: (errors: FieldErrors) =>
|
121
|
+
set((state) => {
|
122
|
+
state.fieldErrors = errors;
|
123
|
+
}),
|
124
|
+
clearFieldError: (fieldName: string) =>
|
125
|
+
set((state) => {
|
126
|
+
delete state.fieldErrors[fieldName];
|
127
|
+
}),
|
128
|
+
|
129
|
+
reset: () =>
|
130
|
+
set((state) => {
|
131
|
+
state.fieldErrors = {};
|
132
|
+
state.touchedFields = {};
|
133
|
+
state.hasBeenSubmitted = false;
|
134
|
+
}),
|
135
|
+
syncFormProps: (props: SyncedFormProps) =>
|
136
|
+
set((state) => {
|
137
|
+
state.formProps = props;
|
138
|
+
state.isHydrated = true;
|
139
|
+
}),
|
140
|
+
setHydrated: () =>
|
141
|
+
set((state) => {
|
142
|
+
state.isHydrated = true;
|
143
|
+
}),
|
144
|
+
setFormElement: (formElement: HTMLFormElement | null) => {
|
145
|
+
// This gets called frequently, so we want to avoid calling set() every time
|
146
|
+
// Or else we wind up with an infinite loop
|
147
|
+
if (get().formElement === formElement) return;
|
148
|
+
set((state) => {
|
149
|
+
// weird type issue here
|
150
|
+
// seems to be because formElement is a writable draft
|
151
|
+
state.formElement = formElement as any;
|
152
|
+
});
|
153
|
+
},
|
154
|
+
validateField: async (field: string) => {
|
155
|
+
const formElement = get().formElement;
|
156
|
+
invariant(
|
157
|
+
formElement,
|
158
|
+
"Cannot find reference to form. This is probably a bug in remix-validated-form."
|
159
|
+
);
|
160
|
+
|
161
|
+
const validator = get().formProps?.validator;
|
162
|
+
invariant(
|
163
|
+
validator,
|
164
|
+
"Cannot validator. This is probably a bug in remix-validated-form."
|
165
|
+
);
|
166
|
+
|
167
|
+
await useControlledFieldStore.getState().awaitValueUpdate?.(formId, field);
|
168
|
+
|
169
|
+
const { error } = await validator.validateField(
|
170
|
+
new FormData(formElement),
|
171
|
+
field
|
172
|
+
);
|
173
|
+
|
174
|
+
if (error) {
|
175
|
+
get().setFieldError(field, error);
|
176
|
+
return error;
|
177
|
+
} else {
|
178
|
+
get().clearFieldError(field);
|
179
|
+
return null;
|
180
|
+
}
|
181
|
+
},
|
182
|
+
|
183
|
+
validate: async () => {
|
184
|
+
const formElement = get().formElement;
|
185
|
+
invariant(
|
186
|
+
formElement,
|
187
|
+
"Cannot find reference to form. This is probably a bug in remix-validated-form."
|
188
|
+
);
|
189
|
+
|
190
|
+
const validator = get().formProps?.validator;
|
191
|
+
invariant(
|
192
|
+
validator,
|
193
|
+
"Cannot validator. This is probably a bug in remix-validated-form."
|
194
|
+
);
|
195
|
+
|
196
|
+
const result = await validator.validate(new FormData(formElement));
|
197
|
+
if (result.error) get().setFieldErrors(result.error.fieldErrors);
|
198
|
+
return result;
|
199
|
+
},
|
200
|
+
|
201
|
+
submit: () => {
|
202
|
+
const formElement = get().formElement;
|
203
|
+
invariant(
|
204
|
+
formElement,
|
205
|
+
"Cannot find reference to form. This is probably a bug in remix-validated-form."
|
206
|
+
);
|
207
|
+
|
208
|
+
formElement.submit();
|
209
|
+
},
|
210
|
+
|
211
|
+
resetFormElement: () => get().formElement?.reset(),
|
212
|
+
});
|
213
|
+
|
214
|
+
export const useRootFormStore = create<FormStoreState>()(
|
215
|
+
immer((set, get) => ({
|
216
|
+
forms: {},
|
217
|
+
form: (formId) => {
|
218
|
+
return get().forms[formId] ?? defaultFormState;
|
219
|
+
},
|
220
|
+
cleanupForm: (formId: InternalFormId) => {
|
221
|
+
set((state) => {
|
222
|
+
delete state.forms[formId];
|
223
|
+
});
|
224
|
+
},
|
225
|
+
registerForm: (formId: InternalFormId) => {
|
226
|
+
if (get().forms[formId]) return;
|
227
|
+
set((state) => {
|
228
|
+
state.forms[formId] = createFormState(
|
229
|
+
formId,
|
230
|
+
(setter) => set((state) => setter(state.forms[formId])),
|
231
|
+
() => get().forms[formId]
|
232
|
+
) as WritableDraft<FormState>;
|
233
|
+
});
|
234
|
+
},
|
235
|
+
}))
|
152
236
|
);
|
@@ -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;
|
@@ -18,8 +18,13 @@ import {
|
|
18
18
|
useSyncedDefaultValues,
|
19
19
|
useFormActionProp,
|
20
20
|
useFormSubactionProp,
|
21
|
+
useSubmitForm,
|
21
22
|
} from "../internal/hooks";
|
22
|
-
import {
|
23
|
+
import {
|
24
|
+
FieldErrors,
|
25
|
+
TouchedFields,
|
26
|
+
ValidationResult,
|
27
|
+
} from "../validation/types";
|
23
28
|
|
24
29
|
export type FormState = {
|
25
30
|
fieldErrors: FieldErrors;
|
@@ -95,7 +100,7 @@ export type FormHelpers = {
|
|
95
100
|
/**
|
96
101
|
* Validate the whole form and populate any errors.
|
97
102
|
*/
|
98
|
-
validate: () => Promise<
|
103
|
+
validate: () => Promise<ValidationResult<unknown>>;
|
99
104
|
/**
|
100
105
|
* Clears all errors on the form.
|
101
106
|
*/
|
@@ -107,6 +112,12 @@ export type FormHelpers = {
|
|
107
112
|
* or clicking a button element with `type="reset"`.
|
108
113
|
*/
|
109
114
|
reset: () => void;
|
115
|
+
/**
|
116
|
+
* Submits the form, running all validations first.
|
117
|
+
*
|
118
|
+
* _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
|
119
|
+
*/
|
120
|
+
submit: () => void;
|
110
121
|
};
|
111
122
|
|
112
123
|
/**
|
@@ -122,6 +133,7 @@ export const useFormHelpers = (formId?: string): FormHelpers => {
|
|
122
133
|
const clearError = useClearError(formContext);
|
123
134
|
const setFieldErrors = useSetFieldErrors(formContext.formId);
|
124
135
|
const reset = useResetFormElement(formContext.formId);
|
136
|
+
const submit = useSubmitForm(formContext.formId);
|
125
137
|
return useMemo(
|
126
138
|
() => ({
|
127
139
|
setTouched,
|
@@ -130,7 +142,16 @@ export const useFormHelpers = (formId?: string): FormHelpers => {
|
|
130
142
|
validate,
|
131
143
|
clearAllErrors: () => setFieldErrors({}),
|
132
144
|
reset,
|
145
|
+
submit,
|
133
146
|
}),
|
134
|
-
[
|
147
|
+
[
|
148
|
+
clearError,
|
149
|
+
reset,
|
150
|
+
setFieldErrors,
|
151
|
+
setTouched,
|
152
|
+
submit,
|
153
|
+
validate,
|
154
|
+
validateField,
|
155
|
+
]
|
135
156
|
);
|
136
157
|
};
|
@@ -1,10 +1,14 @@
|
|
1
|
-
import { useCallback } from "react";
|
1
|
+
import { useCallback, useMemo } from "react";
|
2
2
|
import {
|
3
3
|
useInternalFormContext,
|
4
4
|
useRegisterReceiveFocus,
|
5
5
|
} from "./internal/hooks";
|
6
6
|
import { useFormHelpers, useFormState } from "./unreleased/formStateHooks";
|
7
|
-
import {
|
7
|
+
import {
|
8
|
+
FieldErrors,
|
9
|
+
TouchedFields,
|
10
|
+
ValidationResult,
|
11
|
+
} from "./validation/types";
|
8
12
|
|
9
13
|
export type FormContextValue = {
|
10
14
|
/**
|
@@ -61,7 +65,7 @@ export type FormContextValue = {
|
|
61
65
|
/**
|
62
66
|
* Validate the whole form and populate any errors.
|
63
67
|
*/
|
64
|
-
validate: () => Promise<
|
68
|
+
validate: () => Promise<ValidationResult<unknown>>;
|
65
69
|
/**
|
66
70
|
* Clears all errors on the form.
|
67
71
|
*/
|
@@ -73,6 +77,12 @@ export type FormContextValue = {
|
|
73
77
|
* or clicking a button element with `type="reset"`.
|
74
78
|
*/
|
75
79
|
reset: () => void;
|
80
|
+
/**
|
81
|
+
* Submits the form, running all validations first.
|
82
|
+
*
|
83
|
+
* _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
|
84
|
+
*/
|
85
|
+
submit: () => void;
|
76
86
|
};
|
77
87
|
|
78
88
|
/**
|
@@ -89,6 +99,7 @@ export const useFormContext = (formId?: string): FormContextValue => {
|
|
89
99
|
clearAllErrors,
|
90
100
|
validate,
|
91
101
|
reset,
|
102
|
+
submit,
|
92
103
|
} = useFormHelpers(formId);
|
93
104
|
|
94
105
|
const registerReceiveFocus = useRegisterReceiveFocus(context.formId);
|
@@ -102,14 +113,28 @@ export const useFormContext = (formId?: string): FormContextValue => {
|
|
102
113
|
[internalClearError]
|
103
114
|
);
|
104
115
|
|
105
|
-
return
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
116
|
+
return useMemo(
|
117
|
+
() => ({
|
118
|
+
...state,
|
119
|
+
setFieldTouched: setTouched,
|
120
|
+
validateField,
|
121
|
+
clearError,
|
122
|
+
registerReceiveFocus,
|
123
|
+
clearAllErrors,
|
124
|
+
validate,
|
125
|
+
reset,
|
126
|
+
submit,
|
127
|
+
}),
|
128
|
+
[
|
129
|
+
clearAllErrors,
|
130
|
+
clearError,
|
131
|
+
registerReceiveFocus,
|
132
|
+
reset,
|
133
|
+
setTouched,
|
134
|
+
state,
|
135
|
+
submit,
|
136
|
+
validate,
|
137
|
+
validateField,
|
138
|
+
]
|
139
|
+
);
|
115
140
|
};
|
@@ -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
|
-
};
|