remix-validated-form 3.3.1 → 4.0.0-beta.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 +3 -1
- package/browser/ValidatedForm.d.ts +1 -1
- package/browser/ValidatedForm.js +89 -41
- package/browser/components.d.ts +10 -0
- package/browser/components.js +10 -0
- package/browser/hooks.d.ts +13 -1
- package/browser/hooks.js +32 -7
- package/browser/internal/flatten.d.ts +0 -3
- package/browser/internal/flatten.js +0 -33
- package/browser/internal/formContext.d.ts +2 -3
- package/browser/internal/formContext.js +1 -11
- package/browser/internal/getInputProps.d.ts +10 -4
- package/browser/internal/getInputProps.js +23 -3
- package/browser/server.d.ts +13 -4
- package/browser/server.js +18 -13
- package/browser/types.d.ts +1 -0
- package/browser/types.js +1 -0
- package/browser/validation/createValidator.d.ts +2 -2
- package/browser/validation/createValidator.js +15 -8
- package/browser/validation/types.d.ts +35 -12
- package/build/ValidatedForm.d.ts +1 -1
- package/build/ValidatedForm.js +107 -40
- package/build/hooks.d.ts +13 -1
- package/build/hooks.js +33 -7
- package/build/internal/flatten.d.ts +0 -3
- package/build/internal/flatten.js +1 -35
- package/build/internal/formContext.d.ts +2 -3
- package/build/internal/formContext.js +1 -11
- package/build/internal/getInputProps.d.ts +10 -4
- package/build/internal/getInputProps.js +23 -3
- package/build/server.d.ts +13 -4
- package/build/server.js +18 -13
- package/build/types.d.ts +1 -0
- package/build/types.js +2 -0
- package/build/validation/createValidator.d.ts +2 -2
- package/build/validation/createValidator.js +15 -8
- package/build/validation/types.d.ts +35 -12
- package/package.json +1 -1
- package/src/ValidatedForm.tsx +112 -42
- package/src/hooks.ts +43 -7
- package/src/internal/flatten.ts +0 -44
- package/src/internal/formContext.ts +2 -13
- package/src/internal/getInputProps.ts +36 -9
- package/src/server.ts +28 -21
- package/src/types.ts +1 -0
- package/src/validation/createValidator.ts +21 -10
- package/src/validation/types.ts +38 -7
package/build/server.js
CHANGED
@@ -4,18 +4,23 @@ exports.validationError = void 0;
|
|
4
4
|
const server_runtime_1 = require("@remix-run/server-runtime");
|
5
5
|
/**
|
6
6
|
* Takes the errors from a `Validator` and returns a `Response`.
|
7
|
-
*
|
8
|
-
*
|
7
|
+
* When you return this from your action, `ValidatedForm` on the frontend will automatically
|
8
|
+
* display the errors on the correct fields on the correct form.
|
9
|
+
*
|
10
|
+
* _Recommended_: You can also provide a second argument to `validationError`
|
11
|
+
* to specify how to repopulate the form when JS is disabled.
|
12
|
+
*
|
13
|
+
* @example
|
14
|
+
* ```ts
|
15
|
+
* const result = validator.validate(await request.formData());
|
16
|
+
* if (result.error) return validationError(result.error, result.submittedData);
|
17
|
+
* ```
|
9
18
|
*/
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
}, { status: 422 });
|
18
|
-
}
|
19
|
-
return (0, server_runtime_1.json)({ fieldErrors: errors }, { status: 422 });
|
20
|
-
};
|
19
|
+
function validationError(error, repopulateFields) {
|
20
|
+
return (0, server_runtime_1.json)({
|
21
|
+
fieldErrors: error.fieldErrors,
|
22
|
+
subaction: error.subaction,
|
23
|
+
repopulateFields,
|
24
|
+
}, { status: 422 });
|
25
|
+
}
|
21
26
|
exports.validationError = validationError;
|
package/build/types.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export declare type ValidationState = "idle" | "validating" | "valid" | "invalid";
|
package/build/types.js
ADDED
@@ -1,7 +1,7 @@
|
|
1
|
-
import { Validator } from "..";
|
1
|
+
import { CreateValidatorArg, Validator } from "..";
|
2
2
|
/**
|
3
3
|
* Used to create a validator for a form.
|
4
4
|
* It provides built-in handling for unflattening nested objects and
|
5
5
|
* extracting the values from FormData.
|
6
6
|
*/
|
7
|
-
export declare function createValidator<T>(validator:
|
7
|
+
export declare function createValidator<T>(validator: CreateValidatorArg<T>): Validator<T>;
|
@@ -16,17 +16,24 @@ const preprocessFormData = (data) => {
|
|
16
16
|
*/
|
17
17
|
function createValidator(validator) {
|
18
18
|
return {
|
19
|
-
validate: (value) => {
|
19
|
+
validate: async (value) => {
|
20
20
|
const data = preprocessFormData(value);
|
21
|
-
const result = validator.validate(data);
|
21
|
+
const result = await validator.validate(data);
|
22
22
|
if (result.error) {
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
return {
|
24
|
+
data: undefined,
|
25
|
+
error: {
|
26
|
+
fieldErrors: result.error,
|
27
|
+
subaction: data.subaction,
|
28
|
+
},
|
29
|
+
submittedData: data,
|
30
|
+
};
|
28
31
|
}
|
29
|
-
return
|
32
|
+
return {
|
33
|
+
data: result.data,
|
34
|
+
error: undefined,
|
35
|
+
submittedData: data,
|
36
|
+
};
|
30
37
|
},
|
31
38
|
validateField: (data, field) => validator.validateField(preprocessFormData(data), field),
|
32
39
|
};
|
@@ -1,21 +1,32 @@
|
|
1
1
|
export declare type FieldErrors = Record<string, string>;
|
2
2
|
export declare type TouchedFields = Record<string, boolean>;
|
3
|
-
export declare type FieldErrorsWithData = FieldErrors & {
|
4
|
-
_submittedData: any;
|
5
|
-
};
|
6
3
|
export declare type GenericObject = {
|
7
4
|
[key: string]: any;
|
8
5
|
};
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
export declare type ValidatorError = {
|
7
|
+
subaction?: string;
|
8
|
+
fieldErrors: FieldErrors;
|
9
|
+
};
|
10
|
+
export declare type ValidationErrorResponseData = {
|
11
|
+
subaction?: string;
|
12
|
+
fieldErrors: FieldErrors;
|
13
|
+
repopulateFields?: unknown;
|
14
|
+
};
|
15
|
+
export declare type BaseResult = {
|
16
|
+
submittedData: GenericObject;
|
17
|
+
};
|
18
|
+
export declare type ErrorResult = BaseResult & {
|
19
|
+
error: ValidatorError;
|
20
|
+
data: undefined;
|
21
|
+
};
|
22
|
+
export declare type SuccessResult<DataType> = BaseResult & {
|
13
23
|
data: DataType;
|
14
24
|
error: undefined;
|
15
|
-
} | {
|
16
|
-
error: FieldErrors;
|
17
|
-
data: undefined;
|
18
25
|
};
|
26
|
+
/**
|
27
|
+
* The result when validating a form.
|
28
|
+
*/
|
29
|
+
export declare type ValidationResult<DataType> = SuccessResult<DataType> | ErrorResult;
|
19
30
|
/**
|
20
31
|
* The result when validating an individual field in a form.
|
21
32
|
*/
|
@@ -26,7 +37,19 @@ export declare type ValidateFieldResult = {
|
|
26
37
|
* A `Validator` can be passed to the `validator` prop of a `ValidatedForm`.
|
27
38
|
*/
|
28
39
|
export declare type Validator<DataType> = {
|
29
|
-
validate: (unvalidatedData: GenericObject) => ValidationResult<DataType
|
30
|
-
validateField: (unvalidatedData: GenericObject, field: string) => ValidateFieldResult
|
40
|
+
validate: (unvalidatedData: GenericObject) => Promise<ValidationResult<DataType>>;
|
41
|
+
validateField: (unvalidatedData: GenericObject, field: string) => Promise<ValidateFieldResult>;
|
42
|
+
};
|
43
|
+
export declare type Valid<DataType> = {
|
44
|
+
data: DataType;
|
45
|
+
error: undefined;
|
46
|
+
};
|
47
|
+
export declare type Invalid = {
|
48
|
+
error: FieldErrors;
|
49
|
+
data: undefined;
|
50
|
+
};
|
51
|
+
export declare type CreateValidatorArg<DataType> = {
|
52
|
+
validate: (unvalidatedData: GenericObject) => Promise<Valid<DataType> | Invalid>;
|
53
|
+
validateField: (unvalidatedData: GenericObject, field: string) => Promise<ValidateFieldResult>;
|
31
54
|
};
|
32
55
|
export declare type ValidatorData<T extends Validator<any>> = T extends Validator<infer U> ? U : never;
|
package/package.json
CHANGED
package/src/ValidatedForm.tsx
CHANGED
@@ -3,8 +3,10 @@ import {
|
|
3
3
|
useActionData,
|
4
4
|
useFetcher,
|
5
5
|
useFormAction,
|
6
|
+
useSubmit,
|
6
7
|
useTransition,
|
7
8
|
} from "@remix-run/react";
|
9
|
+
import uniq from "lodash/uniq";
|
8
10
|
import React, {
|
9
11
|
ComponentProps,
|
10
12
|
useEffect,
|
@@ -20,8 +22,8 @@ import { omit, mergeRefs } from "./internal/util";
|
|
20
22
|
import {
|
21
23
|
FieldErrors,
|
22
24
|
Validator,
|
23
|
-
FieldErrorsWithData,
|
24
25
|
TouchedFields,
|
26
|
+
ValidationErrorResponseData,
|
25
27
|
} from "./validation/types";
|
26
28
|
|
27
29
|
export type FormProps<DataType> = {
|
@@ -33,7 +35,10 @@ export type FormProps<DataType> = {
|
|
33
35
|
* A submit callback that gets called when the form is submitted
|
34
36
|
* after all validations have been run.
|
35
37
|
*/
|
36
|
-
onSubmit?: (
|
38
|
+
onSubmit?: (
|
39
|
+
data: DataType,
|
40
|
+
event: React.FormEvent<HTMLFormElement>
|
41
|
+
) => Promise<void>;
|
37
42
|
/**
|
38
43
|
* Allows you to provide a `fetcher` from remix's `useFetcher` hook.
|
39
44
|
* The form will use the fetcher for loading states, action data, etc
|
@@ -68,25 +73,27 @@ export type FormProps<DataType> = {
|
|
68
73
|
disableFocusOnError?: boolean;
|
69
74
|
} & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
|
70
75
|
|
71
|
-
function
|
76
|
+
function useErrorResponseForThisForm(
|
72
77
|
fetcher?: ReturnType<typeof useFetcher>,
|
73
78
|
subaction?: string
|
74
|
-
):
|
79
|
+
): ValidationErrorResponseData | null {
|
75
80
|
const actionData = useActionData<any>();
|
76
|
-
if (fetcher)
|
77
|
-
|
78
|
-
|
79
|
-
const submittedData = actionData.fieldErrors?._submittedData;
|
80
|
-
const subactionsMatch = subaction
|
81
|
-
? subaction === submittedData?.subaction
|
82
|
-
: !submittedData?.subaction;
|
83
|
-
return subactionsMatch ? actionData.fieldErrors : null;
|
81
|
+
if (fetcher) {
|
82
|
+
if ((fetcher.data as any)?.fieldErrors) return fetcher.data as any;
|
83
|
+
return null;
|
84
84
|
}
|
85
|
+
|
86
|
+
if (!actionData?.fieldErrors) return null;
|
87
|
+
if (
|
88
|
+
(!subaction && !actionData.subaction) ||
|
89
|
+
actionData.subaction === subaction
|
90
|
+
)
|
91
|
+
return actionData;
|
85
92
|
return null;
|
86
93
|
}
|
87
94
|
|
88
95
|
function useFieldErrors(
|
89
|
-
fieldErrorsFromBackend?:
|
96
|
+
fieldErrorsFromBackend?: FieldErrors
|
90
97
|
): [FieldErrors, React.Dispatch<React.SetStateAction<FieldErrors>>] {
|
91
98
|
const [fieldErrors, setFieldErrors] = useState<FieldErrors>(
|
92
99
|
fieldErrorsFromBackend ?? {}
|
@@ -136,11 +143,14 @@ const getDataFromForm = (el: HTMLFormElement) => new FormData(el);
|
|
136
143
|
* and only if JS is disabled.
|
137
144
|
*/
|
138
145
|
function useDefaultValues<DataType>(
|
139
|
-
|
146
|
+
repopulateFieldsFromBackend?: any,
|
140
147
|
defaultValues?: Partial<DataType>
|
141
148
|
) {
|
142
|
-
|
143
|
-
|
149
|
+
return repopulateFieldsFromBackend ?? defaultValues;
|
150
|
+
}
|
151
|
+
|
152
|
+
function nonNull<T>(value: T | null | undefined): value is T {
|
153
|
+
return value !== null;
|
144
154
|
}
|
145
155
|
|
146
156
|
const focusFirstInvalidInput = (
|
@@ -148,28 +158,48 @@ const focusFirstInvalidInput = (
|
|
148
158
|
customFocusHandlers: MultiValueMap<string, () => void>,
|
149
159
|
formElement: HTMLFormElement
|
150
160
|
) => {
|
151
|
-
const
|
152
|
-
.map((
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
161
|
+
const namesInOrder = [...formElement.elements]
|
162
|
+
.map((el) => {
|
163
|
+
const input = el instanceof RadioNodeList ? el[0] : el;
|
164
|
+
if (input instanceof HTMLInputElement) return input.name;
|
165
|
+
return null;
|
166
|
+
})
|
167
|
+
.filter(nonNull)
|
168
|
+
.filter((name) => name in fieldErrors);
|
169
|
+
const uniqueNamesInOrder = uniq(namesInOrder);
|
157
170
|
|
158
|
-
|
159
|
-
|
171
|
+
for (const fieldName of uniqueNamesInOrder) {
|
172
|
+
if (customFocusHandlers.has(fieldName)) {
|
173
|
+
customFocusHandlers.getAll(fieldName).forEach((handler) => {
|
160
174
|
handler();
|
161
175
|
});
|
162
176
|
break;
|
163
177
|
}
|
164
178
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
179
|
+
const elem = formElement.elements.namedItem(fieldName);
|
180
|
+
if (!elem) continue;
|
181
|
+
|
182
|
+
if (elem instanceof RadioNodeList) {
|
183
|
+
const selectedRadio =
|
184
|
+
[...elem]
|
185
|
+
.filter(
|
186
|
+
(item): item is HTMLInputElement => item instanceof HTMLInputElement
|
187
|
+
)
|
188
|
+
.find((item) => item.value === elem.value) ?? elem[0];
|
189
|
+
if (selectedRadio && selectedRadio instanceof HTMLInputElement) {
|
190
|
+
selectedRadio.focus();
|
191
|
+
break;
|
192
|
+
}
|
169
193
|
}
|
170
194
|
|
171
|
-
|
172
|
-
|
195
|
+
if (elem instanceof HTMLInputElement) {
|
196
|
+
if (elem.type === "hidden") {
|
197
|
+
continue;
|
198
|
+
}
|
199
|
+
|
200
|
+
elem.focus();
|
201
|
+
break;
|
202
|
+
}
|
173
203
|
}
|
174
204
|
};
|
175
205
|
|
@@ -190,15 +220,21 @@ export function ValidatedForm<DataType>({
|
|
190
220
|
disableFocusOnError,
|
191
221
|
...rest
|
192
222
|
}: FormProps<DataType>) {
|
193
|
-
const
|
194
|
-
const [fieldErrors, setFieldErrors] = useFieldErrors(
|
223
|
+
const backendError = useErrorResponseForThisForm(fetcher, subaction);
|
224
|
+
const [fieldErrors, setFieldErrors] = useFieldErrors(
|
225
|
+
backendError?.fieldErrors
|
226
|
+
);
|
195
227
|
const isSubmitting = useIsSubmitting(action, subaction, fetcher);
|
196
|
-
const defaultsToUse = useDefaultValues(
|
228
|
+
const defaultsToUse = useDefaultValues(
|
229
|
+
backendError?.repopulateFields,
|
230
|
+
defaultValues
|
231
|
+
);
|
197
232
|
const [touchedFields, setTouchedFields] = useState<TouchedFields>({});
|
198
233
|
const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false);
|
234
|
+
const submit = useSubmit();
|
199
235
|
const formRef = useRef<HTMLFormElement>(null);
|
200
236
|
useSubmitComplete(isSubmitting, () => {
|
201
|
-
if (!
|
237
|
+
if (!backendError && resetAfterSubmit) {
|
202
238
|
formRef.current?.reset();
|
203
239
|
}
|
204
240
|
});
|
@@ -220,9 +256,9 @@ export function ValidatedForm<DataType>({
|
|
220
256
|
clearError: (fieldName) => {
|
221
257
|
setFieldErrors((prev) => omit(prev, fieldName));
|
222
258
|
},
|
223
|
-
validateField: (fieldName) => {
|
259
|
+
validateField: async (fieldName) => {
|
224
260
|
invariant(formRef.current, "Cannot find reference to form");
|
225
|
-
const { error } = validator.validateField(
|
261
|
+
const { error } = await validator.validateField(
|
226
262
|
getDataFromForm(formRef.current),
|
227
263
|
fieldName as any
|
228
264
|
);
|
@@ -237,11 +273,13 @@ export function ValidatedForm<DataType>({
|
|
237
273
|
[fieldName]: error,
|
238
274
|
};
|
239
275
|
});
|
276
|
+
return error;
|
240
277
|
} else {
|
241
278
|
setFieldErrors((prev) => {
|
242
279
|
if (!(fieldName in prev)) return prev;
|
243
280
|
return omit(prev, fieldName);
|
244
281
|
});
|
282
|
+
return null;
|
245
283
|
}
|
246
284
|
},
|
247
285
|
registerReceiveFocus: (fieldName, handler) => {
|
@@ -267,26 +305,58 @@ export function ValidatedForm<DataType>({
|
|
267
305
|
|
268
306
|
const Form = fetcher?.Form ?? RemixForm;
|
269
307
|
|
308
|
+
let clickedButtonRef = React.useRef<any>();
|
309
|
+
useEffect(() => {
|
310
|
+
let form = formRef.current;
|
311
|
+
if (!form) return;
|
312
|
+
|
313
|
+
function handleClick(event: MouseEvent) {
|
314
|
+
if (!(event.target instanceof HTMLElement)) return;
|
315
|
+
let submitButton = event.target.closest<
|
316
|
+
HTMLButtonElement | HTMLInputElement
|
317
|
+
>("button,input[type=submit]");
|
318
|
+
|
319
|
+
if (
|
320
|
+
submitButton &&
|
321
|
+
submitButton.form === form &&
|
322
|
+
submitButton.type === "submit"
|
323
|
+
) {
|
324
|
+
clickedButtonRef.current = submitButton;
|
325
|
+
}
|
326
|
+
}
|
327
|
+
|
328
|
+
window.addEventListener("click", handleClick);
|
329
|
+
return () => {
|
330
|
+
window.removeEventListener("click", handleClick);
|
331
|
+
};
|
332
|
+
}, []);
|
333
|
+
|
270
334
|
return (
|
271
335
|
<Form
|
272
336
|
ref={mergeRefs([formRef, formRefProp])}
|
273
337
|
{...rest}
|
274
338
|
action={action}
|
275
|
-
onSubmit={(
|
339
|
+
onSubmit={async (e) => {
|
340
|
+
e.preventDefault();
|
276
341
|
setHasBeenSubmitted(true);
|
277
|
-
const result = validator.validate(
|
342
|
+
const result = await validator.validate(
|
343
|
+
getDataFromForm(e.currentTarget)
|
344
|
+
);
|
278
345
|
if (result.error) {
|
279
|
-
|
280
|
-
setFieldErrors(result.error);
|
346
|
+
setFieldErrors(result.error.fieldErrors);
|
281
347
|
if (!disableFocusOnError) {
|
282
348
|
focusFirstInvalidInput(
|
283
|
-
result.error,
|
349
|
+
result.error.fieldErrors,
|
284
350
|
customFocusHandlers(),
|
285
351
|
formRef.current!
|
286
352
|
);
|
287
353
|
}
|
288
354
|
} else {
|
289
|
-
onSubmit
|
355
|
+
onSubmit && onSubmit(result.data, e);
|
356
|
+
if (fetcher)
|
357
|
+
fetcher.submit(clickedButtonRef.current || e.currentTarget);
|
358
|
+
else submit(clickedButtonRef.current || e.currentTarget);
|
359
|
+
clickedButtonRef.current = null;
|
290
360
|
}
|
291
361
|
}}
|
292
362
|
onReset={(event) => {
|
package/src/hooks.ts
CHANGED
@@ -1,12 +1,22 @@
|
|
1
1
|
import get from "lodash/get";
|
2
2
|
import toPath from "lodash/toPath";
|
3
|
-
import { useContext, useEffect, useMemo } from "react";
|
3
|
+
import { useContext, useEffect, useMemo, useState } from "react";
|
4
4
|
import { FormContext } from "./internal/formContext";
|
5
5
|
import {
|
6
6
|
createGetInputProps,
|
7
7
|
GetInputProps,
|
8
8
|
ValidationBehaviorOptions,
|
9
9
|
} from "./internal/getInputProps";
|
10
|
+
import { ValidationState } from "./types";
|
11
|
+
|
12
|
+
const useInternalFormContext = (hookName: string) => {
|
13
|
+
const context = useContext(FormContext);
|
14
|
+
if (!context)
|
15
|
+
throw new Error(
|
16
|
+
`${hookName} must be used within a ValidatedForm component`
|
17
|
+
);
|
18
|
+
return context;
|
19
|
+
};
|
10
20
|
|
11
21
|
export type FieldProps = {
|
12
22
|
/**
|
@@ -21,6 +31,14 @@ export type FieldProps = {
|
|
21
31
|
* Validates the field.
|
22
32
|
*/
|
23
33
|
validate: () => void;
|
34
|
+
/**
|
35
|
+
* The validation state of the field.
|
36
|
+
* - idle: the field has not been validated yet.
|
37
|
+
* - validating: the field is currently being validated.
|
38
|
+
* - valid: the field is valid.
|
39
|
+
* - invalid: the field is invalid.
|
40
|
+
*/
|
41
|
+
validationState: ValidationState;
|
24
42
|
/**
|
25
43
|
* The default value of the field, if there is one.
|
26
44
|
*/
|
@@ -66,10 +84,11 @@ export const useField = (
|
|
66
84
|
touchedFields,
|
67
85
|
setFieldTouched,
|
68
86
|
hasBeenSubmitted,
|
69
|
-
} =
|
87
|
+
} = useInternalFormContext("useField");
|
70
88
|
|
71
89
|
const isTouched = !!touchedFields[name];
|
72
90
|
const { handleReceiveFocus } = options ?? {};
|
91
|
+
const [isValidating, setValidating] = useState(false);
|
73
92
|
|
74
93
|
useEffect(() => {
|
75
94
|
if (handleReceiveFocus)
|
@@ -77,17 +96,28 @@ export const useField = (
|
|
77
96
|
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
78
97
|
|
79
98
|
const field = useMemo<FieldProps>(() => {
|
99
|
+
const error = fieldErrors[name];
|
100
|
+
const getValidationState = (): ValidationState => {
|
101
|
+
if (isValidating) return "validating";
|
102
|
+
if (error) return "invalid";
|
103
|
+
if (!isTouched && !hasBeenSubmitted) return "idle";
|
104
|
+
return "valid";
|
105
|
+
};
|
80
106
|
const helpers = {
|
81
|
-
error
|
107
|
+
error,
|
82
108
|
clearError: () => {
|
83
109
|
clearError(name);
|
84
110
|
},
|
85
|
-
validate: () =>
|
111
|
+
validate: () => {
|
112
|
+
setValidating(true);
|
113
|
+
validateField(name).then((error) => setValidating(false));
|
114
|
+
},
|
86
115
|
defaultValue: defaultValues
|
87
116
|
? get(defaultValues, toPath(name), undefined)
|
88
117
|
: undefined,
|
89
118
|
touched: isTouched,
|
90
119
|
setTouched: (touched: boolean) => setFieldTouched(name, touched),
|
120
|
+
validationState: getValidationState(),
|
91
121
|
};
|
92
122
|
const getInputProps = createGetInputProps({
|
93
123
|
...helpers,
|
@@ -106,6 +136,7 @@ export const useField = (
|
|
106
136
|
isTouched,
|
107
137
|
hasBeenSubmitted,
|
108
138
|
options?.validationBehavior,
|
139
|
+
isValidating,
|
109
140
|
clearError,
|
110
141
|
validateField,
|
111
142
|
setFieldTouched,
|
@@ -116,13 +147,18 @@ export const useField = (
|
|
116
147
|
|
117
148
|
/**
|
118
149
|
* Provides access to the entire form context.
|
119
|
-
* This is not usually necessary, but can be useful for advanced use cases.
|
120
150
|
*/
|
121
|
-
export const useFormContext = () =>
|
151
|
+
export const useFormContext = () => useInternalFormContext("useFormContext");
|
122
152
|
|
123
153
|
/**
|
124
154
|
* Returns whether or not the parent form is currently being submitted.
|
125
155
|
* This is different from remix's `useTransition().submission` in that it
|
126
156
|
* is aware of what form it's in and when _that_ form is being submitted.
|
127
157
|
*/
|
128
|
-
export const useIsSubmitting = () =>
|
158
|
+
export const useIsSubmitting = () =>
|
159
|
+
useInternalFormContext("useIsSubmitting").isSubmitting;
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Returns whether or not the current form is valid.
|
163
|
+
*/
|
164
|
+
export const useIsValid = () => useInternalFormContext("useIsValid").isValid;
|
package/src/internal/flatten.ts
CHANGED
@@ -1,12 +1,4 @@
|
|
1
|
-
// `flatten` is taken from https://github.com/richie5um/FlattenJS. Decided to implement them here instead of using that package because this is a core functionality of the library and this will add more flexibility in case we need to change the implementation.
|
2
|
-
import assign from "lodash/assign";
|
3
|
-
import isArray from "lodash/isArray";
|
4
|
-
import isObject from "lodash/isObject";
|
5
|
-
import keys from "lodash/keys";
|
6
|
-
import mapKeys from "lodash/mapKeys";
|
7
1
|
import set from "lodash/set";
|
8
|
-
import transform from "lodash/transform";
|
9
|
-
import { GenericObject } from "..";
|
10
2
|
import { MultiValueMap } from "./MultiValueMap";
|
11
3
|
|
12
4
|
export const objectFromPathEntries = (entries: [string, any][]) => {
|
@@ -17,39 +9,3 @@ export const objectFromPathEntries = (entries: [string, any][]) => {
|
|
17
9
|
{}
|
18
10
|
);
|
19
11
|
};
|
20
|
-
|
21
|
-
/** Flatten an object so there are no nested objects or arrays */
|
22
|
-
export function flatten(obj: GenericObject, preserveEmpty = false) {
|
23
|
-
return transform(
|
24
|
-
obj,
|
25
|
-
function (result: GenericObject, value, key) {
|
26
|
-
if (isObject(value)) {
|
27
|
-
let flatMap = mapKeys(
|
28
|
-
flatten(value, preserveEmpty),
|
29
|
-
function (_mvalue, mkey) {
|
30
|
-
if (isArray(value)) {
|
31
|
-
let index = mkey.indexOf(".");
|
32
|
-
if (-1 !== index) {
|
33
|
-
return `${key}[${mkey.slice(0, index)}]${mkey.slice(index)}`;
|
34
|
-
}
|
35
|
-
return `${key}[${mkey}]`;
|
36
|
-
}
|
37
|
-
return `${key}.${mkey}`;
|
38
|
-
}
|
39
|
-
);
|
40
|
-
|
41
|
-
assign(result, flatMap);
|
42
|
-
|
43
|
-
// Preverve Empty arrays and objects
|
44
|
-
if (preserveEmpty && keys(flatMap).length === 0) {
|
45
|
-
result[key] = value;
|
46
|
-
}
|
47
|
-
} else {
|
48
|
-
result[key] = value;
|
49
|
-
}
|
50
|
-
|
51
|
-
return result;
|
52
|
-
},
|
53
|
-
{}
|
54
|
-
);
|
55
|
-
}
|
@@ -13,7 +13,7 @@ export type FormContextValue = {
|
|
13
13
|
/**
|
14
14
|
* Validate the specified field.
|
15
15
|
*/
|
16
|
-
validateField: (fieldName: string) =>
|
16
|
+
validateField: (fieldName: string) => Promise<string | null>;
|
17
17
|
/**
|
18
18
|
* The `action` prop of the form.
|
19
19
|
*/
|
@@ -30,7 +30,6 @@ export type FormContextValue = {
|
|
30
30
|
hasBeenSubmitted: boolean;
|
31
31
|
/**
|
32
32
|
* Whether or not the form is valid.
|
33
|
-
* This is a shortcut for `Object.keys(fieldErrors).length === 0`.
|
34
33
|
*/
|
35
34
|
isValid: boolean;
|
36
35
|
/**
|
@@ -52,14 +51,4 @@ export type FormContextValue = {
|
|
52
51
|
setFieldTouched: (fieldName: string, touched: boolean) => void;
|
53
52
|
};
|
54
53
|
|
55
|
-
export const FormContext = createContext<FormContextValue>(
|
56
|
-
fieldErrors: {},
|
57
|
-
clearError: () => {},
|
58
|
-
validateField: () => {},
|
59
|
-
isSubmitting: false,
|
60
|
-
hasBeenSubmitted: false,
|
61
|
-
isValid: true,
|
62
|
-
registerReceiveFocus: () => () => {},
|
63
|
-
touchedFields: {},
|
64
|
-
setFieldTouched: () => {},
|
65
|
-
});
|
54
|
+
export const FormContext = createContext<FormContextValue | null>(null);
|