remix-validated-form 4.0.1-beta.2 → 4.1.0-beta.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.
- package/.turbo/turbo-build.log +2 -2
- package/README.md +4 -4
- package/browser/ValidatedForm.d.ts +2 -2
- package/browser/ValidatedForm.js +137 -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 +133 -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 +199 -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.2",
|
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,52 @@ 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
|
+
|
204
194
|
/**
|
205
195
|
* The primary form component of `remix-validated-form`.
|
206
196
|
*/
|
@@ -210,102 +200,104 @@ export function ValidatedForm<DataType>({
|
|
210
200
|
children,
|
211
201
|
fetcher,
|
212
202
|
action,
|
213
|
-
defaultValues,
|
203
|
+
defaultValues: providedDefaultValues,
|
214
204
|
formRef: formRefProp,
|
215
205
|
onReset,
|
216
206
|
subaction,
|
217
|
-
resetAfterSubmit,
|
207
|
+
resetAfterSubmit = false,
|
218
208
|
disableFocusOnError,
|
219
209
|
method,
|
220
210
|
replace,
|
211
|
+
id,
|
221
212
|
...rest
|
222
213
|
}: FormProps<DataType>) {
|
223
|
-
const
|
224
|
-
const
|
225
|
-
|
214
|
+
const formId = useFormId(id);
|
215
|
+
const formAtom = formRegistry(formId);
|
216
|
+
const contextValue = useMemo<InternalFormContextValue>(
|
217
|
+
() => ({
|
218
|
+
formId,
|
219
|
+
action,
|
220
|
+
subaction,
|
221
|
+
defaultValuesProp: providedDefaultValues,
|
222
|
+
fetcher,
|
223
|
+
}),
|
224
|
+
[action, fetcher, formId, providedDefaultValues, subaction]
|
226
225
|
);
|
227
|
-
const
|
226
|
+
const backendError = useErrorResponseForForm(contextValue);
|
227
|
+
const backendDefaultValues = useDefaultValuesFromLoader(contextValue);
|
228
|
+
const hasActiveSubmission = useHasActiveFormSubmit(contextValue);
|
229
|
+
const formRef = useRef<HTMLFormElement>(null);
|
230
|
+
const Form = fetcher?.Form ?? RemixForm;
|
228
231
|
|
229
|
-
const defaultsToUse = useDefaultValues(
|
230
|
-
backendError?.repopulateFields,
|
231
|
-
defaultValues
|
232
|
-
);
|
233
|
-
const [touchedFields, setTouchedFields] = useState<TouchedFields>({});
|
234
|
-
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
235
232
|
const submit = useSubmit();
|
236
|
-
const
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
233
|
+
const clearError = useFormUpdateAtom(clearErrorAtom);
|
234
|
+
const addError = useFormUpdateAtom(addErrorAtom);
|
235
|
+
const setFieldErrors = useFormUpdateAtom(setFieldErrorsAtom);
|
236
|
+
const reset = useFormUpdateAtom(resetAtom);
|
237
|
+
const startSubmit = useFormUpdateAtom(startSubmitAtom);
|
238
|
+
const endSubmit = useFormUpdateAtom(endSubmitAtom);
|
239
|
+
const syncFormContext = useFormUpdateAtom(syncFormContextAtom);
|
240
|
+
|
241
|
+
const validateField: FormState["validateField"] = useCallback(
|
242
|
+
async (fieldName) => {
|
243
|
+
invariant(formRef.current, "Cannot find reference to form");
|
244
|
+
const { error } = await validator.validateField(
|
245
|
+
getDataFromForm(formRef.current),
|
246
|
+
fieldName as any
|
247
|
+
);
|
248
|
+
|
249
|
+
if (error) {
|
250
|
+
addError({ formAtom, name: fieldName, error });
|
251
|
+
return error;
|
252
|
+
} else {
|
253
|
+
clearError({ name: fieldName, formAtom });
|
254
|
+
return null;
|
255
|
+
}
|
256
|
+
},
|
257
|
+
[addError, clearError, formAtom, validator]
|
258
|
+
);
|
259
|
+
|
243
260
|
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
261
|
+
const registerReceiveFocus: FormState["registerReceiveFocus"] = useCallback(
|
262
|
+
(fieldName, handler) => {
|
263
|
+
customFocusHandlers().add(fieldName, handler);
|
264
|
+
return () => {
|
265
|
+
customFocusHandlers().remove(fieldName, handler);
|
266
|
+
};
|
267
|
+
},
|
268
|
+
[customFocusHandlers]
|
269
|
+
);
|
244
270
|
|
245
|
-
|
246
|
-
(
|
247
|
-
|
271
|
+
useLayoutEffect(() => {
|
272
|
+
syncFormContext({
|
273
|
+
formAtom,
|
248
274
|
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
|
-
);
|
275
|
+
defaultValues: providedDefaultValues ?? backendDefaultValues,
|
276
|
+
subaction,
|
277
|
+
validateField,
|
278
|
+
registerReceiveFocus,
|
279
|
+
});
|
280
|
+
}, [
|
281
|
+
action,
|
282
|
+
formAtom,
|
283
|
+
providedDefaultValues,
|
284
|
+
registerReceiveFocus,
|
285
|
+
subaction,
|
286
|
+
syncFormContext,
|
287
|
+
validateField,
|
288
|
+
backendDefaultValues,
|
289
|
+
]);
|
267
290
|
|
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
|
-
);
|
291
|
+
useEffect(() => {
|
292
|
+
setFieldErrors({
|
293
|
+
fieldErrors: backendError?.fieldErrors ?? {},
|
294
|
+
formAtom,
|
295
|
+
});
|
296
|
+
}, [backendError?.fieldErrors, formAtom, setFieldErrors]);
|
307
297
|
|
308
|
-
|
298
|
+
useSubmitComplete(hasActiveSubmission, () => {
|
299
|
+
endSubmit({ formAtom });
|
300
|
+
});
|
309
301
|
|
310
302
|
let clickedButtonRef = React.useRef<any>();
|
311
303
|
useEffect(() => {
|
@@ -333,56 +325,63 @@ export function ValidatedForm<DataType>({
|
|
333
325
|
};
|
334
326
|
}, []);
|
335
327
|
|
328
|
+
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
329
|
+
startSubmit({ formAtom });
|
330
|
+
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
331
|
+
if (result.error) {
|
332
|
+
endSubmit({ formAtom });
|
333
|
+
setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
|
334
|
+
if (!disableFocusOnError) {
|
335
|
+
focusFirstInvalidInput(
|
336
|
+
result.error.fieldErrors,
|
337
|
+
customFocusHandlers(),
|
338
|
+
formRef.current!
|
339
|
+
);
|
340
|
+
}
|
341
|
+
} else {
|
342
|
+
const eventProxy = formEventProxy(e);
|
343
|
+
await onSubmit?.(result.data, eventProxy);
|
344
|
+
if (eventProxy.defaultPrevented) {
|
345
|
+
endSubmit({ formAtom });
|
346
|
+
return;
|
347
|
+
}
|
348
|
+
|
349
|
+
if (fetcher) fetcher.submit(clickedButtonRef.current || e.currentTarget);
|
350
|
+
else
|
351
|
+
submit(clickedButtonRef.current || e.currentTarget, {
|
352
|
+
method,
|
353
|
+
replace,
|
354
|
+
});
|
355
|
+
clickedButtonRef.current = null;
|
356
|
+
}
|
357
|
+
};
|
358
|
+
|
336
359
|
return (
|
337
360
|
<Form
|
338
361
|
ref={mergeRefs([formRef, formRefProp])}
|
339
362
|
{...rest}
|
363
|
+
id={id}
|
340
364
|
action={action}
|
341
365
|
method={method}
|
342
366
|
replace={replace}
|
343
|
-
onSubmit={
|
367
|
+
onSubmit={(e) => {
|
344
368
|
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
|
-
}
|
369
|
+
handleSubmit(e);
|
371
370
|
}}
|
372
371
|
onReset={(event) => {
|
373
372
|
onReset?.(event);
|
374
373
|
if (event.defaultPrevented) return;
|
375
|
-
|
376
|
-
setTouchedFields({});
|
377
|
-
setHasBeenSubmitted(false);
|
374
|
+
reset({ formAtom });
|
378
375
|
}}
|
379
376
|
>
|
380
|
-
<
|
377
|
+
<InternalFormContext.Provider value={contextValue}>
|
378
|
+
<FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
|
381
379
|
{subaction && (
|
382
380
|
<input type="hidden" value={subaction} name="subaction" />
|
383
381
|
)}
|
382
|
+
{id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
|
384
383
|
{children}
|
385
|
-
</
|
384
|
+
</InternalFormContext.Provider>
|
386
385
|
</Form>
|
387
386
|
);
|
388
387
|
}
|