remix-validated-form 4.0.1 → 4.1.0-beta.3
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 +2 -2
- package/README.md +4 -4
- package/browser/ValidatedForm.d.ts +2 -2
- package/browser/ValidatedForm.js +142 -149
- package/browser/components.d.ts +5 -8
- package/browser/components.js +5 -5
- package/browser/hooks.d.ts +19 -14
- package/browser/hooks.js +41 -39
- package/browser/index.d.ts +1 -1
- package/browser/index.js +1 -0
- package/browser/internal/constants.d.ts +3 -0
- package/browser/internal/constants.js +3 -0
- package/browser/internal/formContext.d.ts +7 -49
- package/browser/internal/formContext.js +1 -1
- package/browser/internal/getInputProps.js +4 -3
- package/browser/internal/hooks.d.ts +22 -0
- package/browser/internal/hooks.js +110 -0
- package/browser/internal/state.d.ts +269 -0
- package/browser/internal/state.js +82 -0
- package/browser/internal/util.d.ts +1 -0
- package/browser/internal/util.js +2 -0
- package/browser/lowLevelHooks.d.ts +0 -0
- package/browser/lowLevelHooks.js +1 -0
- package/browser/server.d.ts +5 -0
- package/browser/server.js +5 -0
- package/browser/userFacingFormContext.d.ts +56 -0
- package/browser/userFacingFormContext.js +40 -0
- package/browser/validation/createValidator.js +4 -0
- package/browser/validation/types.d.ts +3 -0
- package/build/ValidatedForm.d.ts +2 -2
- package/build/ValidatedForm.js +138 -145
- package/build/hooks.d.ts +19 -14
- package/build/hooks.js +43 -45
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -0
- package/build/internal/constants.d.ts +3 -0
- package/build/internal/constants.js +7 -0
- package/build/internal/formContext.d.ts +7 -49
- package/build/internal/formContext.js +2 -2
- package/build/internal/getInputProps.js +7 -3
- package/build/internal/hooks.d.ts +22 -0
- package/build/internal/hooks.js +130 -0
- package/build/internal/state.d.ts +269 -0
- package/build/internal/state.js +92 -0
- package/build/internal/util.d.ts +1 -0
- package/build/internal/util.js +3 -1
- package/build/server.d.ts +5 -0
- package/build/server.js +7 -1
- package/build/userFacingFormContext.d.ts +56 -0
- package/build/userFacingFormContext.js +44 -0
- package/build/validation/createValidator.js +4 -0
- package/build/validation/types.d.ts +3 -0
- package/package.json +3 -1
- package/src/ValidatedForm.tsx +205 -200
- package/src/hooks.ts +71 -54
- package/src/index.ts +1 -1
- package/src/internal/constants.ts +4 -0
- package/src/internal/formContext.ts +8 -49
- package/src/internal/getInputProps.ts +6 -4
- package/src/internal/hooks.ts +191 -0
- package/src/internal/state.ts +210 -0
- package/src/internal/util.ts +4 -0
- package/src/server.ts +16 -0
- package/src/userFacingFormContext.ts +129 -0
- package/src/validation/createValidator.ts +4 -0
- package/src/validation/types.ts +3 -1
@@ -0,0 +1,44 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useFormContext = void 0;
|
4
|
+
const react_1 = require("react");
|
5
|
+
const hooks_1 = require("./hooks");
|
6
|
+
const hooks_2 = require("./internal/hooks");
|
7
|
+
const state_1 = require("./internal/state");
|
8
|
+
/**
|
9
|
+
* Provides access to some of the internal state of the form.
|
10
|
+
*/
|
11
|
+
const useFormContext = (formId) => {
|
12
|
+
// Try to access context so we get our error specific to this hook if it's not there
|
13
|
+
const context = (0, hooks_2.useInternalFormContext)(formId, "useFormContext");
|
14
|
+
const action = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.actionAtom);
|
15
|
+
const isSubmitting = (0, hooks_1.useIsSubmitting)(formId);
|
16
|
+
const hasBeenSubmitted = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.hasBeenSubmittedAtom);
|
17
|
+
const isValid = (0, hooks_1.useIsValid)(formId);
|
18
|
+
const defaultValues = (0, hooks_2.useHydratableSelector)(context, state_1.defaultValuesAtom, (0, hooks_2.useDefaultValuesForForm)(context));
|
19
|
+
const fieldErrors = (0, hooks_2.useHydratableSelector)(context, state_1.fieldErrorsAtom, (0, hooks_2.useFieldErrorsForForm)(context));
|
20
|
+
const setFieldTouched = (0, hooks_2.useSetTouched)(context);
|
21
|
+
const touchedFields = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.touchedFieldsAtom);
|
22
|
+
const validateField = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.validateFieldAtom);
|
23
|
+
const registerReceiveFocus = (0, hooks_2.useContextSelectAtom)(context.formId, state_1.registerReceiveFocusAtom);
|
24
|
+
const internalClearError = (0, hooks_2.useClearError)(context);
|
25
|
+
const clearError = (0, react_1.useCallback)((...names) => {
|
26
|
+
names.forEach((name) => {
|
27
|
+
internalClearError(name);
|
28
|
+
});
|
29
|
+
}, [internalClearError]);
|
30
|
+
return {
|
31
|
+
isSubmitting,
|
32
|
+
hasBeenSubmitted,
|
33
|
+
isValid,
|
34
|
+
defaultValues,
|
35
|
+
clearError,
|
36
|
+
fieldErrors: fieldErrors !== null && fieldErrors !== void 0 ? fieldErrors : {},
|
37
|
+
action,
|
38
|
+
setFieldTouched,
|
39
|
+
touchedFields,
|
40
|
+
validateField,
|
41
|
+
registerReceiveFocus,
|
42
|
+
};
|
43
|
+
};
|
44
|
+
exports.useFormContext = useFormContext;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.createValidator = void 0;
|
4
|
+
const constants_1 = require("../internal/constants");
|
4
5
|
const flatten_1 = require("../internal/flatten");
|
5
6
|
const preprocessFormData = (data) => {
|
6
7
|
// A slightly janky way of determining if the data is a FormData object
|
@@ -25,14 +26,17 @@ function createValidator(validator) {
|
|
25
26
|
error: {
|
26
27
|
fieldErrors: result.error,
|
27
28
|
subaction: data.subaction,
|
29
|
+
formId: data[constants_1.FORM_ID_FIELD],
|
28
30
|
},
|
29
31
|
submittedData: data,
|
32
|
+
formId: data[constants_1.FORM_ID_FIELD],
|
30
33
|
};
|
31
34
|
}
|
32
35
|
return {
|
33
36
|
data: result.data,
|
34
37
|
error: undefined,
|
35
38
|
submittedData: data,
|
39
|
+
formId: data[constants_1.FORM_ID_FIELD],
|
36
40
|
};
|
37
41
|
},
|
38
42
|
validateField: (data, field) => validator.validateField(preprocessFormData(data), field),
|
@@ -5,15 +5,18 @@ export declare type GenericObject = {
|
|
5
5
|
};
|
6
6
|
export declare type ValidatorError = {
|
7
7
|
subaction?: string;
|
8
|
+
formId?: string;
|
8
9
|
fieldErrors: FieldErrors;
|
9
10
|
};
|
10
11
|
export declare type ValidationErrorResponseData = {
|
11
12
|
subaction?: string;
|
13
|
+
formId?: string;
|
12
14
|
fieldErrors: FieldErrors;
|
13
15
|
repopulateFields?: unknown;
|
14
16
|
};
|
15
17
|
export declare type BaseResult = {
|
16
18
|
submittedData: GenericObject;
|
19
|
+
formId?: string;
|
17
20
|
};
|
18
21
|
export declare type ErrorResult = BaseResult & {
|
19
22
|
error: ValidatorError;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "remix-validated-form",
|
3
|
-
"version": "4.0.
|
3
|
+
"version": "4.1.0-beta.3",
|
4
4
|
"description": "Form component and utils for easy form validation in remix",
|
5
5
|
"browser": "./browser/index.js",
|
6
6
|
"main": "./build/index.js",
|
@@ -48,6 +48,8 @@
|
|
48
48
|
"typescript": "^4.5.3"
|
49
49
|
},
|
50
50
|
"dependencies": {
|
51
|
+
"immer": "^9.0.12",
|
52
|
+
"jotai": "^1.5.3",
|
51
53
|
"lodash": "^4.17.21",
|
52
54
|
"tiny-invariant": "^1.2.0"
|
53
55
|
}
|
package/src/ValidatedForm.tsx
CHANGED
@@ -1,31 +1,46 @@
|
|
1
|
-
import {
|
2
|
-
Form as RemixForm,
|
3
|
-
useActionData,
|
4
|
-
useFetcher,
|
5
|
-
useFormAction,
|
6
|
-
useSubmit,
|
7
|
-
useTransition,
|
8
|
-
} from "@remix-run/react";
|
9
|
-
import { Fetcher } from "@remix-run/react/transition";
|
1
|
+
import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
|
10
2
|
import uniq from "lodash/uniq";
|
11
3
|
import React, {
|
12
4
|
ComponentProps,
|
5
|
+
FormEvent,
|
6
|
+
RefObject,
|
7
|
+
useCallback,
|
13
8
|
useEffect,
|
14
9
|
useMemo,
|
15
10
|
useRef,
|
16
11
|
useState,
|
17
12
|
} from "react";
|
18
13
|
import invariant from "tiny-invariant";
|
19
|
-
import {
|
14
|
+
import { useIsSubmitting, useIsValid } from "./hooks";
|
15
|
+
import { FORM_ID_FIELD } from "./internal/constants";
|
16
|
+
import {
|
17
|
+
InternalFormContext,
|
18
|
+
InternalFormContextValue,
|
19
|
+
} from "./internal/formContext";
|
20
|
+
import {
|
21
|
+
useDefaultValuesFromLoader,
|
22
|
+
useErrorResponseForForm,
|
23
|
+
useFormUpdateAtom,
|
24
|
+
useHasActiveFormSubmit,
|
25
|
+
} from "./internal/hooks";
|
20
26
|
import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
|
27
|
+
import {
|
28
|
+
addErrorAtom,
|
29
|
+
clearErrorAtom,
|
30
|
+
endSubmitAtom,
|
31
|
+
formRegistry,
|
32
|
+
FormState,
|
33
|
+
resetAtom,
|
34
|
+
setFieldErrorsAtom,
|
35
|
+
startSubmitAtom,
|
36
|
+
syncFormContextAtom,
|
37
|
+
} from "./internal/state";
|
21
38
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
22
|
-
import { omit, mergeRefs } from "./internal/util";
|
23
39
|
import {
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
} from "./validation/types";
|
40
|
+
mergeRefs,
|
41
|
+
useIsomorphicLayoutEffect as useLayoutEffect,
|
42
|
+
} from "./internal/util";
|
43
|
+
import { FieldErrors, Validator } from "./validation/types";
|
29
44
|
|
30
45
|
export type FormProps<DataType> = {
|
31
46
|
/**
|
@@ -39,7 +54,7 @@ export type FormProps<DataType> = {
|
|
39
54
|
onSubmit?: (
|
40
55
|
data: DataType,
|
41
56
|
event: React.FormEvent<HTMLFormElement>
|
42
|
-
) => Promise<void>;
|
57
|
+
) => void | Promise<void>;
|
43
58
|
/**
|
44
59
|
* Allows you to provide a `fetcher` from remix's `useFetcher` hook.
|
45
60
|
* The form will use the fetcher for loading states, action data, etc
|
@@ -74,79 +89,8 @@ export type FormProps<DataType> = {
|
|
74
89
|
disableFocusOnError?: boolean;
|
75
90
|
} & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
|
76
91
|
|
77
|
-
function useErrorResponseForThisForm(
|
78
|
-
fetcher?: ReturnType<typeof useFetcher>,
|
79
|
-
subaction?: string
|
80
|
-
): ValidationErrorResponseData | null {
|
81
|
-
const actionData = useActionData<any>();
|
82
|
-
if (fetcher) {
|
83
|
-
if ((fetcher.data as any)?.fieldErrors) return fetcher.data as any;
|
84
|
-
return null;
|
85
|
-
}
|
86
|
-
|
87
|
-
if (!actionData?.fieldErrors) return null;
|
88
|
-
if (
|
89
|
-
(!subaction && !actionData.subaction) ||
|
90
|
-
actionData.subaction === subaction
|
91
|
-
)
|
92
|
-
return actionData;
|
93
|
-
return null;
|
94
|
-
}
|
95
|
-
|
96
|
-
function useFieldErrors(
|
97
|
-
fieldErrorsFromBackend?: FieldErrors
|
98
|
-
): [FieldErrors, React.Dispatch<React.SetStateAction<FieldErrors>>] {
|
99
|
-
const [fieldErrors, setFieldErrors] = useState<FieldErrors>(
|
100
|
-
fieldErrorsFromBackend ?? {}
|
101
|
-
);
|
102
|
-
useEffect(() => {
|
103
|
-
if (fieldErrorsFromBackend) setFieldErrors(fieldErrorsFromBackend);
|
104
|
-
}, [fieldErrorsFromBackend]);
|
105
|
-
|
106
|
-
return [fieldErrors, setFieldErrors];
|
107
|
-
}
|
108
|
-
|
109
|
-
const useIsSubmitting = (
|
110
|
-
fetcher?: Fetcher
|
111
|
-
): [boolean, () => void, () => void] => {
|
112
|
-
const [isSubmitStarted, setSubmitStarted] = useState(false);
|
113
|
-
const transition = useTransition();
|
114
|
-
const hasActiveSubmission = fetcher
|
115
|
-
? fetcher.state === "submitting"
|
116
|
-
: !!transition.submission;
|
117
|
-
const isSubmitting = hasActiveSubmission && isSubmitStarted;
|
118
|
-
|
119
|
-
const startSubmit = () => setSubmitStarted(true);
|
120
|
-
const endSubmit = () => setSubmitStarted(false);
|
121
|
-
|
122
|
-
return [isSubmitting, startSubmit, endSubmit];
|
123
|
-
};
|
124
|
-
|
125
92
|
const getDataFromForm = (el: HTMLFormElement) => new FormData(el);
|
126
93
|
|
127
|
-
/**
|
128
|
-
* The purpose for this logic is to handle validation errors when javascript is disabled.
|
129
|
-
* Normally (without js), when a form is submitted and the action returns the validation errors,
|
130
|
-
* the form will be reset. The errors will be displayed on the correct fields,
|
131
|
-
* but all the values in the form will be gone. This is not good UX.
|
132
|
-
*
|
133
|
-
* To get around this, we return the submitted form data from the server,
|
134
|
-
* and use those to populate the form via `defaultValues`.
|
135
|
-
* This results in a more seamless UX akin to what you would see when js is enabled.
|
136
|
-
*
|
137
|
-
* One potential downside is that resetting the form will reset the form
|
138
|
-
* to the _new_ default values that were returned from the server with the validation errors.
|
139
|
-
* However, this case is less of a problem than the janky UX caused by losing the form values.
|
140
|
-
* It will only ever be a problem if the form includes a `<button type="reset" />`
|
141
|
-
* and only if JS is disabled.
|
142
|
-
*/
|
143
|
-
function useDefaultValues<DataType>(
|
144
|
-
repopulateFieldsFromBackend?: any,
|
145
|
-
defaultValues?: Partial<DataType>
|
146
|
-
) {
|
147
|
-
return repopulateFieldsFromBackend ?? defaultValues;
|
148
|
-
}
|
149
|
-
|
150
94
|
function nonNull<T>(value: T | null | undefined): value is T {
|
151
95
|
return value !== null;
|
152
96
|
}
|
@@ -201,6 +145,58 @@ const focusFirstInvalidInput = (
|
|
201
145
|
}
|
202
146
|
};
|
203
147
|
|
148
|
+
const useFormId = (providedId?: string): string | symbol => {
|
149
|
+
// We can use a `Symbol` here because we only use it after hydration
|
150
|
+
const [symbolId] = useState(() => Symbol("remix-validated-form-id"));
|
151
|
+
return providedId ?? symbolId;
|
152
|
+
};
|
153
|
+
|
154
|
+
/**
|
155
|
+
* Use a component to access the state so we don't cause
|
156
|
+
* any extra rerenders of the whole form.
|
157
|
+
*/
|
158
|
+
const FormResetter = ({
|
159
|
+
resetAfterSubmit,
|
160
|
+
formRef,
|
161
|
+
}: {
|
162
|
+
resetAfterSubmit: boolean;
|
163
|
+
formRef: RefObject<HTMLFormElement>;
|
164
|
+
}) => {
|
165
|
+
const isSubmitting = useIsSubmitting();
|
166
|
+
const isValid = useIsValid();
|
167
|
+
useSubmitComplete(isSubmitting, () => {
|
168
|
+
if (isValid && resetAfterSubmit) {
|
169
|
+
formRef.current?.reset();
|
170
|
+
}
|
171
|
+
});
|
172
|
+
return null;
|
173
|
+
};
|
174
|
+
|
175
|
+
function formEventProxy<T extends object>(event: T): T {
|
176
|
+
let defaultPrevented = false;
|
177
|
+
return new Proxy(event, {
|
178
|
+
get: (target, prop) => {
|
179
|
+
if (prop === "preventDefault") {
|
180
|
+
return () => {
|
181
|
+
defaultPrevented = true;
|
182
|
+
};
|
183
|
+
}
|
184
|
+
|
185
|
+
if (prop === "defaultPrevented") {
|
186
|
+
return defaultPrevented;
|
187
|
+
}
|
188
|
+
|
189
|
+
return target[prop as keyof T];
|
190
|
+
},
|
191
|
+
}) as T;
|
192
|
+
}
|
193
|
+
|
194
|
+
const useFormAtom = (formId: string | symbol) => {
|
195
|
+
const formAtom = formRegistry(formId);
|
196
|
+
useEffect(() => () => formRegistry.remove(formId), [formId]);
|
197
|
+
return formAtom;
|
198
|
+
};
|
199
|
+
|
204
200
|
/**
|
205
201
|
* The primary form component of `remix-validated-form`.
|
206
202
|
*/
|
@@ -210,102 +206,104 @@ export function ValidatedForm<DataType>({
|
|
210
206
|
children,
|
211
207
|
fetcher,
|
212
208
|
action,
|
213
|
-
defaultValues,
|
209
|
+
defaultValues: providedDefaultValues,
|
214
210
|
formRef: formRefProp,
|
215
211
|
onReset,
|
216
212
|
subaction,
|
217
|
-
resetAfterSubmit,
|
213
|
+
resetAfterSubmit = false,
|
218
214
|
disableFocusOnError,
|
219
215
|
method,
|
220
216
|
replace,
|
217
|
+
id,
|
221
218
|
...rest
|
222
219
|
}: FormProps<DataType>) {
|
223
|
-
const
|
224
|
-
const
|
225
|
-
|
220
|
+
const formId = useFormId(id);
|
221
|
+
const formAtom = useFormAtom(formId);
|
222
|
+
const contextValue = useMemo<InternalFormContextValue>(
|
223
|
+
() => ({
|
224
|
+
formId,
|
225
|
+
action,
|
226
|
+
subaction,
|
227
|
+
defaultValuesProp: providedDefaultValues,
|
228
|
+
fetcher,
|
229
|
+
}),
|
230
|
+
[action, fetcher, formId, providedDefaultValues, subaction]
|
226
231
|
);
|
227
|
-
const
|
232
|
+
const backendError = useErrorResponseForForm(contextValue);
|
233
|
+
const backendDefaultValues = useDefaultValuesFromLoader(contextValue);
|
234
|
+
const hasActiveSubmission = useHasActiveFormSubmit(contextValue);
|
235
|
+
const formRef = useRef<HTMLFormElement>(null);
|
236
|
+
const Form = fetcher?.Form ?? RemixForm;
|
228
237
|
|
229
|
-
const defaultsToUse = useDefaultValues(
|
230
|
-
backendError?.repopulateFields,
|
231
|
-
defaultValues
|
232
|
-
);
|
233
|
-
const [touchedFields, setTouchedFields] = useState<TouchedFields>({});
|
234
|
-
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
235
238
|
const submit = useSubmit();
|
236
|
-
const
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
239
|
+
const clearError = useFormUpdateAtom(clearErrorAtom);
|
240
|
+
const addError = useFormUpdateAtom(addErrorAtom);
|
241
|
+
const setFieldErrors = useFormUpdateAtom(setFieldErrorsAtom);
|
242
|
+
const reset = useFormUpdateAtom(resetAtom);
|
243
|
+
const startSubmit = useFormUpdateAtom(startSubmitAtom);
|
244
|
+
const endSubmit = useFormUpdateAtom(endSubmitAtom);
|
245
|
+
const syncFormContext = useFormUpdateAtom(syncFormContextAtom);
|
246
|
+
|
247
|
+
const validateField: FormState["validateField"] = useCallback(
|
248
|
+
async (fieldName) => {
|
249
|
+
invariant(formRef.current, "Cannot find reference to form");
|
250
|
+
const { error } = await validator.validateField(
|
251
|
+
getDataFromForm(formRef.current),
|
252
|
+
fieldName as any
|
253
|
+
);
|
254
|
+
|
255
|
+
if (error) {
|
256
|
+
addError({ formAtom, name: fieldName, error });
|
257
|
+
return error;
|
258
|
+
} else {
|
259
|
+
clearError({ name: fieldName, formAtom });
|
260
|
+
return null;
|
261
|
+
}
|
262
|
+
},
|
263
|
+
[addError, clearError, formAtom, validator]
|
264
|
+
);
|
265
|
+
|
243
266
|
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
267
|
+
const registerReceiveFocus: FormState["registerReceiveFocus"] = useCallback(
|
268
|
+
(fieldName, handler) => {
|
269
|
+
customFocusHandlers().add(fieldName, handler);
|
270
|
+
return () => {
|
271
|
+
customFocusHandlers().remove(fieldName, handler);
|
272
|
+
};
|
273
|
+
},
|
274
|
+
[customFocusHandlers]
|
275
|
+
);
|
244
276
|
|
245
|
-
|
246
|
-
(
|
247
|
-
|
277
|
+
useLayoutEffect(() => {
|
278
|
+
syncFormContext({
|
279
|
+
formAtom,
|
248
280
|
action,
|
249
|
-
defaultValues:
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
getDataFromForm(formRef.current),
|
265
|
-
fieldName as any
|
266
|
-
);
|
281
|
+
defaultValues: providedDefaultValues ?? backendDefaultValues,
|
282
|
+
subaction,
|
283
|
+
validateField,
|
284
|
+
registerReceiveFocus,
|
285
|
+
});
|
286
|
+
}, [
|
287
|
+
action,
|
288
|
+
formAtom,
|
289
|
+
providedDefaultValues,
|
290
|
+
registerReceiveFocus,
|
291
|
+
subaction,
|
292
|
+
syncFormContext,
|
293
|
+
validateField,
|
294
|
+
backendDefaultValues,
|
295
|
+
]);
|
267
296
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
...prev,
|
275
|
-
[fieldName]: error,
|
276
|
-
};
|
277
|
-
});
|
278
|
-
return error;
|
279
|
-
} else {
|
280
|
-
setFieldErrors((prev) => {
|
281
|
-
if (!(fieldName in prev)) return prev;
|
282
|
-
return omit(prev, fieldName);
|
283
|
-
});
|
284
|
-
return null;
|
285
|
-
}
|
286
|
-
},
|
287
|
-
registerReceiveFocus: (fieldName, handler) => {
|
288
|
-
customFocusHandlers().add(fieldName, handler);
|
289
|
-
return () => {
|
290
|
-
customFocusHandlers().remove(fieldName, handler);
|
291
|
-
};
|
292
|
-
},
|
293
|
-
hasBeenSubmitted,
|
294
|
-
}),
|
295
|
-
[
|
296
|
-
fieldErrors,
|
297
|
-
action,
|
298
|
-
defaultsToUse,
|
299
|
-
isSubmitting,
|
300
|
-
touchedFields,
|
301
|
-
hasBeenSubmitted,
|
302
|
-
setFieldErrors,
|
303
|
-
validator,
|
304
|
-
customFocusHandlers,
|
305
|
-
]
|
306
|
-
);
|
297
|
+
useEffect(() => {
|
298
|
+
setFieldErrors({
|
299
|
+
fieldErrors: backendError?.fieldErrors ?? {},
|
300
|
+
formAtom,
|
301
|
+
});
|
302
|
+
}, [backendError?.fieldErrors, formAtom, setFieldErrors]);
|
307
303
|
|
308
|
-
|
304
|
+
useSubmitComplete(hasActiveSubmission, () => {
|
305
|
+
endSubmit({ formAtom });
|
306
|
+
});
|
309
307
|
|
310
308
|
let clickedButtonRef = React.useRef<any>();
|
311
309
|
useEffect(() => {
|
@@ -333,56 +331,63 @@ export function ValidatedForm<DataType>({
|
|
333
331
|
};
|
334
332
|
}, []);
|
335
333
|
|
334
|
+
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
335
|
+
startSubmit({ formAtom });
|
336
|
+
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
337
|
+
if (result.error) {
|
338
|
+
endSubmit({ formAtom });
|
339
|
+
setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
|
340
|
+
if (!disableFocusOnError) {
|
341
|
+
focusFirstInvalidInput(
|
342
|
+
result.error.fieldErrors,
|
343
|
+
customFocusHandlers(),
|
344
|
+
formRef.current!
|
345
|
+
);
|
346
|
+
}
|
347
|
+
} else {
|
348
|
+
const eventProxy = formEventProxy(e);
|
349
|
+
await onSubmit?.(result.data, eventProxy);
|
350
|
+
if (eventProxy.defaultPrevented) {
|
351
|
+
endSubmit({ formAtom });
|
352
|
+
return;
|
353
|
+
}
|
354
|
+
|
355
|
+
if (fetcher) fetcher.submit(clickedButtonRef.current || e.currentTarget);
|
356
|
+
else
|
357
|
+
submit(clickedButtonRef.current || e.currentTarget, {
|
358
|
+
method,
|
359
|
+
replace,
|
360
|
+
});
|
361
|
+
clickedButtonRef.current = null;
|
362
|
+
}
|
363
|
+
};
|
364
|
+
|
336
365
|
return (
|
337
366
|
<Form
|
338
367
|
ref={mergeRefs([formRef, formRefProp])}
|
339
368
|
{...rest}
|
369
|
+
id={id}
|
340
370
|
action={action}
|
341
371
|
method={method}
|
342
372
|
replace={replace}
|
343
|
-
onSubmit={
|
373
|
+
onSubmit={(e) => {
|
344
374
|
e.preventDefault();
|
345
|
-
|
346
|
-
startSubmit();
|
347
|
-
const result = await validator.validate(
|
348
|
-
getDataFromForm(e.currentTarget)
|
349
|
-
);
|
350
|
-
if (result.error) {
|
351
|
-
endSubmit();
|
352
|
-
setFieldErrors(result.error.fieldErrors);
|
353
|
-
if (!disableFocusOnError) {
|
354
|
-
focusFirstInvalidInput(
|
355
|
-
result.error.fieldErrors,
|
356
|
-
customFocusHandlers(),
|
357
|
-
formRef.current!
|
358
|
-
);
|
359
|
-
}
|
360
|
-
} else {
|
361
|
-
onSubmit && onSubmit(result.data, e);
|
362
|
-
if (fetcher)
|
363
|
-
fetcher.submit(clickedButtonRef.current || e.currentTarget);
|
364
|
-
else
|
365
|
-
submit(clickedButtonRef.current || e.currentTarget, {
|
366
|
-
method,
|
367
|
-
replace,
|
368
|
-
});
|
369
|
-
clickedButtonRef.current = null;
|
370
|
-
}
|
375
|
+
handleSubmit(e);
|
371
376
|
}}
|
372
377
|
onReset={(event) => {
|
373
378
|
onReset?.(event);
|
374
379
|
if (event.defaultPrevented) return;
|
375
|
-
|
376
|
-
setTouchedFields({});
|
377
|
-
setHasBeenSubmitted(false);
|
380
|
+
reset({ formAtom });
|
378
381
|
}}
|
379
382
|
>
|
380
|
-
<
|
383
|
+
<InternalFormContext.Provider value={contextValue}>
|
384
|
+
<FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
|
381
385
|
{subaction && (
|
382
386
|
<input type="hidden" value={subaction} name="subaction" />
|
383
387
|
)}
|
388
|
+
{id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
|
384
389
|
{children}
|
385
|
-
</
|
390
|
+
</InternalFormContext.Provider>
|
386
391
|
</Form>
|
387
392
|
);
|
388
393
|
}
|