remix-validated-form 4.0.1-beta.1 → 4.1.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 +2 -2
- package/README.md +4 -4
- package/browser/ValidatedForm.d.ts +1 -1
- package/browser/ValidatedForm.js +99 -140
- package/browser/components.d.ts +5 -8
- package/browser/components.js +5 -5
- package/browser/hooks.d.ts +19 -14
- package/browser/hooks.js +36 -40
- 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 +23 -0
- package/browser/internal/hooks.js +114 -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 +1 -1
- package/build/ValidatedForm.js +95 -136
- package/build/hooks.d.ts +19 -14
- package/build/hooks.js +38 -46
- 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 +23 -0
- package/build/internal/hooks.js +135 -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 +150 -181
- package/src/hooks.ts +69 -55
- 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 +200 -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.1",
|
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,30 +1,45 @@
|
|
1
|
-
import {
|
2
|
-
Form as RemixForm,
|
3
|
-
useActionData,
|
4
|
-
useFetcher,
|
5
|
-
useFormAction,
|
6
|
-
useSubmit,
|
7
|
-
useTransition,
|
8
|
-
} from "@remix-run/react";
|
1
|
+
import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
|
9
2
|
import uniq from "lodash/uniq";
|
10
3
|
import React, {
|
11
4
|
ComponentProps,
|
5
|
+
RefObject,
|
6
|
+
useCallback,
|
12
7
|
useEffect,
|
13
8
|
useMemo,
|
14
9
|
useRef,
|
15
10
|
useState,
|
16
11
|
} from "react";
|
17
12
|
import invariant from "tiny-invariant";
|
18
|
-
import {
|
13
|
+
import { useIsSubmitting, useIsValid } from "./hooks";
|
14
|
+
import { FORM_ID_FIELD } from "./internal/constants";
|
15
|
+
import {
|
16
|
+
InternalFormContext,
|
17
|
+
InternalFormContextValue,
|
18
|
+
} from "./internal/formContext";
|
19
|
+
import {
|
20
|
+
useDefaultValuesFromLoader,
|
21
|
+
useErrorResponseForForm,
|
22
|
+
useFormUpdateAtom,
|
23
|
+
useHasActiveFormSubmit,
|
24
|
+
} from "./internal/hooks";
|
19
25
|
import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
|
26
|
+
import {
|
27
|
+
addErrorAtom,
|
28
|
+
clearErrorAtom,
|
29
|
+
endSubmitAtom,
|
30
|
+
formRegistry,
|
31
|
+
FormState,
|
32
|
+
resetAtom,
|
33
|
+
setFieldErrorsAtom,
|
34
|
+
startSubmitAtom,
|
35
|
+
syncFormContextAtom,
|
36
|
+
} from "./internal/state";
|
20
37
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
21
|
-
import { omit, mergeRefs } from "./internal/util";
|
22
38
|
import {
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
} from "./validation/types";
|
39
|
+
mergeRefs,
|
40
|
+
useIsomorphicLayoutEffect as useLayoutEffect,
|
41
|
+
} from "./internal/util";
|
42
|
+
import { FieldErrors, Validator } from "./validation/types";
|
28
43
|
|
29
44
|
export type FormProps<DataType> = {
|
30
45
|
/**
|
@@ -73,82 +88,8 @@ export type FormProps<DataType> = {
|
|
73
88
|
disableFocusOnError?: boolean;
|
74
89
|
} & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
|
75
90
|
|
76
|
-
function useErrorResponseForThisForm(
|
77
|
-
fetcher?: ReturnType<typeof useFetcher>,
|
78
|
-
subaction?: string
|
79
|
-
): ValidationErrorResponseData | null {
|
80
|
-
const actionData = useActionData<any>();
|
81
|
-
if (fetcher) {
|
82
|
-
if ((fetcher.data as any)?.fieldErrors) return fetcher.data as any;
|
83
|
-
return null;
|
84
|
-
}
|
85
|
-
|
86
|
-
if (!actionData?.fieldErrors) return null;
|
87
|
-
if (
|
88
|
-
(!subaction && !actionData.subaction) ||
|
89
|
-
actionData.subaction === subaction
|
90
|
-
)
|
91
|
-
return actionData;
|
92
|
-
return null;
|
93
|
-
}
|
94
|
-
|
95
|
-
function useFieldErrors(
|
96
|
-
fieldErrorsFromBackend?: FieldErrors
|
97
|
-
): [FieldErrors, React.Dispatch<React.SetStateAction<FieldErrors>>] {
|
98
|
-
const [fieldErrors, setFieldErrors] = useState<FieldErrors>(
|
99
|
-
fieldErrorsFromBackend ?? {}
|
100
|
-
);
|
101
|
-
useEffect(() => {
|
102
|
-
if (fieldErrorsFromBackend) setFieldErrors(fieldErrorsFromBackend);
|
103
|
-
}, [fieldErrorsFromBackend]);
|
104
|
-
|
105
|
-
return [fieldErrors, setFieldErrors];
|
106
|
-
}
|
107
|
-
|
108
|
-
const useIsSubmitting = (
|
109
|
-
action?: string,
|
110
|
-
subaction?: string,
|
111
|
-
fetcher?: ReturnType<typeof useFetcher>
|
112
|
-
) => {
|
113
|
-
const actionForCurrentPage = useFormAction();
|
114
|
-
const pendingFormSubmit = useTransition().submission;
|
115
|
-
|
116
|
-
if (fetcher) return fetcher.state === "submitting";
|
117
|
-
if (!pendingFormSubmit) return false;
|
118
|
-
|
119
|
-
const { formData, action: pendingAction } = pendingFormSubmit;
|
120
|
-
const pendingSubAction = formData.get("subaction");
|
121
|
-
const expectedAction = action ?? actionForCurrentPage;
|
122
|
-
if (subaction)
|
123
|
-
return expectedAction === pendingAction && subaction === pendingSubAction;
|
124
|
-
return expectedAction === pendingAction && !pendingSubAction;
|
125
|
-
};
|
126
|
-
|
127
91
|
const getDataFromForm = (el: HTMLFormElement) => new FormData(el);
|
128
92
|
|
129
|
-
/**
|
130
|
-
* The purpose for this logic is to handle validation errors when javascript is disabled.
|
131
|
-
* Normally (without js), when a form is submitted and the action returns the validation errors,
|
132
|
-
* the form will be reset. The errors will be displayed on the correct fields,
|
133
|
-
* but all the values in the form will be gone. This is not good UX.
|
134
|
-
*
|
135
|
-
* To get around this, we return the submitted form data from the server,
|
136
|
-
* and use those to populate the form via `defaultValues`.
|
137
|
-
* This results in a more seamless UX akin to what you would see when js is enabled.
|
138
|
-
*
|
139
|
-
* One potential downside is that resetting the form will reset the form
|
140
|
-
* to the _new_ default values that were returned from the server with the validation errors.
|
141
|
-
* However, this case is less of a problem than the janky UX caused by losing the form values.
|
142
|
-
* It will only ever be a problem if the form includes a `<button type="reset" />`
|
143
|
-
* and only if JS is disabled.
|
144
|
-
*/
|
145
|
-
function useDefaultValues<DataType>(
|
146
|
-
repopulateFieldsFromBackend?: any,
|
147
|
-
defaultValues?: Partial<DataType>
|
148
|
-
) {
|
149
|
-
return repopulateFieldsFromBackend ?? defaultValues;
|
150
|
-
}
|
151
|
-
|
152
93
|
function nonNull<T>(value: T | null | undefined): value is T {
|
153
94
|
return value !== null;
|
154
95
|
}
|
@@ -203,6 +144,33 @@ const focusFirstInvalidInput = (
|
|
203
144
|
}
|
204
145
|
};
|
205
146
|
|
147
|
+
const useFormId = (providedId?: string): string | symbol => {
|
148
|
+
// We can use a `Symbol` here because we only use it after hydration
|
149
|
+
const [symbolId] = useState(() => Symbol("remix-validated-form-id"));
|
150
|
+
return providedId ?? symbolId;
|
151
|
+
};
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Use a component to access the state so we don't cause
|
155
|
+
* any extra rerenders of the whole form.
|
156
|
+
*/
|
157
|
+
const FormResetter = ({
|
158
|
+
resetAfterSubmit,
|
159
|
+
formRef,
|
160
|
+
}: {
|
161
|
+
resetAfterSubmit: boolean;
|
162
|
+
formRef: RefObject<HTMLFormElement>;
|
163
|
+
}) => {
|
164
|
+
const isSubmitting = useIsSubmitting();
|
165
|
+
const isValid = useIsValid();
|
166
|
+
useSubmitComplete(isSubmitting, () => {
|
167
|
+
if (isValid && resetAfterSubmit) {
|
168
|
+
formRef.current?.reset();
|
169
|
+
}
|
170
|
+
});
|
171
|
+
return null;
|
172
|
+
};
|
173
|
+
|
206
174
|
/**
|
207
175
|
* The primary form component of `remix-validated-form`.
|
208
176
|
*/
|
@@ -212,103 +180,104 @@ export function ValidatedForm<DataType>({
|
|
212
180
|
children,
|
213
181
|
fetcher,
|
214
182
|
action,
|
215
|
-
defaultValues,
|
183
|
+
defaultValues: providedDefaultValues,
|
216
184
|
formRef: formRefProp,
|
217
185
|
onReset,
|
218
186
|
subaction,
|
219
|
-
resetAfterSubmit,
|
187
|
+
resetAfterSubmit = false,
|
220
188
|
disableFocusOnError,
|
221
189
|
method,
|
222
190
|
replace,
|
191
|
+
id,
|
223
192
|
...rest
|
224
193
|
}: FormProps<DataType>) {
|
225
|
-
const
|
226
|
-
const
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
194
|
+
const formId = useFormId(id);
|
195
|
+
const formAtom = formRegistry(formId);
|
196
|
+
const contextValue = useMemo<InternalFormContextValue>(
|
197
|
+
() => ({
|
198
|
+
formId,
|
199
|
+
action,
|
200
|
+
subaction,
|
201
|
+
defaultValuesProp: providedDefaultValues,
|
202
|
+
fetcher,
|
203
|
+
}),
|
204
|
+
[action, fetcher, formId, providedDefaultValues, subaction]
|
234
205
|
);
|
235
|
-
const
|
236
|
-
const
|
237
|
-
const
|
206
|
+
const backendError = useErrorResponseForForm(contextValue);
|
207
|
+
const backendDefaultValues = useDefaultValuesFromLoader(contextValue);
|
208
|
+
const hasActiveSubmission = useHasActiveFormSubmit(contextValue);
|
238
209
|
const formRef = useRef<HTMLFormElement>(null);
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
210
|
+
const Form = fetcher?.Form ?? RemixForm;
|
211
|
+
|
212
|
+
const submit = useSubmit();
|
213
|
+
const clearError = useFormUpdateAtom(clearErrorAtom);
|
214
|
+
const addError = useFormUpdateAtom(addErrorAtom);
|
215
|
+
const setFieldErrors = useFormUpdateAtom(setFieldErrorsAtom);
|
216
|
+
const reset = useFormUpdateAtom(resetAtom);
|
217
|
+
const startSubmit = useFormUpdateAtom(startSubmitAtom);
|
218
|
+
const endSubmit = useFormUpdateAtom(endSubmitAtom);
|
219
|
+
const syncFormContext = useFormUpdateAtom(syncFormContextAtom);
|
220
|
+
|
221
|
+
const validateField: FormState["validateField"] = useCallback(
|
222
|
+
async (fieldName) => {
|
223
|
+
invariant(formRef.current, "Cannot find reference to form");
|
224
|
+
const { error } = await validator.validateField(
|
225
|
+
getDataFromForm(formRef.current),
|
226
|
+
fieldName as any
|
227
|
+
);
|
228
|
+
|
229
|
+
if (error) {
|
230
|
+
addError({ formAtom, name: fieldName, error });
|
231
|
+
return error;
|
232
|
+
} else {
|
233
|
+
clearError({ name: fieldName, formAtom });
|
234
|
+
return null;
|
235
|
+
}
|
236
|
+
},
|
237
|
+
[addError, clearError, formAtom, validator]
|
238
|
+
);
|
239
|
+
|
245
240
|
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
241
|
+
const registerReceiveFocus: FormState["registerReceiveFocus"] = useCallback(
|
242
|
+
(fieldName, handler) => {
|
243
|
+
customFocusHandlers().add(fieldName, handler);
|
244
|
+
return () => {
|
245
|
+
customFocusHandlers().remove(fieldName, handler);
|
246
|
+
};
|
247
|
+
},
|
248
|
+
[customFocusHandlers]
|
249
|
+
);
|
246
250
|
|
247
|
-
|
248
|
-
(
|
249
|
-
|
251
|
+
useLayoutEffect(() => {
|
252
|
+
syncFormContext({
|
253
|
+
formAtom,
|
250
254
|
action,
|
251
|
-
defaultValues:
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
getDataFromForm(formRef.current),
|
267
|
-
fieldName as any
|
268
|
-
);
|
255
|
+
defaultValues: providedDefaultValues ?? backendDefaultValues,
|
256
|
+
subaction,
|
257
|
+
validateField,
|
258
|
+
registerReceiveFocus,
|
259
|
+
});
|
260
|
+
}, [
|
261
|
+
action,
|
262
|
+
formAtom,
|
263
|
+
providedDefaultValues,
|
264
|
+
registerReceiveFocus,
|
265
|
+
subaction,
|
266
|
+
syncFormContext,
|
267
|
+
validateField,
|
268
|
+
backendDefaultValues,
|
269
|
+
]);
|
269
270
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
...prev,
|
277
|
-
[fieldName]: error,
|
278
|
-
};
|
279
|
-
});
|
280
|
-
return error;
|
281
|
-
} else {
|
282
|
-
setFieldErrors((prev) => {
|
283
|
-
if (!(fieldName in prev)) return prev;
|
284
|
-
return omit(prev, fieldName);
|
285
|
-
});
|
286
|
-
return null;
|
287
|
-
}
|
288
|
-
},
|
289
|
-
registerReceiveFocus: (fieldName, handler) => {
|
290
|
-
customFocusHandlers().add(fieldName, handler);
|
291
|
-
return () => {
|
292
|
-
customFocusHandlers().remove(fieldName, handler);
|
293
|
-
};
|
294
|
-
},
|
295
|
-
hasBeenSubmitted,
|
296
|
-
}),
|
297
|
-
[
|
298
|
-
fieldErrors,
|
299
|
-
action,
|
300
|
-
defaultsToUse,
|
301
|
-
isValidating,
|
302
|
-
isSubmitting,
|
303
|
-
touchedFields,
|
304
|
-
hasBeenSubmitted,
|
305
|
-
setFieldErrors,
|
306
|
-
validator,
|
307
|
-
customFocusHandlers,
|
308
|
-
]
|
309
|
-
);
|
271
|
+
useEffect(() => {
|
272
|
+
setFieldErrors({
|
273
|
+
fieldErrors: backendError?.fieldErrors ?? {},
|
274
|
+
formAtom,
|
275
|
+
});
|
276
|
+
}, [backendError?.fieldErrors, formAtom, setFieldErrors]);
|
310
277
|
|
311
|
-
|
278
|
+
useSubmitComplete(hasActiveSubmission, () => {
|
279
|
+
endSubmit({ formAtom });
|
280
|
+
});
|
312
281
|
|
313
282
|
let clickedButtonRef = React.useRef<any>();
|
314
283
|
useEffect(() => {
|
@@ -340,19 +309,19 @@ export function ValidatedForm<DataType>({
|
|
340
309
|
<Form
|
341
310
|
ref={mergeRefs([formRef, formRefProp])}
|
342
311
|
{...rest}
|
312
|
+
id={id}
|
343
313
|
action={action}
|
344
314
|
method={method}
|
345
315
|
replace={replace}
|
346
316
|
onSubmit={async (e) => {
|
347
317
|
e.preventDefault();
|
348
|
-
|
349
|
-
setIsValidating(true);
|
318
|
+
startSubmit({ formAtom });
|
350
319
|
const result = await validator.validate(
|
351
320
|
getDataFromForm(e.currentTarget)
|
352
321
|
);
|
353
322
|
if (result.error) {
|
354
|
-
|
355
|
-
setFieldErrors(result.error.fieldErrors);
|
323
|
+
endSubmit({ formAtom });
|
324
|
+
setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
|
356
325
|
if (!disableFocusOnError) {
|
357
326
|
focusFirstInvalidInput(
|
358
327
|
result.error.fieldErrors,
|
@@ -361,7 +330,7 @@ export function ValidatedForm<DataType>({
|
|
361
330
|
);
|
362
331
|
}
|
363
332
|
} else {
|
364
|
-
onSubmit
|
333
|
+
onSubmit?.(result.data, e);
|
365
334
|
if (fetcher)
|
366
335
|
fetcher.submit(clickedButtonRef.current || e.currentTarget);
|
367
336
|
else
|
@@ -375,17 +344,17 @@ export function ValidatedForm<DataType>({
|
|
375
344
|
onReset={(event) => {
|
376
345
|
onReset?.(event);
|
377
346
|
if (event.defaultPrevented) return;
|
378
|
-
|
379
|
-
setTouchedFields({});
|
380
|
-
setHasBeenSubmitted(false);
|
347
|
+
reset({ formAtom });
|
381
348
|
}}
|
382
349
|
>
|
383
|
-
<
|
350
|
+
<InternalFormContext.Provider value={contextValue}>
|
351
|
+
<FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
|
384
352
|
{subaction && (
|
385
353
|
<input type="hidden" value={subaction} name="subaction" />
|
386
354
|
)}
|
355
|
+
{id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
|
387
356
|
{children}
|
388
|
-
</
|
357
|
+
</InternalFormContext.Provider>
|
389
358
|
</Form>
|
390
359
|
);
|
391
360
|
}
|
package/src/hooks.ts
CHANGED
@@ -1,21 +1,44 @@
|
|
1
|
-
import
|
2
|
-
import toPath from "lodash/toPath";
|
3
|
-
import { useContext, useEffect, useMemo } from "react";
|
4
|
-
import { FormContext } from "./internal/formContext";
|
1
|
+
import { useEffect, useMemo } from "react";
|
5
2
|
import {
|
6
3
|
createGetInputProps,
|
7
4
|
GetInputProps,
|
8
5
|
ValidationBehaviorOptions,
|
9
6
|
} from "./internal/getInputProps";
|
7
|
+
import {
|
8
|
+
useUnknownFormContextSelectAtom,
|
9
|
+
useInternalFormContext,
|
10
|
+
useFieldTouched,
|
11
|
+
useFieldError,
|
12
|
+
useFieldDefaultValue,
|
13
|
+
useContextSelectAtom,
|
14
|
+
useClearError,
|
15
|
+
useSetTouched,
|
16
|
+
} from "./internal/hooks";
|
17
|
+
import {
|
18
|
+
hasBeenSubmittedAtom,
|
19
|
+
isSubmittingAtom,
|
20
|
+
isValidAtom,
|
21
|
+
registerReceiveFocusAtom,
|
22
|
+
validateFieldAtom,
|
23
|
+
} from "./internal/state";
|
10
24
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
25
|
+
/**
|
26
|
+
* Returns whether or not the parent form is currently being submitted.
|
27
|
+
* This is different from remix's `useTransition().submission` in that it
|
28
|
+
* is aware of what form it's in and when _that_ form is being submitted.
|
29
|
+
*
|
30
|
+
* @param formId
|
31
|
+
*/
|
32
|
+
export const useIsSubmitting = (formId?: string) =>
|
33
|
+
useUnknownFormContextSelectAtom(formId, isSubmittingAtom, "useIsSubmitting");
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Returns whether or not the current form is valid.
|
37
|
+
*
|
38
|
+
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
39
|
+
*/
|
40
|
+
export const useIsValid = (formId?: string) =>
|
41
|
+
useUnknownFormContextSelectAtom(formId, isValidAtom, "useIsValid");
|
19
42
|
|
20
43
|
export type FieldProps = {
|
21
44
|
/**
|
@@ -64,21 +87,34 @@ export const useField = (
|
|
64
87
|
* Allows you to specify when a field gets validated (when using getInputProps)
|
65
88
|
*/
|
66
89
|
validationBehavior?: Partial<ValidationBehaviorOptions>;
|
90
|
+
/**
|
91
|
+
* The formId of the form you want to use.
|
92
|
+
* This is not necesary if the input is used inside a form.
|
93
|
+
*/
|
94
|
+
formId?: string;
|
67
95
|
}
|
68
96
|
): FieldProps => {
|
69
|
-
const {
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
touchedFields,
|
76
|
-
setFieldTouched,
|
77
|
-
hasBeenSubmitted,
|
78
|
-
} = useInternalFormContext("useField");
|
97
|
+
const { handleReceiveFocus, formId: providedFormId } = options ?? {};
|
98
|
+
const formContext = useInternalFormContext(providedFormId, "useField");
|
99
|
+
|
100
|
+
const defaultValue = useFieldDefaultValue(name, formContext);
|
101
|
+
const touched = useFieldTouched(name, formContext);
|
102
|
+
const error = useFieldError(name, formContext);
|
79
103
|
|
80
|
-
const
|
81
|
-
const
|
104
|
+
const clearError = useClearError(formContext);
|
105
|
+
const setTouched = useSetTouched(formContext);
|
106
|
+
const hasBeenSubmitted = useContextSelectAtom(
|
107
|
+
formContext.formId,
|
108
|
+
hasBeenSubmittedAtom
|
109
|
+
);
|
110
|
+
const validateField = useContextSelectAtom(
|
111
|
+
formContext.formId,
|
112
|
+
validateFieldAtom
|
113
|
+
);
|
114
|
+
const registerReceiveFocus = useContextSelectAtom(
|
115
|
+
formContext.formId,
|
116
|
+
registerReceiveFocusAtom
|
117
|
+
);
|
82
118
|
|
83
119
|
useEffect(() => {
|
84
120
|
if (handleReceiveFocus)
|
@@ -87,18 +123,14 @@ export const useField = (
|
|
87
123
|
|
88
124
|
const field = useMemo<FieldProps>(() => {
|
89
125
|
const helpers = {
|
90
|
-
error
|
91
|
-
clearError: () =>
|
92
|
-
clearError(name);
|
93
|
-
},
|
126
|
+
error,
|
127
|
+
clearError: () => clearError(name),
|
94
128
|
validate: () => {
|
95
129
|
validateField(name);
|
96
130
|
},
|
97
|
-
defaultValue
|
98
|
-
|
99
|
-
|
100
|
-
touched: isTouched,
|
101
|
-
setTouched: (touched: boolean) => setFieldTouched(name, touched),
|
131
|
+
defaultValue,
|
132
|
+
touched,
|
133
|
+
setTouched: (touched: boolean) => setTouched(name, touched),
|
102
134
|
};
|
103
135
|
const getInputProps = createGetInputProps({
|
104
136
|
...helpers,
|
@@ -111,34 +143,16 @@ export const useField = (
|
|
111
143
|
getInputProps,
|
112
144
|
};
|
113
145
|
}, [
|
114
|
-
|
146
|
+
error,
|
147
|
+
defaultValue,
|
148
|
+
touched,
|
115
149
|
name,
|
116
|
-
defaultValues,
|
117
|
-
isTouched,
|
118
150
|
hasBeenSubmitted,
|
119
151
|
options?.validationBehavior,
|
120
152
|
clearError,
|
121
153
|
validateField,
|
122
|
-
|
154
|
+
setTouched,
|
123
155
|
]);
|
124
156
|
|
125
157
|
return field;
|
126
158
|
};
|
127
|
-
|
128
|
-
/**
|
129
|
-
* Provides access to the entire form context.
|
130
|
-
*/
|
131
|
-
export const useFormContext = () => useInternalFormContext("useFormContext");
|
132
|
-
|
133
|
-
/**
|
134
|
-
* Returns whether or not the parent form is currently being submitted.
|
135
|
-
* This is different from remix's `useTransition().submission` in that it
|
136
|
-
* is aware of what form it's in and when _that_ form is being submitted.
|
137
|
-
*/
|
138
|
-
export const useIsSubmitting = () =>
|
139
|
-
useInternalFormContext("useIsSubmitting").isSubmitting;
|
140
|
-
|
141
|
-
/**
|
142
|
-
* Returns whether or not the current form is valid.
|
143
|
-
*/
|
144
|
-
export const useIsValid = () => useInternalFormContext("useIsValid").isValid;
|