remix-validated-form 4.5.0-beta.0 → 4.5.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.
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;