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.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/browser/ValidatedForm.js +10 -32
  3. package/browser/internal/hooks.d.ts +3 -1
  4. package/browser/internal/hooks.js +2 -0
  5. package/browser/internal/logic/nestedObjectToPathObject.d.ts +1 -0
  6. package/browser/internal/logic/nestedObjectToPathObject.js +47 -0
  7. package/browser/internal/state/arrayUtil.d.ts +12 -0
  8. package/browser/internal/state/arrayUtil.js +337 -0
  9. package/browser/internal/state/createFormStore.d.ts +4 -2
  10. package/browser/internal/state/createFormStore.js +21 -4
  11. package/browser/internal/state/fieldArray.d.ts +28 -0
  12. package/browser/internal/state/fieldArray.js +73 -0
  13. package/browser/internal/state/types.d.ts +0 -0
  14. package/browser/internal/state/types.js +0 -0
  15. package/browser/unreleased/formStateHooks.d.ts +12 -2
  16. package/browser/unreleased/formStateHooks.js +15 -2
  17. package/browser/userFacingFormContext.d.ts +12 -2
  18. package/browser/userFacingFormContext.js +5 -1
  19. package/dist/remix-validated-form.cjs.js +3 -3
  20. package/dist/remix-validated-form.cjs.js.map +1 -1
  21. package/dist/remix-validated-form.es.js +55 -37
  22. package/dist/remix-validated-form.es.js.map +1 -1
  23. package/dist/remix-validated-form.umd.js +3 -3
  24. package/dist/remix-validated-form.umd.js.map +1 -1
  25. package/dist/types/internal/hooks.d.ts +3 -1
  26. package/dist/types/internal/state/createFormStore.d.ts +4 -2
  27. package/dist/types/unreleased/formStateHooks.d.ts +12 -2
  28. package/dist/types/userFacingFormContext.d.ts +12 -2
  29. package/package.json +1 -1
  30. package/src/ValidatedForm.tsx +24 -42
  31. package/src/internal/hooks.ts +6 -0
  32. package/src/internal/state/createFormStore.ts +40 -6
  33. package/src/unreleased/formStateHooks.ts +32 -3
  34. package/src/userFacingFormContext.ts +22 -2
  35. package/src/validation/validation.test.ts +7 -7
@@ -3,15 +3,15 @@
3
3
  transforming...
4
4
  ✓ 319 modules transformed.
5
5
  rendering chunks...
6
- dist/remix-validated-form.cjs.js  45.13 KiB / gzip: 17.03 KiB
7
- dist/remix-validated-form.cjs.js.map 250.93 KiB
8
- dist/remix-validated-form.es.js  100.56 KiB / gzip: 23.71 KiB
9
- dist/remix-validated-form.es.js.map 258.79 KiB
10
- dist/remix-validated-form.umd.js  45.38 KiB / gzip: 17.15 KiB
11
- dist/remix-validated-form.umd.js.map 250.91 KiB
6
+ dist/remix-validated-form.cjs.js  45.46 KiB / gzip: 17.05 KiB
7
+ dist/remix-validated-form.cjs.js.map 252.62 KiB
8
+ dist/remix-validated-form.es.js  101.10 KiB / gzip: 23.71 KiB
9
+ dist/remix-validated-form.es.js.map 260.60 KiB
10
+ dist/remix-validated-form.umd.js  45.71 KiB / gzip: 17.16 KiB
11
+ dist/remix-validated-form.umd.js.map 252.60 KiB
12
12
  
13
13
  [vite:dts] Start generate declaration files...
14
- [vite:dts] Declaration files built in 1725ms.
14
+ [vite:dts] Declaration files built in 2049ms.
15
15
  
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'
@@ -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 React, { useCallback, useEffect, useMemo, useRef, useState, } from "react";
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 HTMLInputElement)
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 HTMLInputElement) {
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
- let clickedButtonRef = React.useRef();
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 = e.nativeEvent
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 || e.currentTarget, { method, replace });
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: _jsxs(InternalFormContext.Provider, { value: contextValue, 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));
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<void>;
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<void>;
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 { error } = await validator.validate(new FormData(formElement));
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;