remix-validated-form 4.6.12 → 5.0.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 +8 -8
- package/dist/index.cjs.js +178 -52
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +34 -12
- package/dist/index.esm.js +180 -54
- package/dist/index.esm.js.map +1 -1
- package/package.json +6 -5
- package/src/ValidatedForm.tsx +46 -16
- package/src/hooks.ts +5 -7
- package/src/internal/hooks.ts +5 -5
- package/src/internal/state/arrayUtil.ts +41 -3
- package/src/internal/state/createFormStore.ts +104 -27
- package/src/internal/state/fieldArray.tsx +56 -14
- package/src/server.ts +3 -0
- package/src/unreleased/formStateHooks.ts +8 -3
- package/src/validation/types.ts +4 -1
- package/tsconfig.json +1 -1
- package/src/validation/validation.test.ts +0 -330
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "remix-validated-form",
|
3
|
-
"version": "
|
3
|
+
"version": "5.0.1",
|
4
4
|
"description": "Form component and utils for easy form validation in remix",
|
5
5
|
"main": "./dist/index.cjs.js",
|
6
6
|
"module": "./dist/index.esm.js",
|
@@ -33,14 +33,14 @@
|
|
33
33
|
"validation"
|
34
34
|
],
|
35
35
|
"peerDependencies": {
|
36
|
-
"@remix-run/react": "1.
|
36
|
+
"@remix-run/react": ">= 1.15.0",
|
37
37
|
"@remix-run/server-runtime": "1.x",
|
38
38
|
"react": "^17.0.2 || ^18.0.0"
|
39
39
|
},
|
40
40
|
"devDependencies": {
|
41
|
-
"@remix-run/node": "^1.
|
42
|
-
"@remix-run/react": "^1.
|
43
|
-
"@remix-run/server-runtime": "^1.
|
41
|
+
"@remix-run/node": "^1.16.1",
|
42
|
+
"@remix-run/react": "^1.16.1",
|
43
|
+
"@remix-run/server-runtime": "^1.16.1",
|
44
44
|
"@testing-library/react": "^13.3.0",
|
45
45
|
"@types/lodash.get": "^4.4.7",
|
46
46
|
"@types/react": "^18.0.9",
|
@@ -55,6 +55,7 @@
|
|
55
55
|
"dependencies": {
|
56
56
|
"immer": "^9.0.12",
|
57
57
|
"lodash.get": "^4.4.2",
|
58
|
+
"nanoid": "3.3.6",
|
58
59
|
"remeda": "^1.2.0",
|
59
60
|
"tiny-invariant": "^1.2.0",
|
60
61
|
"zustand": "^4.3.0"
|
package/src/ValidatedForm.tsx
CHANGED
@@ -3,6 +3,8 @@ import {
|
|
3
3
|
Form as RemixForm,
|
4
4
|
FormMethod,
|
5
5
|
useSubmit,
|
6
|
+
SubmitOptions,
|
7
|
+
FormEncType,
|
6
8
|
} from "@remix-run/react";
|
7
9
|
import React, {
|
8
10
|
ComponentProps,
|
@@ -41,7 +43,24 @@ import {
|
|
41
43
|
} from "./internal/util";
|
42
44
|
import { FieldErrors, Validator } from "./validation/types";
|
43
45
|
|
44
|
-
|
46
|
+
type SubactionData<
|
47
|
+
DataType,
|
48
|
+
Subaction extends string | undefined
|
49
|
+
> = DataType & { subaction: Subaction };
|
50
|
+
|
51
|
+
// Not all validation libraries support encoding a literal value in the schema type (e.g. yup).
|
52
|
+
// This condition here allows us to provide strictness for users who are using a validation library that does support it,
|
53
|
+
// but also allows us to support users who are using a validation library that doesn't support it.
|
54
|
+
type DataForSubaction<
|
55
|
+
DataType,
|
56
|
+
Subaction extends string | undefined
|
57
|
+
> = Subaction extends string // Not all validation libraries support encoding a literal value in the schema type.
|
58
|
+
? SubactionData<DataType, Subaction> extends undefined
|
59
|
+
? DataType
|
60
|
+
: SubactionData<DataType, Subaction>
|
61
|
+
: DataType;
|
62
|
+
|
63
|
+
export type FormProps<DataType, Subaction extends string | undefined> = {
|
45
64
|
/**
|
46
65
|
* A `Validator` object that describes how to validate the form.
|
47
66
|
*/
|
@@ -51,7 +70,7 @@ export type FormProps<DataType> = {
|
|
51
70
|
* after all validations have been run.
|
52
71
|
*/
|
53
72
|
onSubmit?: (
|
54
|
-
data: DataType,
|
73
|
+
data: DataForSubaction<DataType, Subaction>,
|
55
74
|
event: React.FormEvent<HTMLFormElement>
|
56
75
|
) => void | Promise<void>;
|
57
76
|
/**
|
@@ -64,7 +83,7 @@ export type FormProps<DataType> = {
|
|
64
83
|
* Accepts an object of default values for the form
|
65
84
|
* that will automatically be propagated to the form fields via `useField`.
|
66
85
|
*/
|
67
|
-
defaultValues?: Partial<DataType
|
86
|
+
defaultValues?: Partial<DataForSubaction<DataType, Subaction>>;
|
68
87
|
/**
|
69
88
|
* A ref to the form element.
|
70
89
|
*/
|
@@ -74,7 +93,7 @@ export type FormProps<DataType> = {
|
|
74
93
|
* Setting a value here will cause the form to be submitted with an extra `subaction` value.
|
75
94
|
* This can be useful when there are multiple forms on the screen handled by the same action.
|
76
95
|
*/
|
77
|
-
subaction?:
|
96
|
+
subaction?: Subaction;
|
78
97
|
/**
|
79
98
|
* Reset the form to the default values after the form has been successfully submitted.
|
80
99
|
* This is useful if you want to submit the same form multiple times,
|
@@ -202,7 +221,7 @@ type HTMLFormSubmitter = HTMLButtonElement | HTMLInputElement;
|
|
202
221
|
/**
|
203
222
|
* The primary form component of `remix-validated-form`.
|
204
223
|
*/
|
205
|
-
export function ValidatedForm<DataType>({
|
224
|
+
export function ValidatedForm<DataType, Subaction extends string | undefined>({
|
206
225
|
validator,
|
207
226
|
onSubmit,
|
208
227
|
children,
|
@@ -217,8 +236,11 @@ export function ValidatedForm<DataType>({
|
|
217
236
|
method,
|
218
237
|
replace,
|
219
238
|
id,
|
239
|
+
preventScrollReset,
|
240
|
+
relative,
|
241
|
+
encType,
|
220
242
|
...rest
|
221
|
-
}: FormProps<DataType>) {
|
243
|
+
}: FormProps<DataType, Subaction>) {
|
222
244
|
const formId = useFormId(id);
|
223
245
|
const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults);
|
224
246
|
const contextValue = useMemo<InternalFormContextValue>(
|
@@ -321,12 +343,12 @@ export function ValidatedForm<DataType>({
|
|
321
343
|
startSubmit();
|
322
344
|
const submitter = nativeEvent.submitter as HTMLFormSubmitter | null;
|
323
345
|
const formMethod = (submitter?.formMethod as FormMethod) || method;
|
324
|
-
const
|
346
|
+
const formData = getDataFromForm(target);
|
325
347
|
if (submitter?.name) {
|
326
|
-
|
348
|
+
formData.append(submitter.name, submitter.value);
|
327
349
|
}
|
328
350
|
|
329
|
-
const result = await validator.validate(
|
351
|
+
const result = await validator.validate(formData);
|
330
352
|
if (result.error) {
|
331
353
|
setFieldErrors(result.error.fieldErrors);
|
332
354
|
endSubmit();
|
@@ -340,23 +362,28 @@ export function ValidatedForm<DataType>({
|
|
340
362
|
} else {
|
341
363
|
setFieldErrors({});
|
342
364
|
const eventProxy = formEventProxy(e);
|
343
|
-
await onSubmit?.(result.data, eventProxy);
|
365
|
+
await onSubmit?.(result.data as any, eventProxy);
|
344
366
|
if (eventProxy.defaultPrevented) {
|
345
367
|
endSubmit();
|
346
368
|
return;
|
347
369
|
}
|
348
370
|
|
371
|
+
const opts: SubmitOptions = {
|
372
|
+
method: formMethod,
|
373
|
+
replace,
|
374
|
+
preventScrollReset,
|
375
|
+
relative,
|
376
|
+
action,
|
377
|
+
encType: encType as FormEncType | undefined,
|
378
|
+
};
|
379
|
+
|
349
380
|
// We deviate from the Remix code here a bit because of our async submit.
|
350
381
|
// In Remix's `FormImpl`, they use `event.currentTarget` to get the form,
|
351
382
|
// but we already have the form in `formRef.current` so we can just use that.
|
352
383
|
// If we use `event.currentTarget` here, it will break because `currentTarget`
|
353
384
|
// will have changed since the start of the submission.
|
354
|
-
if (fetcher) fetcher.submit(
|
355
|
-
else
|
356
|
-
submit(submitter || target, {
|
357
|
-
replace,
|
358
|
-
method: formMethod,
|
359
|
-
});
|
385
|
+
if (fetcher) fetcher.submit(formData, opts);
|
386
|
+
else submit(formData, opts);
|
360
387
|
}
|
361
388
|
};
|
362
389
|
|
@@ -367,7 +394,10 @@ export function ValidatedForm<DataType>({
|
|
367
394
|
id={id}
|
368
395
|
action={action}
|
369
396
|
method={method}
|
397
|
+
encType={encType}
|
370
398
|
replace={replace}
|
399
|
+
preventScrollReset={preventScrollReset}
|
400
|
+
relative={relative}
|
371
401
|
onSubmit={(e) => {
|
372
402
|
e.preventDefault();
|
373
403
|
handleSubmit(
|
package/src/hooks.ts
CHANGED
@@ -13,8 +13,8 @@ import {
|
|
13
13
|
useInternalIsSubmitting,
|
14
14
|
useInternalIsValid,
|
15
15
|
useInternalHasBeenSubmitted,
|
16
|
-
useValidateField,
|
17
16
|
useRegisterReceiveFocus,
|
17
|
+
useSmartValidate,
|
18
18
|
} from "./internal/hooks";
|
19
19
|
import {
|
20
20
|
useControllableValue,
|
@@ -23,7 +23,7 @@ import {
|
|
23
23
|
|
24
24
|
/**
|
25
25
|
* Returns whether or not the parent form is currently being submitted.
|
26
|
-
* This is different from Remix's `
|
26
|
+
* This is different from Remix's `useNavigation()` in that it
|
27
27
|
* is aware of what form it's in and when _that_ form is being submitted.
|
28
28
|
*
|
29
29
|
* @param formId
|
@@ -106,7 +106,7 @@ export const useField = (
|
|
106
106
|
const clearError = useClearError(formContext);
|
107
107
|
|
108
108
|
const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId);
|
109
|
-
const
|
109
|
+
const smartValidate = useSmartValidate(formContext.formId);
|
110
110
|
const registerReceiveFocus = useRegisterReceiveFocus(formContext.formId);
|
111
111
|
|
112
112
|
useEffect(() => {
|
@@ -118,9 +118,7 @@ export const useField = (
|
|
118
118
|
const helpers = {
|
119
119
|
error,
|
120
120
|
clearError: () => clearError(name),
|
121
|
-
validate: () => {
|
122
|
-
validateField(name);
|
123
|
-
},
|
121
|
+
validate: () => smartValidate({ alwaysIncludeErrorsFromFields: [name] }),
|
124
122
|
defaultValue,
|
125
123
|
touched,
|
126
124
|
setTouched,
|
@@ -144,7 +142,7 @@ export const useField = (
|
|
144
142
|
name,
|
145
143
|
hasBeenSubmitted,
|
146
144
|
options?.validationBehavior,
|
147
|
-
|
145
|
+
smartValidate,
|
148
146
|
]);
|
149
147
|
|
150
148
|
return field;
|
package/src/internal/hooks.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { useActionData, useMatches,
|
1
|
+
import { useActionData, useMatches, useNavigation } from "@remix-run/react";
|
2
2
|
import { useCallback, useContext } from "react";
|
3
3
|
import { getPath } from "set-get";
|
4
4
|
import invariant from "tiny-invariant";
|
@@ -107,10 +107,10 @@ export const useDefaultValuesForForm = (
|
|
107
107
|
export const useHasActiveFormSubmit = ({
|
108
108
|
fetcher,
|
109
109
|
}: InternalFormContextValue): boolean => {
|
110
|
-
|
110
|
+
let navigation = useNavigation();
|
111
111
|
const hasActiveSubmission = fetcher
|
112
112
|
? fetcher.state === "submitting"
|
113
|
-
:
|
113
|
+
: navigation.state === "submitting";
|
114
114
|
return hasActiveSubmission;
|
115
115
|
};
|
116
116
|
|
@@ -169,8 +169,8 @@ export const useInternalIsValid = (formId: InternalFormId) =>
|
|
169
169
|
export const useInternalHasBeenSubmitted = (formId: InternalFormId) =>
|
170
170
|
useFormStore(formId, (state) => state.hasBeenSubmitted);
|
171
171
|
|
172
|
-
export const
|
173
|
-
useFormStore(formId, (state) => state.
|
172
|
+
export const useSmartValidate = (formId: InternalFormId) =>
|
173
|
+
useFormStore(formId, (state) => state.smartValidate);
|
174
174
|
|
175
175
|
export const useValidate = (formId: InternalFormId) =>
|
176
176
|
useFormStore(formId, (state) => state.validate);
|
@@ -20,6 +20,8 @@ export const getArray = (values: any, field: string): unknown[] => {
|
|
20
20
|
return value;
|
21
21
|
};
|
22
22
|
|
23
|
+
export const sparseCopy = <T>(array: T[]): T[] => array.slice();
|
24
|
+
|
23
25
|
export const swap = (array: unknown[], indexA: number, indexB: number) => {
|
24
26
|
const itemA = array[indexA];
|
25
27
|
const itemB = array[indexB];
|
@@ -46,7 +48,7 @@ export const swap = (array: unknown[], indexA: number, indexB: number) => {
|
|
46
48
|
function sparseSplice(
|
47
49
|
array: unknown[],
|
48
50
|
start: number,
|
49
|
-
deleteCount
|
51
|
+
deleteCount?: number,
|
50
52
|
item?: unknown
|
51
53
|
) {
|
52
54
|
// Inserting an item into an array won't behave as we need it to if the array isn't
|
@@ -56,8 +58,9 @@ function sparseSplice(
|
|
56
58
|
}
|
57
59
|
|
58
60
|
// If we just pass item in, it'll be undefined and splice will delete the item.
|
59
|
-
if (arguments.length === 4) return array.splice(start, deleteCount
|
60
|
-
return array.splice(start, deleteCount);
|
61
|
+
if (arguments.length === 4) return array.splice(start, deleteCount!, item);
|
62
|
+
else if (arguments.length === 3) return array.splice(start, deleteCount);
|
63
|
+
return array.splice(start);
|
61
64
|
}
|
62
65
|
|
63
66
|
export const move = (array: unknown[], from: number, to: number) => {
|
@@ -69,6 +72,13 @@ export const insert = (array: unknown[], index: number, value: unknown) => {
|
|
69
72
|
sparseSplice(array, index, 0, value);
|
70
73
|
};
|
71
74
|
|
75
|
+
export const insertEmpty = (array: unknown[], index: number) => {
|
76
|
+
const tail = sparseSplice(array, index);
|
77
|
+
tail.forEach((item, i) => {
|
78
|
+
sparseSplice(array, index + i + 1, 0, item);
|
79
|
+
});
|
80
|
+
};
|
81
|
+
|
72
82
|
export const remove = (array: unknown[], index: number) => {
|
73
83
|
sparseSplice(array, index, 1);
|
74
84
|
};
|
@@ -240,6 +250,34 @@ if (import.meta.vitest) {
|
|
240
250
|
});
|
241
251
|
});
|
242
252
|
|
253
|
+
describe("insertEmpty", () => {
|
254
|
+
it("should insert an empty item at a given index", () => {
|
255
|
+
const array = [1, 2, 3];
|
256
|
+
insertEmpty(array, 1);
|
257
|
+
// eslint-disable-next-line no-sparse-arrays
|
258
|
+
expect(array).toStrictEqual([1, , 2, 3]);
|
259
|
+
expect(array).not.toStrictEqual([1, undefined, 2, 3]);
|
260
|
+
});
|
261
|
+
|
262
|
+
it("should work with already sparse arrays", () => {
|
263
|
+
// eslint-disable-next-line no-sparse-arrays
|
264
|
+
const array = [, , 1, , 2, , 3];
|
265
|
+
insertEmpty(array, 3);
|
266
|
+
// eslint-disable-next-line no-sparse-arrays
|
267
|
+
expect(array).toStrictEqual([, , 1, , , 2, , 3]);
|
268
|
+
expect(array).not.toStrictEqual([
|
269
|
+
undefined,
|
270
|
+
undefined,
|
271
|
+
1,
|
272
|
+
undefined,
|
273
|
+
undefined,
|
274
|
+
2,
|
275
|
+
undefined,
|
276
|
+
3,
|
277
|
+
]);
|
278
|
+
});
|
279
|
+
});
|
280
|
+
|
243
281
|
describe("remove", () => {
|
244
282
|
it("should remove an item at a given index", () => {
|
245
283
|
const array = [1, 2, 3];
|
@@ -22,6 +22,10 @@ export type SyncedFormProps = {
|
|
22
22
|
validator: Validator<unknown>;
|
23
23
|
};
|
24
24
|
|
25
|
+
export type SmartValidateOpts = {
|
26
|
+
alwaysIncludeErrorsFromFields?: string[];
|
27
|
+
};
|
28
|
+
|
25
29
|
export type FormStoreState = {
|
26
30
|
forms: { [formId: InternalFormId]: FormState };
|
27
31
|
form: (formId: InternalFormId) => FormState;
|
@@ -49,8 +53,10 @@ export type FormState = {
|
|
49
53
|
reset: () => void;
|
50
54
|
syncFormProps: (props: SyncedFormProps) => void;
|
51
55
|
setFormElement: (formElement: HTMLFormElement | null) => void;
|
52
|
-
validateField: (fieldName: string) => Promise<string | null>;
|
53
56
|
validate: () => Promise<ValidationResult<unknown>>;
|
57
|
+
smartValidate: (
|
58
|
+
opts?: SmartValidateOpts
|
59
|
+
) => Promise<ValidationResult<unknown>>;
|
54
60
|
resetFormElement: () => void;
|
55
61
|
submit: () => void;
|
56
62
|
getValues: () => FormData;
|
@@ -101,12 +107,15 @@ const defaultFormState: FormState = {
|
|
101
107
|
reset: () => noOp,
|
102
108
|
syncFormProps: noOp,
|
103
109
|
setFormElement: noOp,
|
104
|
-
validateField: async () => null,
|
105
110
|
|
106
111
|
validate: async () => {
|
107
112
|
throw new Error("Validate called before form was initialized.");
|
108
113
|
},
|
109
114
|
|
115
|
+
smartValidate: async () => {
|
116
|
+
throw new Error("Validate called before form was initialized.");
|
117
|
+
},
|
118
|
+
|
110
119
|
submit: async () => {
|
111
120
|
throw new Error("Submit called before form was initialized.");
|
112
121
|
},
|
@@ -210,7 +219,7 @@ const createFormState = (
|
|
210
219
|
state.formElement = formElement as any;
|
211
220
|
});
|
212
221
|
},
|
213
|
-
|
222
|
+
validate: async () => {
|
214
223
|
const formElement = get().formElement;
|
215
224
|
invariant(
|
216
225
|
formElement,
|
@@ -220,26 +229,15 @@ const createFormState = (
|
|
220
229
|
const validator = get().formProps?.validator;
|
221
230
|
invariant(
|
222
231
|
validator,
|
223
|
-
"Cannot validator. This is probably a bug in remix-validated-form."
|
232
|
+
"Cannot find validator. This is probably a bug in remix-validated-form."
|
224
233
|
);
|
225
234
|
|
226
|
-
await
|
227
|
-
|
228
|
-
|
229
|
-
new FormData(formElement),
|
230
|
-
field
|
231
|
-
);
|
232
|
-
|
233
|
-
if (error) {
|
234
|
-
get().setFieldError(field, error);
|
235
|
-
return error;
|
236
|
-
} else {
|
237
|
-
get().clearFieldError(field);
|
238
|
-
return null;
|
239
|
-
}
|
235
|
+
const result = await validator.validate(new FormData(formElement));
|
236
|
+
if (result.error) get().setFieldErrors(result.error.fieldErrors);
|
237
|
+
return result;
|
240
238
|
},
|
241
239
|
|
242
|
-
|
240
|
+
smartValidate: async ({ alwaysIncludeErrorsFromFields = [] } = {}) => {
|
243
241
|
const formElement = get().formElement;
|
244
242
|
invariant(
|
245
243
|
formElement,
|
@@ -249,12 +247,91 @@ const createFormState = (
|
|
249
247
|
const validator = get().formProps?.validator;
|
250
248
|
invariant(
|
251
249
|
validator,
|
252
|
-
"Cannot validator. This is probably a bug in remix-validated-form."
|
250
|
+
"Cannot find validator. This is probably a bug in remix-validated-form."
|
253
251
|
);
|
254
252
|
|
255
|
-
|
256
|
-
|
257
|
-
|
253
|
+
await Promise.all(
|
254
|
+
alwaysIncludeErrorsFromFields.map((field) =>
|
255
|
+
get().controlledFields.awaitValueUpdate?.(field)
|
256
|
+
)
|
257
|
+
);
|
258
|
+
|
259
|
+
const validationResult = await validator.validate(
|
260
|
+
new FormData(formElement)
|
261
|
+
);
|
262
|
+
if (!validationResult.error) {
|
263
|
+
// Only update the field errors if it hasn't changed
|
264
|
+
const hadErrors = Object.keys(get().fieldErrors).length > 0;
|
265
|
+
if (hadErrors) get().setFieldErrors({});
|
266
|
+
return validationResult;
|
267
|
+
}
|
268
|
+
|
269
|
+
const {
|
270
|
+
error: { fieldErrors },
|
271
|
+
} = validationResult;
|
272
|
+
const errorFields = new Set<string>();
|
273
|
+
const incomingErrors = new Set<string>();
|
274
|
+
const prevErrors = new Set<string>();
|
275
|
+
|
276
|
+
Object.keys(fieldErrors).forEach((field) => {
|
277
|
+
errorFields.add(field);
|
278
|
+
incomingErrors.add(field);
|
279
|
+
});
|
280
|
+
|
281
|
+
Object.keys(get().fieldErrors).forEach((field) => {
|
282
|
+
errorFields.add(field);
|
283
|
+
prevErrors.add(field);
|
284
|
+
});
|
285
|
+
|
286
|
+
const fieldsToUpdate = new Set<string>();
|
287
|
+
const fieldsToDelete = new Set<string>();
|
288
|
+
|
289
|
+
errorFields.forEach((field) => {
|
290
|
+
// If an error has been cleared, remove it.
|
291
|
+
if (!incomingErrors.has(field)) {
|
292
|
+
fieldsToDelete.add(field);
|
293
|
+
return;
|
294
|
+
}
|
295
|
+
|
296
|
+
// If an error has changed, we should update it.
|
297
|
+
if (prevErrors.has(field) && incomingErrors.has(field)) {
|
298
|
+
// Only update if the error has changed to avoid unnecessary rerenders
|
299
|
+
if (fieldErrors[field] !== get().fieldErrors[field])
|
300
|
+
fieldsToUpdate.add(field);
|
301
|
+
return;
|
302
|
+
}
|
303
|
+
|
304
|
+
// If the error is always included, then we should update it.
|
305
|
+
if (alwaysIncludeErrorsFromFields.includes(field)) {
|
306
|
+
fieldsToUpdate.add(field);
|
307
|
+
return;
|
308
|
+
}
|
309
|
+
|
310
|
+
// If the error is new, then only update if the field has been touched
|
311
|
+
// or if the form has been submitted
|
312
|
+
if (!prevErrors.has(field)) {
|
313
|
+
const fieldTouched = get().touchedFields[field];
|
314
|
+
const formHasBeenSubmitted = get().hasBeenSubmitted;
|
315
|
+
if (fieldTouched || formHasBeenSubmitted) fieldsToUpdate.add(field);
|
316
|
+
return;
|
317
|
+
}
|
318
|
+
});
|
319
|
+
|
320
|
+
if (fieldsToDelete.size === 0 && fieldsToUpdate.size === 0) {
|
321
|
+
return { ...validationResult, error: { fieldErrors: get().fieldErrors } };
|
322
|
+
}
|
323
|
+
|
324
|
+
set((state) => {
|
325
|
+
fieldsToDelete.forEach((field) => {
|
326
|
+
delete state.fieldErrors[field];
|
327
|
+
});
|
328
|
+
|
329
|
+
fieldsToUpdate.forEach((field) => {
|
330
|
+
state.fieldErrors[field] = fieldErrors[field];
|
331
|
+
});
|
332
|
+
});
|
333
|
+
|
334
|
+
return { ...validationResult, error: { fieldErrors: get().fieldErrors } };
|
258
335
|
},
|
259
336
|
|
260
337
|
submit: () => {
|
@@ -409,10 +486,10 @@ const createFormState = (
|
|
409
486
|
);
|
410
487
|
// Even though this is a new item, we need to push around other items.
|
411
488
|
arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
|
412
|
-
arrayUtil.
|
489
|
+
arrayUtil.insertEmpty(array, index)
|
413
490
|
);
|
414
491
|
arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
|
415
|
-
arrayUtil.
|
492
|
+
arrayUtil.insertEmpty(array, index)
|
416
493
|
);
|
417
494
|
});
|
418
495
|
get().controlledFields.kickoffValueUpdate(fieldName);
|
@@ -458,10 +535,10 @@ const createFormState = (
|
|
458
535
|
.getArray(state.currentDefaultValues, fieldName)
|
459
536
|
.unshift(value);
|
460
537
|
arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
|
461
|
-
|
538
|
+
arrayUtil.insertEmpty(array, 0)
|
462
539
|
);
|
463
540
|
arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
|
464
|
-
|
541
|
+
arrayUtil.insertEmpty(array, 0)
|
465
542
|
);
|
466
543
|
});
|
467
544
|
},
|