remix-validated-form 4.0.2 → 4.1.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 +2 -2
- package/README.md +4 -4
- package/browser/ValidatedForm.d.ts +2 -2
- package/browser/ValidatedForm.js +142 -151
- 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 -147
- 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 -203
- 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",
|
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,82 +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
|
-
|
118
|
-
const startSubmit = () => setSubmitStarted(true);
|
119
|
-
const endSubmit = () => setSubmitStarted(false);
|
120
|
-
|
121
|
-
useSubmitComplete(hasActiveSubmission, () => {
|
122
|
-
endSubmit();
|
123
|
-
});
|
124
|
-
|
125
|
-
return [isSubmitStarted, startSubmit, endSubmit];
|
126
|
-
};
|
127
|
-
|
128
92
|
const getDataFromForm = (el: HTMLFormElement) => new FormData(el);
|
129
93
|
|
130
|
-
/**
|
131
|
-
* The purpose for this logic is to handle validation errors when javascript is disabled.
|
132
|
-
* Normally (without js), when a form is submitted and the action returns the validation errors,
|
133
|
-
* the form will be reset. The errors will be displayed on the correct fields,
|
134
|
-
* but all the values in the form will be gone. This is not good UX.
|
135
|
-
*
|
136
|
-
* To get around this, we return the submitted form data from the server,
|
137
|
-
* and use those to populate the form via `defaultValues`.
|
138
|
-
* This results in a more seamless UX akin to what you would see when js is enabled.
|
139
|
-
*
|
140
|
-
* One potential downside is that resetting the form will reset the form
|
141
|
-
* to the _new_ default values that were returned from the server with the validation errors.
|
142
|
-
* However, this case is less of a problem than the janky UX caused by losing the form values.
|
143
|
-
* It will only ever be a problem if the form includes a `<button type="reset" />`
|
144
|
-
* and only if JS is disabled.
|
145
|
-
*/
|
146
|
-
function useDefaultValues<DataType>(
|
147
|
-
repopulateFieldsFromBackend?: any,
|
148
|
-
defaultValues?: Partial<DataType>
|
149
|
-
) {
|
150
|
-
return repopulateFieldsFromBackend ?? defaultValues;
|
151
|
-
}
|
152
|
-
|
153
94
|
function nonNull<T>(value: T | null | undefined): value is T {
|
154
95
|
return value !== null;
|
155
96
|
}
|
@@ -204,6 +145,58 @@ const focusFirstInvalidInput = (
|
|
204
145
|
}
|
205
146
|
};
|
206
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
|
+
|
207
200
|
/**
|
208
201
|
* The primary form component of `remix-validated-form`.
|
209
202
|
*/
|
@@ -213,102 +206,104 @@ export function ValidatedForm<DataType>({
|
|
213
206
|
children,
|
214
207
|
fetcher,
|
215
208
|
action,
|
216
|
-
defaultValues,
|
209
|
+
defaultValues: providedDefaultValues,
|
217
210
|
formRef: formRefProp,
|
218
211
|
onReset,
|
219
212
|
subaction,
|
220
|
-
resetAfterSubmit,
|
213
|
+
resetAfterSubmit = false,
|
221
214
|
disableFocusOnError,
|
222
215
|
method,
|
223
216
|
replace,
|
217
|
+
id,
|
224
218
|
...rest
|
225
219
|
}: FormProps<DataType>) {
|
226
|
-
const
|
227
|
-
const
|
228
|
-
|
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]
|
229
231
|
);
|
230
|
-
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;
|
231
237
|
|
232
|
-
const defaultsToUse = useDefaultValues(
|
233
|
-
backendError?.repopulateFields,
|
234
|
-
defaultValues
|
235
|
-
);
|
236
|
-
const [touchedFields, setTouchedFields] = useState<TouchedFields>({});
|
237
|
-
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
238
238
|
const submit = useSubmit();
|
239
|
-
const
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
+
|
246
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
|
+
);
|
247
276
|
|
248
|
-
|
249
|
-
(
|
250
|
-
|
277
|
+
useLayoutEffect(() => {
|
278
|
+
syncFormContext({
|
279
|
+
formAtom,
|
251
280
|
action,
|
252
|
-
defaultValues:
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
getDataFromForm(formRef.current),
|
268
|
-
fieldName as any
|
269
|
-
);
|
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
|
+
]);
|
270
296
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
...prev,
|
278
|
-
[fieldName]: error,
|
279
|
-
};
|
280
|
-
});
|
281
|
-
return error;
|
282
|
-
} else {
|
283
|
-
setFieldErrors((prev) => {
|
284
|
-
if (!(fieldName in prev)) return prev;
|
285
|
-
return omit(prev, fieldName);
|
286
|
-
});
|
287
|
-
return null;
|
288
|
-
}
|
289
|
-
},
|
290
|
-
registerReceiveFocus: (fieldName, handler) => {
|
291
|
-
customFocusHandlers().add(fieldName, handler);
|
292
|
-
return () => {
|
293
|
-
customFocusHandlers().remove(fieldName, handler);
|
294
|
-
};
|
295
|
-
},
|
296
|
-
hasBeenSubmitted,
|
297
|
-
}),
|
298
|
-
[
|
299
|
-
fieldErrors,
|
300
|
-
action,
|
301
|
-
defaultsToUse,
|
302
|
-
isSubmitting,
|
303
|
-
touchedFields,
|
304
|
-
hasBeenSubmitted,
|
305
|
-
setFieldErrors,
|
306
|
-
validator,
|
307
|
-
customFocusHandlers,
|
308
|
-
]
|
309
|
-
);
|
297
|
+
useEffect(() => {
|
298
|
+
setFieldErrors({
|
299
|
+
fieldErrors: backendError?.fieldErrors ?? {},
|
300
|
+
formAtom,
|
301
|
+
});
|
302
|
+
}, [backendError?.fieldErrors, formAtom, setFieldErrors]);
|
310
303
|
|
311
|
-
|
304
|
+
useSubmitComplete(hasActiveSubmission, () => {
|
305
|
+
endSubmit({ formAtom });
|
306
|
+
});
|
312
307
|
|
313
308
|
let clickedButtonRef = React.useRef<any>();
|
314
309
|
useEffect(() => {
|
@@ -336,56 +331,63 @@ export function ValidatedForm<DataType>({
|
|
336
331
|
};
|
337
332
|
}, []);
|
338
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
|
+
|
339
365
|
return (
|
340
366
|
<Form
|
341
367
|
ref={mergeRefs([formRef, formRefProp])}
|
342
368
|
{...rest}
|
369
|
+
id={id}
|
343
370
|
action={action}
|
344
371
|
method={method}
|
345
372
|
replace={replace}
|
346
|
-
onSubmit={
|
373
|
+
onSubmit={(e) => {
|
347
374
|
e.preventDefault();
|
348
|
-
|
349
|
-
startSubmit();
|
350
|
-
const result = await validator.validate(
|
351
|
-
getDataFromForm(e.currentTarget)
|
352
|
-
);
|
353
|
-
if (result.error) {
|
354
|
-
endSubmit();
|
355
|
-
setFieldErrors(result.error.fieldErrors);
|
356
|
-
if (!disableFocusOnError) {
|
357
|
-
focusFirstInvalidInput(
|
358
|
-
result.error.fieldErrors,
|
359
|
-
customFocusHandlers(),
|
360
|
-
formRef.current!
|
361
|
-
);
|
362
|
-
}
|
363
|
-
} else {
|
364
|
-
onSubmit && onSubmit(result.data, e);
|
365
|
-
if (fetcher)
|
366
|
-
fetcher.submit(clickedButtonRef.current || e.currentTarget);
|
367
|
-
else
|
368
|
-
submit(clickedButtonRef.current || e.currentTarget, {
|
369
|
-
method,
|
370
|
-
replace,
|
371
|
-
});
|
372
|
-
clickedButtonRef.current = null;
|
373
|
-
}
|
375
|
+
handleSubmit(e);
|
374
376
|
}}
|
375
377
|
onReset={(event) => {
|
376
378
|
onReset?.(event);
|
377
379
|
if (event.defaultPrevented) return;
|
378
|
-
|
379
|
-
setTouchedFields({});
|
380
|
-
setHasBeenSubmitted(false);
|
380
|
+
reset({ formAtom });
|
381
381
|
}}
|
382
382
|
>
|
383
|
-
<
|
383
|
+
<InternalFormContext.Provider value={contextValue}>
|
384
|
+
<FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
|
384
385
|
{subaction && (
|
385
386
|
<input type="hidden" value={subaction} name="subaction" />
|
386
387
|
)}
|
388
|
+
{id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
|
387
389
|
{children}
|
388
|
-
</
|
390
|
+
</InternalFormContext.Provider>
|
389
391
|
</Form>
|
390
392
|
);
|
391
393
|
}
|