remix-validated-form 4.5.0-beta.0 → 4.5.1
Sign up to get free protection for your applications and to get access to all the features.
- package/.turbo/turbo-build.log +7 -7
- package/browser/ValidatedForm.js +10 -32
- package/browser/internal/hooks.d.ts +3 -1
- package/browser/internal/hooks.js +2 -0
- package/browser/internal/logic/nestedObjectToPathObject.d.ts +1 -0
- package/browser/internal/logic/nestedObjectToPathObject.js +47 -0
- package/browser/internal/state/arrayUtil.d.ts +12 -0
- package/browser/internal/state/arrayUtil.js +337 -0
- package/browser/internal/state/createFormStore.d.ts +4 -2
- package/browser/internal/state/createFormStore.js +21 -4
- package/browser/internal/state/fieldArray.d.ts +28 -0
- package/browser/internal/state/fieldArray.js +73 -0
- package/browser/internal/state/types.d.ts +0 -0
- package/browser/internal/state/types.js +0 -0
- package/browser/unreleased/formStateHooks.d.ts +12 -2
- package/browser/unreleased/formStateHooks.js +15 -2
- package/browser/userFacingFormContext.d.ts +12 -2
- package/browser/userFacingFormContext.js +5 -1
- package/dist/remix-validated-form.cjs.js +3 -3
- package/dist/remix-validated-form.cjs.js.map +1 -1
- package/dist/remix-validated-form.es.js +55 -37
- package/dist/remix-validated-form.es.js.map +1 -1
- package/dist/remix-validated-form.umd.js +3 -3
- package/dist/remix-validated-form.umd.js.map +1 -1
- package/dist/types/internal/hooks.d.ts +3 -1
- package/dist/types/internal/state/createFormStore.d.ts +4 -2
- package/dist/types/unreleased/formStateHooks.d.ts +12 -2
- package/dist/types/userFacingFormContext.d.ts +12 -2
- package/package.json +1 -1
- package/src/ValidatedForm.tsx +24 -42
- package/src/internal/hooks.ts +6 -0
- package/src/internal/state/createFormStore.ts +40 -6
- package/src/unreleased/formStateHooks.ts +32 -3
- package/src/userFacingFormContext.ts +22 -2
- package/src/validation/validation.test.ts +7 -7
package/.turbo/turbo-build.log
CHANGED
@@ -3,15 +3,15 @@
|
|
3
3
|
transforming...
|
4
4
|
[32m✓[39m 319 modules transformed.
|
5
5
|
rendering chunks...
|
6
|
-
[90m[37m[2mdist/[22m[90m[39m[36mremix-validated-form.cjs.js [39m [2m45.
|
7
|
-
[90m[37m[2mdist/[22m[90m[39m[90mremix-validated-form.cjs.js.map[39m [
|
8
|
-
[90m[37m[2mdist/[22m[90m[39m[36mremix-validated-form.es.js [39m [
|
9
|
-
[90m[37m[2mdist/[22m[90m[39m[90mremix-validated-form.es.js.map[39m [
|
10
|
-
[90m[37m[2mdist/[22m[90m[39m[36mremix-validated-form.umd.js [39m [2m45.
|
11
|
-
[90m[37m[2mdist/[22m[90m[39m[90mremix-validated-form.umd.js.map[39m [
|
6
|
+
[90m[37m[2mdist/[22m[90m[39m[36mremix-validated-form.cjs.js [39m [2m45.46 KiB / gzip: 17.05 KiB[22m
|
7
|
+
[90m[37m[2mdist/[22m[90m[39m[90mremix-validated-form.cjs.js.map[39m [2m252.62 KiB[22m
|
8
|
+
[90m[37m[2mdist/[22m[90m[39m[36mremix-validated-form.es.js [39m [2m101.10 KiB / gzip: 23.71 KiB[22m
|
9
|
+
[90m[37m[2mdist/[22m[90m[39m[90mremix-validated-form.es.js.map[39m [2m260.60 KiB[22m
|
10
|
+
[90m[37m[2mdist/[22m[90m[39m[36mremix-validated-form.umd.js [39m [2m45.71 KiB / gzip: 17.16 KiB[22m
|
11
|
+
[90m[37m[2mdist/[22m[90m[39m[90mremix-validated-form.umd.js.map[39m [2m252.60 KiB[22m
|
12
12
|
[32m[39m
|
13
13
|
[32m[36m[vite:dts][39m[32m Start generate declaration files...[39m
|
14
|
-
[32m[36m[vite:dts][39m[32m Declaration files built in
|
14
|
+
[32m[36m[vite:dts][39m[32m Declaration files built in 2049ms.[39m
|
15
15
|
[32m[39m
|
16
16
|
No name was provided for external module 'react' in output.globals – guessing 'React'
|
17
17
|
No name was provided for external module '@remix-run/react' in output.globals – guessing 'react'
|
package/browser/ValidatedForm.js
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
2
2
|
import { Form as RemixForm, useSubmit } from "@remix-run/react";
|
3
3
|
import uniq from "lodash/uniq";
|
4
|
-
import
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
5
5
|
import { useIsSubmitting, useIsValid } from "./hooks";
|
6
6
|
import { FORM_ID_FIELD } from "./internal/constants";
|
7
7
|
import { InternalFormContext, } from "./internal/formContext";
|
@@ -21,7 +21,7 @@ const focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) =
|
|
21
21
|
const namesInOrder = [...formElement.elements]
|
22
22
|
.map((el) => {
|
23
23
|
const input = el instanceof RadioNodeList ? el[0] : el;
|
24
|
-
if (input instanceof
|
24
|
+
if (input instanceof HTMLElement && "name" in input)
|
25
25
|
return input.name;
|
26
26
|
return null;
|
27
27
|
})
|
@@ -47,8 +47,8 @@ const focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) =
|
|
47
47
|
break;
|
48
48
|
}
|
49
49
|
}
|
50
|
-
if (elem instanceof
|
51
|
-
if (elem.type === "hidden") {
|
50
|
+
if (elem instanceof HTMLElement) {
|
51
|
+
if (elem instanceof HTMLInputElement && elem.type === "hidden") {
|
52
52
|
continue;
|
53
53
|
}
|
54
54
|
elem.focus();
|
@@ -163,27 +163,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
163
163
|
useSubmitComplete(hasActiveSubmission, () => {
|
164
164
|
endSubmit();
|
165
165
|
});
|
166
|
-
|
167
|
-
useEffect(() => {
|
168
|
-
let form = formRef.current;
|
169
|
-
if (!form)
|
170
|
-
return;
|
171
|
-
function handleClick(event) {
|
172
|
-
if (!(event.target instanceof HTMLElement))
|
173
|
-
return;
|
174
|
-
let submitButton = event.target.closest("button,input[type=submit]");
|
175
|
-
if (submitButton &&
|
176
|
-
submitButton.form === form &&
|
177
|
-
submitButton.type === "submit") {
|
178
|
-
clickedButtonRef.current = submitButton;
|
179
|
-
}
|
180
|
-
}
|
181
|
-
window.addEventListener("click", handleClick, { capture: true });
|
182
|
-
return () => {
|
183
|
-
window.removeEventListener("click", handleClick, { capture: true });
|
184
|
-
};
|
185
|
-
}, []);
|
186
|
-
const handleSubmit = async (e) => {
|
166
|
+
const handleSubmit = async (e, target, nativeEvent) => {
|
187
167
|
startSubmit();
|
188
168
|
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
189
169
|
if (result.error) {
|
@@ -200,8 +180,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
200
180
|
endSubmit();
|
201
181
|
return;
|
202
182
|
}
|
203
|
-
const submitter =
|
204
|
-
.submitter;
|
183
|
+
const submitter = nativeEvent.submitter;
|
205
184
|
// We deviate from the remix code here a bit because of our async submit.
|
206
185
|
// In remix's `FormImpl`, they use `event.currentTarget` to get the form,
|
207
186
|
// but we already have the form in `formRef.current` so we can just use that.
|
@@ -210,18 +189,17 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
210
189
|
if (fetcher)
|
211
190
|
fetcher.submit(submitter || e.currentTarget);
|
212
191
|
else
|
213
|
-
submit(submitter ||
|
214
|
-
clickedButtonRef.current = null;
|
192
|
+
submit(submitter || target, { replace });
|
215
193
|
}
|
216
194
|
};
|
217
195
|
return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
|
218
196
|
e.preventDefault();
|
219
|
-
handleSubmit(e);
|
197
|
+
handleSubmit(e, e.currentTarget, e.nativeEvent);
|
220
198
|
}, onReset: (event) => {
|
221
199
|
onReset === null || onReset === void 0 ? void 0 : onReset(event);
|
222
200
|
if (event.defaultPrevented)
|
223
201
|
return;
|
224
202
|
reset();
|
225
203
|
resetControlledFields(formId);
|
226
|
-
}, children:
|
204
|
+
}, children: _jsx(InternalFormContext.Provider, { value: contextValue, children: _jsxs(_Fragment, { children: [_jsx(FormResetter, { formRef: formRef, resetAfterSubmit: resetAfterSubmit }, void 0), subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), id && _jsx("input", { type: "hidden", value: id, name: FORM_ID_FIELD }, void 0), children] }, void 0) }, void 0) }, void 0));
|
227
205
|
}
|
@@ -18,7 +18,7 @@ export declare const useInternalIsSubmitting: (formId: InternalFormId) => boolea
|
|
18
18
|
export declare const useInternalIsValid: (formId: InternalFormId) => boolean;
|
19
19
|
export declare const useInternalHasBeenSubmitted: (formId: InternalFormId) => boolean;
|
20
20
|
export declare const useValidateField: (formId: InternalFormId) => (fieldName: string) => Promise<string | null>;
|
21
|
-
export declare const useValidate: (formId: InternalFormId) => () => Promise<
|
21
|
+
export declare const useValidate: (formId: InternalFormId) => () => Promise<import("..").ValidationResult<unknown>>;
|
22
22
|
export declare const useRegisterReceiveFocus: (formId: InternalFormId) => (fieldName: string, handler: () => void) => () => void;
|
23
23
|
export declare const useSyncedDefaultValues: (formId: InternalFormId) => {
|
24
24
|
[fieldName: string]: any;
|
@@ -28,5 +28,7 @@ export declare const useTouchedFields: (formId: InternalFormId) => import("..").
|
|
28
28
|
export declare const useFieldErrors: (formId: InternalFormId) => FieldErrors;
|
29
29
|
export declare const useSetFieldErrors: (formId: InternalFormId) => (errors: FieldErrors) => void;
|
30
30
|
export declare const useResetFormElement: (formId: InternalFormId) => () => void;
|
31
|
+
export declare const useSubmitForm: (formId: InternalFormId) => () => void;
|
31
32
|
export declare const useFormActionProp: (formId: InternalFormId) => string | undefined;
|
32
33
|
export declare const useFormSubactionProp: (formId: InternalFormId) => string | undefined;
|
34
|
+
export declare const useFormValues: (formId: InternalFormId) => () => FormData;
|
@@ -113,5 +113,7 @@ export const useTouchedFields = (formId) => useFormStore(formId, (state) => stat
|
|
113
113
|
export const useFieldErrors = (formId) => useFormStore(formId, (state) => state.fieldErrors);
|
114
114
|
export const useSetFieldErrors = (formId) => useFormStore(formId, (state) => state.setFieldErrors);
|
115
115
|
export const useResetFormElement = (formId) => useFormStore(formId, (state) => state.resetFormElement);
|
116
|
+
export const useSubmitForm = (formId) => useFormStore(formId, (state) => state.submit);
|
116
117
|
export const useFormActionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.action; });
|
117
118
|
export const useFormSubactionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.subaction; });
|
119
|
+
export const useFormValues = (formId) => useFormStore(formId, (state) => state.getValues);
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const nestedObjectToPathObject: (val: any, acc: Record<string, any>, path: string) => any;
|
@@ -0,0 +1,47 @@
|
|
1
|
+
export const nestedObjectToPathObject = (val, acc, path) => {
|
2
|
+
if (Array.isArray(val)) {
|
3
|
+
val.forEach((v, index) => nestedObjectToPathObject(v, acc, `${path}[${index}]`));
|
4
|
+
return acc;
|
5
|
+
}
|
6
|
+
if (typeof val === "object") {
|
7
|
+
Object.entries(val).forEach(([key, value]) => {
|
8
|
+
const nextPath = path ? `${path}.${key}` : key;
|
9
|
+
nestedObjectToPathObject(value, acc, nextPath);
|
10
|
+
});
|
11
|
+
return acc;
|
12
|
+
}
|
13
|
+
if (val !== undefined) {
|
14
|
+
acc[path] = val;
|
15
|
+
}
|
16
|
+
return acc;
|
17
|
+
};
|
18
|
+
if (import.meta.vitest) {
|
19
|
+
const { describe, expect, it } = import.meta.vitest;
|
20
|
+
describe("nestedObjectToPathObject", () => {
|
21
|
+
it("should return an object with the correct path", () => {
|
22
|
+
const result = nestedObjectToPathObject({
|
23
|
+
a: 1,
|
24
|
+
b: 2,
|
25
|
+
c: { foo: "bar", baz: [true, false] },
|
26
|
+
d: [
|
27
|
+
{ foo: "bar", baz: [true, false] },
|
28
|
+
{ e: true, f: "hi" },
|
29
|
+
],
|
30
|
+
g: undefined,
|
31
|
+
}, {}, "");
|
32
|
+
expect(result).toEqual({
|
33
|
+
a: 1,
|
34
|
+
b: 2,
|
35
|
+
"c.foo": "bar",
|
36
|
+
"c.baz[0]": true,
|
37
|
+
"c.baz[1]": false,
|
38
|
+
"d[0].foo": "bar",
|
39
|
+
"d[0].baz[0]": true,
|
40
|
+
"d[0].baz[1]": false,
|
41
|
+
"d[1].e": true,
|
42
|
+
"d[1].f": "hi",
|
43
|
+
});
|
44
|
+
expect(Object.keys(result)).toHaveLength(10);
|
45
|
+
});
|
46
|
+
});
|
47
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
export declare const getArray: (values: any, field: string) => unknown[];
|
2
|
+
export declare const swap: (array: unknown[], indexA: number, indexB: number) => void;
|
3
|
+
export declare const move: (array: unknown[], from: number, to: number) => void;
|
4
|
+
export declare const insert: (array: unknown[], index: number, value: unknown) => void;
|
5
|
+
export declare const remove: (array: unknown[], index: number) => void;
|
6
|
+
export declare const replace: (array: unknown[], index: number, value: unknown) => void;
|
7
|
+
/**
|
8
|
+
* The purpose of this helper is to make it easier to update `fieldErrors` and `touchedFields`.
|
9
|
+
* We key those objects by full paths to the fields.
|
10
|
+
* When we're doing array mutations, that makes it difficult to update those objects.
|
11
|
+
*/
|
12
|
+
export declare const mutateAsArray: (field: string, obj: Record<string, any>, mutate: (arr: any[]) => void) => void;
|
@@ -0,0 +1,337 @@
|
|
1
|
+
import lodashGet from "lodash/get";
|
2
|
+
import lodashSet from "lodash/set";
|
3
|
+
import invariant from "tiny-invariant";
|
4
|
+
////
|
5
|
+
// All of these array helpers are written in a way that mutates the original array.
|
6
|
+
// This is because we're working with immer.
|
7
|
+
////
|
8
|
+
export const getArray = (values, field) => {
|
9
|
+
const value = lodashGet(values, field);
|
10
|
+
if (value === undefined || value === null) {
|
11
|
+
const newValue = [];
|
12
|
+
lodashSet(values, field, newValue);
|
13
|
+
return newValue;
|
14
|
+
}
|
15
|
+
invariant(Array.isArray(value), `FieldArray: defaultValue value for ${field} must be an array, null, or undefined`);
|
16
|
+
return value;
|
17
|
+
};
|
18
|
+
export const swap = (array, indexA, indexB) => {
|
19
|
+
const itemA = array[indexA];
|
20
|
+
const itemB = array[indexB];
|
21
|
+
const hasItemA = indexA in array;
|
22
|
+
const hasItemB = indexB in array;
|
23
|
+
// If we're dealing with a sparse array (i.e. one of the indeces doesn't exist),
|
24
|
+
// we should keep it sparse
|
25
|
+
if (hasItemA) {
|
26
|
+
array[indexB] = itemA;
|
27
|
+
}
|
28
|
+
else {
|
29
|
+
delete array[indexB];
|
30
|
+
}
|
31
|
+
if (hasItemB) {
|
32
|
+
array[indexA] = itemB;
|
33
|
+
}
|
34
|
+
else {
|
35
|
+
delete array[indexA];
|
36
|
+
}
|
37
|
+
};
|
38
|
+
// A splice that can handle sparse arrays
|
39
|
+
function sparseSplice(array, start, deleteCount, item) {
|
40
|
+
// Inserting an item into an array won't behave as we need it to if the array isn't
|
41
|
+
// at least as long as the start index. We can force the array to be long enough like this.
|
42
|
+
if (array.length < start && item) {
|
43
|
+
array.length = start;
|
44
|
+
}
|
45
|
+
// If we just pass item in, it'll be undefined and splice will delete the item.
|
46
|
+
if (arguments.length === 4)
|
47
|
+
return array.splice(start, deleteCount, item);
|
48
|
+
return array.splice(start, deleteCount);
|
49
|
+
}
|
50
|
+
export const move = (array, from, to) => {
|
51
|
+
const [item] = sparseSplice(array, from, 1);
|
52
|
+
sparseSplice(array, to, 0, item);
|
53
|
+
};
|
54
|
+
export const insert = (array, index, value) => {
|
55
|
+
sparseSplice(array, index, 0, value);
|
56
|
+
};
|
57
|
+
export const remove = (array, index) => {
|
58
|
+
sparseSplice(array, index, 1);
|
59
|
+
};
|
60
|
+
export const replace = (array, index, value) => {
|
61
|
+
sparseSplice(array, index, 1, value);
|
62
|
+
};
|
63
|
+
/**
|
64
|
+
* The purpose of this helper is to make it easier to update `fieldErrors` and `touchedFields`.
|
65
|
+
* We key those objects by full paths to the fields.
|
66
|
+
* When we're doing array mutations, that makes it difficult to update those objects.
|
67
|
+
*/
|
68
|
+
export const mutateAsArray = (field, obj, mutate) => {
|
69
|
+
const beforeKeys = new Set();
|
70
|
+
const arr = [];
|
71
|
+
for (const [key, value] of Object.entries(obj)) {
|
72
|
+
if (key.startsWith(field) && key !== field) {
|
73
|
+
beforeKeys.add(key);
|
74
|
+
}
|
75
|
+
lodashSet(arr, key.substring(field.length), value);
|
76
|
+
}
|
77
|
+
mutate(arr);
|
78
|
+
for (const key of beforeKeys) {
|
79
|
+
delete obj[key];
|
80
|
+
}
|
81
|
+
const newKeys = getDeepArrayPaths(arr);
|
82
|
+
for (const key of newKeys) {
|
83
|
+
const val = lodashGet(arr, key);
|
84
|
+
obj[`${field}${key}`] = val;
|
85
|
+
}
|
86
|
+
};
|
87
|
+
const getDeepArrayPaths = (obj, basePath = "") => {
|
88
|
+
// This only needs to handle arrays and plain objects
|
89
|
+
// and we can assume the first call is always an array.
|
90
|
+
if (Array.isArray(obj)) {
|
91
|
+
return obj.flatMap((item, index) => getDeepArrayPaths(item, `${basePath}[${index}]`));
|
92
|
+
}
|
93
|
+
if (typeof obj === "object") {
|
94
|
+
return Object.keys(obj).flatMap((key) => getDeepArrayPaths(obj[key], `${basePath}.${key}`));
|
95
|
+
}
|
96
|
+
return [basePath];
|
97
|
+
};
|
98
|
+
if (import.meta.vitest) {
|
99
|
+
const { describe, expect, it } = import.meta.vitest;
|
100
|
+
// Count the actual number of items in the array
|
101
|
+
// instead of just getting the length.
|
102
|
+
// This is useful for validating that sparse arrays are handled correctly.
|
103
|
+
const countArrayItems = (arr) => {
|
104
|
+
let count = 0;
|
105
|
+
arr.forEach(() => count++);
|
106
|
+
return count;
|
107
|
+
};
|
108
|
+
describe("getArray", () => {
|
109
|
+
it("shoud get a deeply nested array that can be mutated to update the nested value", () => {
|
110
|
+
const values = {
|
111
|
+
d: [
|
112
|
+
{ foo: "bar", baz: [true, false] },
|
113
|
+
{ e: true, f: "hi" },
|
114
|
+
],
|
115
|
+
};
|
116
|
+
const result = getArray(values, "d[0].baz");
|
117
|
+
const finalValues = {
|
118
|
+
d: [
|
119
|
+
{ foo: "bar", baz: [true, false, true] },
|
120
|
+
{ e: true, f: "hi" },
|
121
|
+
],
|
122
|
+
};
|
123
|
+
expect(result).toEqual([true, false]);
|
124
|
+
result.push(true);
|
125
|
+
expect(values).toEqual(finalValues);
|
126
|
+
});
|
127
|
+
it("should return an empty array that can be mutated if result is null or undefined", () => {
|
128
|
+
const values = {};
|
129
|
+
const result = getArray(values, "a.foo[0].bar");
|
130
|
+
const finalValues = {
|
131
|
+
a: { foo: [{ bar: ["Bob ross"] }] },
|
132
|
+
};
|
133
|
+
expect(result).toEqual([]);
|
134
|
+
result.push("Bob ross");
|
135
|
+
expect(values).toEqual(finalValues);
|
136
|
+
});
|
137
|
+
it("should throw if the value is defined and not an array", () => {
|
138
|
+
const values = { foo: "foo" };
|
139
|
+
expect(() => getArray(values, "foo")).toThrow();
|
140
|
+
});
|
141
|
+
});
|
142
|
+
describe("swap", () => {
|
143
|
+
it("should swap two items", () => {
|
144
|
+
const array = [1, 2, 3];
|
145
|
+
swap(array, 0, 1);
|
146
|
+
expect(array).toEqual([2, 1, 3]);
|
147
|
+
});
|
148
|
+
it("should work for sparse arrays", () => {
|
149
|
+
// A bit of a sanity check for native array behavior
|
150
|
+
const arr = [];
|
151
|
+
arr[0] = true;
|
152
|
+
swap(arr, 0, 2);
|
153
|
+
expect(countArrayItems(arr)).toEqual(1);
|
154
|
+
expect(0 in arr).toBe(false);
|
155
|
+
expect(2 in arr).toBe(true);
|
156
|
+
expect(arr[2]).toEqual(true);
|
157
|
+
});
|
158
|
+
});
|
159
|
+
describe("move", () => {
|
160
|
+
it("should move an item to a new index", () => {
|
161
|
+
const array = [1, 2, 3];
|
162
|
+
move(array, 0, 1);
|
163
|
+
expect(array).toEqual([2, 1, 3]);
|
164
|
+
});
|
165
|
+
it("should work with sparse arrays", () => {
|
166
|
+
const array = [1];
|
167
|
+
move(array, 0, 2);
|
168
|
+
expect(countArrayItems(array)).toEqual(1);
|
169
|
+
expect(array).toEqual([undefined, undefined, 1]);
|
170
|
+
});
|
171
|
+
});
|
172
|
+
describe("insert", () => {
|
173
|
+
it("should insert an item at a new index", () => {
|
174
|
+
const array = [1, 2, 3];
|
175
|
+
insert(array, 1, 4);
|
176
|
+
expect(array).toEqual([1, 4, 2, 3]);
|
177
|
+
});
|
178
|
+
it("should be able to insert falsey values", () => {
|
179
|
+
const array = [1, 2, 3];
|
180
|
+
insert(array, 1, null);
|
181
|
+
expect(array).toEqual([1, null, 2, 3]);
|
182
|
+
});
|
183
|
+
it("should handle sparse arrays", () => {
|
184
|
+
const array = [];
|
185
|
+
array[2] = true;
|
186
|
+
insert(array, 0, true);
|
187
|
+
expect(countArrayItems(array)).toEqual(2);
|
188
|
+
expect(array).toEqual([true, undefined, undefined, true]);
|
189
|
+
});
|
190
|
+
});
|
191
|
+
describe("remove", () => {
|
192
|
+
it("should remove an item at a given index", () => {
|
193
|
+
const array = [1, 2, 3];
|
194
|
+
remove(array, 1);
|
195
|
+
expect(array).toEqual([1, 3]);
|
196
|
+
});
|
197
|
+
it("should handle sparse arrays", () => {
|
198
|
+
const array = [];
|
199
|
+
array[2] = true;
|
200
|
+
remove(array, 0);
|
201
|
+
expect(countArrayItems(array)).toEqual(1);
|
202
|
+
expect(array).toEqual([undefined, true]);
|
203
|
+
});
|
204
|
+
});
|
205
|
+
describe("replace", () => {
|
206
|
+
it("should replace an item at a given index", () => {
|
207
|
+
const array = [1, 2, 3];
|
208
|
+
replace(array, 1, 4);
|
209
|
+
expect(array).toEqual([1, 4, 3]);
|
210
|
+
});
|
211
|
+
it("should handle sparse arrays", () => {
|
212
|
+
const array = [];
|
213
|
+
array[2] = true;
|
214
|
+
replace(array, 0, true);
|
215
|
+
expect(countArrayItems(array)).toEqual(2);
|
216
|
+
expect(array).toEqual([true, undefined, true]);
|
217
|
+
});
|
218
|
+
});
|
219
|
+
describe("mutateAsArray", () => {
|
220
|
+
it("should handle swap", () => {
|
221
|
+
const values = {
|
222
|
+
myField: "something",
|
223
|
+
"myField[0]": "foo",
|
224
|
+
"myField[2]": "bar",
|
225
|
+
otherField: "baz",
|
226
|
+
"otherField[0]": "something else",
|
227
|
+
};
|
228
|
+
mutateAsArray("myField", values, (arr) => {
|
229
|
+
swap(arr, 0, 2);
|
230
|
+
});
|
231
|
+
expect(values).toEqual({
|
232
|
+
myField: "something",
|
233
|
+
"myField[0]": "bar",
|
234
|
+
"myField[2]": "foo",
|
235
|
+
otherField: "baz",
|
236
|
+
"otherField[0]": "something else",
|
237
|
+
});
|
238
|
+
});
|
239
|
+
it("should swap sparse arrays", () => {
|
240
|
+
const values = {
|
241
|
+
myField: "something",
|
242
|
+
"myField[0]": "foo",
|
243
|
+
otherField: "baz",
|
244
|
+
"otherField[0]": "something else",
|
245
|
+
};
|
246
|
+
mutateAsArray("myField", values, (arr) => {
|
247
|
+
swap(arr, 0, 2);
|
248
|
+
});
|
249
|
+
expect(values).toEqual({
|
250
|
+
myField: "something",
|
251
|
+
"myField[2]": "foo",
|
252
|
+
otherField: "baz",
|
253
|
+
"otherField[0]": "something else",
|
254
|
+
});
|
255
|
+
});
|
256
|
+
it("should handle arrays with nested values", () => {
|
257
|
+
const values = {
|
258
|
+
myField: "something",
|
259
|
+
"myField[0].title": "foo",
|
260
|
+
"myField[0].note": "bar",
|
261
|
+
"myField[2].title": "other",
|
262
|
+
"myField[2].note": "other",
|
263
|
+
otherField: "baz",
|
264
|
+
"otherField[0]": "something else",
|
265
|
+
};
|
266
|
+
mutateAsArray("myField", values, (arr) => {
|
267
|
+
swap(arr, 0, 2);
|
268
|
+
});
|
269
|
+
expect(values).toEqual({
|
270
|
+
myField: "something",
|
271
|
+
"myField[0].title": "other",
|
272
|
+
"myField[0].note": "other",
|
273
|
+
"myField[2].title": "foo",
|
274
|
+
"myField[2].note": "bar",
|
275
|
+
otherField: "baz",
|
276
|
+
"otherField[0]": "something else",
|
277
|
+
});
|
278
|
+
});
|
279
|
+
it("should handle move", () => {
|
280
|
+
const values = {
|
281
|
+
myField: "something",
|
282
|
+
"myField[0]": "foo",
|
283
|
+
"myField[1]": "bar",
|
284
|
+
"myField[2]": "baz",
|
285
|
+
"otherField[0]": "something else",
|
286
|
+
};
|
287
|
+
mutateAsArray("myField", values, (arr) => {
|
288
|
+
move(arr, 0, 2);
|
289
|
+
});
|
290
|
+
expect(values).toEqual({
|
291
|
+
myField: "something",
|
292
|
+
"myField[0]": "bar",
|
293
|
+
"myField[1]": "baz",
|
294
|
+
"myField[2]": "foo",
|
295
|
+
"otherField[0]": "something else",
|
296
|
+
});
|
297
|
+
});
|
298
|
+
it("should handle remove", () => {
|
299
|
+
const values = {
|
300
|
+
myField: "something",
|
301
|
+
"myField[0]": "foo",
|
302
|
+
"myField[1]": "bar",
|
303
|
+
"myField[2]": "baz",
|
304
|
+
"otherField[0]": "something else",
|
305
|
+
};
|
306
|
+
mutateAsArray("myField", values, (arr) => {
|
307
|
+
remove(arr, 1);
|
308
|
+
});
|
309
|
+
expect(values).toEqual({
|
310
|
+
myField: "something",
|
311
|
+
"myField[0]": "foo",
|
312
|
+
"myField[1]": "baz",
|
313
|
+
"otherField[0]": "something else",
|
314
|
+
});
|
315
|
+
expect("myField[2]" in values).toBe(false);
|
316
|
+
});
|
317
|
+
});
|
318
|
+
describe("getDeepArrayPaths", () => {
|
319
|
+
it("should return all paths recursively", () => {
|
320
|
+
const obj = [
|
321
|
+
true,
|
322
|
+
true,
|
323
|
+
[true, true],
|
324
|
+
{ foo: true, bar: { baz: true, test: [true] } },
|
325
|
+
];
|
326
|
+
expect(getDeepArrayPaths(obj, "myField")).toEqual([
|
327
|
+
"myField[0]",
|
328
|
+
"myField[1]",
|
329
|
+
"myField[2][0]",
|
330
|
+
"myField[2][1]",
|
331
|
+
"myField[3].foo",
|
332
|
+
"myField[3].bar.baz",
|
333
|
+
"myField[3].bar.test[0]",
|
334
|
+
]);
|
335
|
+
});
|
336
|
+
});
|
337
|
+
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { WritableDraft } from "immer/dist/internal";
|
2
|
-
import { FieldErrors, TouchedFields, Validator } from "../../validation/types";
|
2
|
+
import { FieldErrors, TouchedFields, ValidationResult, Validator } from "../../validation/types";
|
3
3
|
import { InternalFormId } from "./types";
|
4
4
|
export declare type SyncedFormProps = {
|
5
5
|
formId?: string;
|
@@ -39,8 +39,10 @@ export declare type FormState = {
|
|
39
39
|
setHydrated: () => void;
|
40
40
|
setFormElement: (formElement: HTMLFormElement | null) => void;
|
41
41
|
validateField: (fieldName: string) => Promise<string | null>;
|
42
|
-
validate: () => Promise<
|
42
|
+
validate: () => Promise<ValidationResult<unknown>>;
|
43
43
|
resetFormElement: () => void;
|
44
|
+
submit: () => void;
|
45
|
+
getValues: () => FormData;
|
44
46
|
};
|
45
47
|
export declare const useRootFormStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<FormStoreState>, "setState"> & {
|
46
48
|
setState(nextStateOrUpdater: FormStoreState | Partial<FormStoreState> | ((state: WritableDraft<FormStoreState>) => void), shouldReplace?: boolean | undefined): void;
|
@@ -22,8 +22,14 @@ const defaultFormState = {
|
|
22
22
|
setHydrated: noOp,
|
23
23
|
setFormElement: noOp,
|
24
24
|
validateField: async () => null,
|
25
|
-
validate: async () => {
|
25
|
+
validate: async () => {
|
26
|
+
throw new Error("Validate called before form was initialized.");
|
27
|
+
},
|
28
|
+
submit: async () => {
|
29
|
+
throw new Error("Submit called before form was initialized.");
|
30
|
+
},
|
26
31
|
resetFormElement: noOp,
|
32
|
+
getValues: () => new FormData(),
|
27
33
|
};
|
28
34
|
const createFormState = (formId, set, get) => ({
|
29
35
|
// It's not "hydrated" until the form props are synced
|
@@ -99,9 +105,20 @@ const createFormState = (formId, set, get) => ({
|
|
99
105
|
invariant(formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form.");
|
100
106
|
const validator = (_a = get().formProps) === null || _a === void 0 ? void 0 : _a.validator;
|
101
107
|
invariant(validator, "Cannot validator. This is probably a bug in remix-validated-form.");
|
102
|
-
const
|
103
|
-
if (error)
|
104
|
-
get().setFieldErrors(error.fieldErrors);
|
108
|
+
const result = await validator.validate(new FormData(formElement));
|
109
|
+
if (result.error)
|
110
|
+
get().setFieldErrors(result.error.fieldErrors);
|
111
|
+
return result;
|
112
|
+
},
|
113
|
+
submit: () => {
|
114
|
+
const formElement = get().formElement;
|
115
|
+
invariant(formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form.");
|
116
|
+
formElement.submit();
|
117
|
+
},
|
118
|
+
getValues: () => {
|
119
|
+
const formElement = get().formElement;
|
120
|
+
invariant(formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form.");
|
121
|
+
return new FormData(formElement);
|
105
122
|
},
|
106
123
|
resetFormElement: () => { var _a; return (_a = get().formElement) === null || _a === void 0 ? void 0 : _a.reset(); },
|
107
124
|
});
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import React from "react";
|
2
|
+
export declare type FieldArrayValidationBehavior = "onChange" | "onSubmit";
|
3
|
+
export declare type FieldArrayValidationBehaviorOptions = {
|
4
|
+
initial: FieldArrayValidationBehavior;
|
5
|
+
whenSubmitted: FieldArrayValidationBehavior;
|
6
|
+
};
|
7
|
+
export declare type FieldArrayHelpers<Item = any> = {
|
8
|
+
push: (item: Item) => void;
|
9
|
+
swap: (indexA: number, indexB: number) => void;
|
10
|
+
move: (from: number, to: number) => void;
|
11
|
+
insert: (index: number, value: Item) => void;
|
12
|
+
unshift: (value: Item) => void;
|
13
|
+
remove: (index: number) => void;
|
14
|
+
pop: () => void;
|
15
|
+
replace: (index: number, value: Item) => void;
|
16
|
+
};
|
17
|
+
export declare type UseFieldArrayOptions = {
|
18
|
+
formId?: string;
|
19
|
+
validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>;
|
20
|
+
};
|
21
|
+
export declare function useFieldArray<Item = any>(name: string, { formId, validationBehavior }?: UseFieldArrayOptions): [itemDefaults: Item[], helpers: FieldArrayHelpers<any>, error: string | undefined];
|
22
|
+
export declare type FieldArrayProps = {
|
23
|
+
name: string;
|
24
|
+
children: (itemDefaults: any[], helpers: FieldArrayHelpers, error: string | undefined) => React.ReactNode;
|
25
|
+
formId?: string;
|
26
|
+
validationBehavior?: FieldArrayValidationBehaviorOptions;
|
27
|
+
};
|
28
|
+
export declare const FieldArray: ({ name, children, formId, validationBehavior, }: FieldArrayProps) => React.ReactNode;
|