remix-validated-form 4.5.0 → 4.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/browser/ValidatedForm.js +4 -4
  3. package/browser/internal/hooks.d.ts +1 -0
  4. package/browser/internal/hooks.js +1 -0
  5. package/browser/internal/logic/nestedObjectToPathObject.d.ts +0 -0
  6. package/browser/internal/logic/nestedObjectToPathObject.js +0 -0
  7. package/browser/internal/state/arrayUtil.d.ts +6 -0
  8. package/browser/internal/state/arrayUtil.js +236 -7
  9. package/browser/internal/state/createFormStore.d.ts +1 -0
  10. package/browser/internal/state/createFormStore.js +6 -0
  11. package/browser/internal/state/fieldArray.d.ts +18 -11
  12. package/browser/internal/state/fieldArray.js +44 -21
  13. package/browser/unreleased/formStateHooks.d.ts +4 -0
  14. package/browser/unreleased/formStateHooks.js +4 -1
  15. package/browser/userFacingFormContext.d.ts +4 -0
  16. package/browser/userFacingFormContext.js +3 -1
  17. package/dist/remix-validated-form.cjs.js +2 -2
  18. package/dist/remix-validated-form.cjs.js.map +1 -1
  19. package/dist/remix-validated-form.es.js +23 -10
  20. package/dist/remix-validated-form.es.js.map +1 -1
  21. package/dist/remix-validated-form.umd.js +2 -2
  22. package/dist/remix-validated-form.umd.js.map +1 -1
  23. package/dist/types/internal/hooks.d.ts +1 -0
  24. package/dist/types/internal/state/createFormStore.d.ts +1 -0
  25. package/dist/types/unreleased/formStateHooks.d.ts +4 -0
  26. package/dist/types/userFacingFormContext.d.ts +4 -0
  27. package/package.json +1 -1
  28. package/src/ValidatedForm.tsx +5 -4
  29. package/src/internal/hooks.ts +3 -0
  30. package/src/internal/state/createFormStore.ts +12 -1
  31. package/src/unreleased/formStateHooks.ts +8 -0
  32. package/src/userFacingFormContext.ts +7 -0
  33. package/src/validation/validation.test.ts +7 -7
@@ -1,17 +1,17 @@
1
1
  $ vite build
2
2
  vite v2.9.5 building for production...
3
3
  transforming...
4
- ✓ 320 modules transformed.
4
+ ✓ 319 modules transformed.
5
5
  rendering chunks...
6
- dist/remix-validated-form.cjs.js  47.23 KiB / gzip: 17.41 KiB
7
- dist/remix-validated-form.cjs.js.map 265.34 KiB
8
- dist/remix-validated-form.es.js  104.23 KiB / gzip: 24.22 KiB
9
- dist/remix-validated-form.es.js.map 273.50 KiB
10
- dist/remix-validated-form.umd.js  47.48 KiB / gzip: 17.53 KiB
11
- dist/remix-validated-form.umd.js.map 265.31 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 1961ms.
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'
@@ -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();
@@ -189,7 +189,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
189
189
  if (fetcher)
190
190
  fetcher.submit(submitter || e.currentTarget);
191
191
  else
192
- submit(submitter || target, { method, replace });
192
+ submit(submitter || target, { replace });
193
193
  }
194
194
  };
195
195
  return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
@@ -31,3 +31,4 @@ export declare const useResetFormElement: (formId: InternalFormId) => () => void
31
31
  export declare const useSubmitForm: (formId: InternalFormId) => () => void;
32
32
  export declare const useFormActionProp: (formId: InternalFormId) => string | undefined;
33
33
  export declare const useFormSubactionProp: (formId: InternalFormId) => string | undefined;
34
+ export declare const useFormValues: (formId: InternalFormId) => () => FormData;
@@ -116,3 +116,4 @@ export const useResetFormElement = (formId) => useFormStore(formId, (state) => s
116
116
  export const useSubmitForm = (formId) => useFormStore(formId, (state) => state.submit);
117
117
  export const useFormActionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.action; });
118
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);
@@ -4,3 +4,9 @@ export declare const move: (array: unknown[], from: number, to: number) => void;
4
4
  export declare const insert: (array: unknown[], index: number, value: unknown) => void;
5
5
  export declare const remove: (array: unknown[], index: number) => void;
6
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;
@@ -18,24 +18,93 @@ export const getArray = (values, field) => {
18
18
  export const swap = (array, indexA, indexB) => {
19
19
  const itemA = array[indexA];
20
20
  const itemB = array[indexB];
21
- array[indexA] = itemB;
22
- array[indexB] = itemA;
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
+ }
23
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
+ }
24
50
  export const move = (array, from, to) => {
25
- const [item] = array.splice(from, 1);
26
- array.splice(to, 0, item);
51
+ const [item] = sparseSplice(array, from, 1);
52
+ sparseSplice(array, to, 0, item);
27
53
  };
28
54
  export const insert = (array, index, value) => {
29
- array.splice(index, 0, value);
55
+ sparseSplice(array, index, 0, value);
30
56
  };
31
57
  export const remove = (array, index) => {
32
- array.splice(index, 1);
58
+ sparseSplice(array, index, 1);
33
59
  };
34
60
  export const replace = (array, index, value) => {
35
- array.splice(index, 1, 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];
36
97
  };
37
98
  if (import.meta.vitest) {
38
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
+ };
39
108
  describe("getArray", () => {
40
109
  it("shoud get a deeply nested array that can be mutated to update the nested value", () => {
41
110
  const values = {
@@ -76,6 +145,16 @@ if (import.meta.vitest) {
76
145
  swap(array, 0, 1);
77
146
  expect(array).toEqual([2, 1, 3]);
78
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
+ });
79
158
  });
80
159
  describe("move", () => {
81
160
  it("should move an item to a new index", () => {
@@ -83,6 +162,12 @@ if (import.meta.vitest) {
83
162
  move(array, 0, 1);
84
163
  expect(array).toEqual([2, 1, 3]);
85
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
+ });
86
171
  });
87
172
  describe("insert", () => {
88
173
  it("should insert an item at a new index", () => {
@@ -90,6 +175,18 @@ if (import.meta.vitest) {
90
175
  insert(array, 1, 4);
91
176
  expect(array).toEqual([1, 4, 2, 3]);
92
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
+ });
93
190
  });
94
191
  describe("remove", () => {
95
192
  it("should remove an item at a given index", () => {
@@ -97,6 +194,13 @@ if (import.meta.vitest) {
97
194
  remove(array, 1);
98
195
  expect(array).toEqual([1, 3]);
99
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
+ });
100
204
  });
101
205
  describe("replace", () => {
102
206
  it("should replace an item at a given index", () => {
@@ -104,5 +208,130 @@ if (import.meta.vitest) {
104
208
  replace(array, 1, 4);
105
209
  expect(array).toEqual([1, 4, 3]);
106
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
+ });
107
336
  });
108
337
  }
@@ -42,6 +42,7 @@ export declare type FormState = {
42
42
  validate: () => Promise<ValidationResult<unknown>>;
43
43
  resetFormElement: () => void;
44
44
  submit: () => void;
45
+ getValues: () => FormData;
45
46
  };
46
47
  export declare const useRootFormStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<FormStoreState>, "setState"> & {
47
48
  setState(nextStateOrUpdater: FormStoreState | Partial<FormStoreState> | ((state: WritableDraft<FormStoreState>) => void), shouldReplace?: boolean | undefined): void;
@@ -29,6 +29,7 @@ const defaultFormState = {
29
29
  throw new Error("Submit called before form was initialized.");
30
30
  },
31
31
  resetFormElement: noOp,
32
+ getValues: () => new FormData(),
32
33
  };
33
34
  const createFormState = (formId, set, get) => ({
34
35
  // It's not "hydrated" until the form props are synced
@@ -114,6 +115,11 @@ const createFormState = (formId, set, get) => ({
114
115
  invariant(formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form.");
115
116
  formElement.submit();
116
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);
122
+ },
117
123
  resetFormElement: () => { var _a; return (_a = get().formElement) === null || _a === void 0 ? void 0 : _a.reset(); },
118
124
  });
119
125
  export const useRootFormStore = create()(immer((set, get) => ({
@@ -1,21 +1,28 @@
1
1
  import React from "react";
2
- export declare const FieldArrayContext: React.Context<{
3
- defaultValues: any[];
4
- name: string;
5
- } | null>;
6
- export declare type FieldArrayHelpers = {
7
- push: (item: any) => void;
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;
8
9
  swap: (indexA: number, indexB: number) => void;
9
10
  move: (from: number, to: number) => void;
10
- insert: (index: number, value: any) => void;
11
- unshift: () => void;
11
+ insert: (index: number, value: Item) => void;
12
+ unshift: (value: Item) => void;
12
13
  remove: (index: number) => void;
13
14
  pop: () => void;
14
- replace: (index: number, value: any) => void;
15
+ replace: (index: number, value: Item) => void;
16
+ };
17
+ export declare type UseFieldArrayOptions = {
18
+ formId?: string;
19
+ validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>;
15
20
  };
21
+ export declare function useFieldArray<Item = any>(name: string, { formId, validationBehavior }?: UseFieldArrayOptions): [itemDefaults: Item[], helpers: FieldArrayHelpers<any>, error: string | undefined];
16
22
  export declare type FieldArrayProps = {
17
23
  name: string;
18
- children: (itemDefaults: any[], helpers: FieldArrayHelpers) => React.ReactNode;
24
+ children: (itemDefaults: any[], helpers: FieldArrayHelpers, error: string | undefined) => React.ReactNode;
19
25
  formId?: string;
26
+ validationBehavior?: FieldArrayValidationBehaviorOptions;
20
27
  };
21
- export declare const FieldArray: ({ name, children, formId }: FieldArrayProps) => JSX.Element;
28
+ export declare const FieldArray: ({ name, children, formId, validationBehavior, }: FieldArrayProps) => React.ReactNode;
@@ -1,50 +1,73 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useMemo, createContext } from "react";
1
+ import { useMemo } from "react";
2
+ import { useCallback } from "react";
3
3
  import invariant from "tiny-invariant";
4
- import { useInternalFormContext } from "../hooks";
5
- import { useControllableValue } from "./controlledFields";
4
+ import { useFieldDefaultValue, useFieldError, useInternalFormContext, useInternalHasBeenSubmitted, useValidateField, } from "../hooks";
5
+ import { useRegisterControlledField } from "./controlledFields";
6
6
  import { useFormStore } from "./storeHooks";
7
- const useFieldArray = (context, field) => {
8
- // TODO: Fieldarrays need to handle/update these things, too:
9
- // - touchedFields & fieldErrors should be updated when fields are added/removed
10
- // - Could probably move some of these callbacks into the store
11
- // - There's a bug where adding a new field to the fieldarray validates the new field.
12
- // - For some reason this only happens in the test-app, but not in the docs app.
13
- const [value] = useControllableValue(context, field);
7
+ const useInternalFieldArray = (context, field, validationBehavior) => {
8
+ const value = useFieldDefaultValue(field, context);
9
+ useRegisterControlledField(context, field);
10
+ const hasBeenSubmitted = useInternalHasBeenSubmitted(context.formId);
11
+ const validateField = useValidateField(context.formId);
12
+ const error = useFieldError(field, context);
13
+ const resolvedValidationBehavior = {
14
+ initial: "onSubmit",
15
+ whenSubmitted: "onChange",
16
+ ...validationBehavior,
17
+ };
18
+ const behavior = hasBeenSubmitted
19
+ ? resolvedValidationBehavior.whenSubmitted
20
+ : resolvedValidationBehavior.initial;
21
+ const maybeValidate = useCallback(() => {
22
+ if (behavior === "onChange") {
23
+ validateField(field);
24
+ }
25
+ }, [behavior, field, validateField]);
26
+ invariant(value === undefined || value === null || Array.isArray(value), `FieldArray: defaultValue value for ${field} must be an array, null, or undefined`);
14
27
  const arr = useFormStore(context.formId, (state) => state.controlledFields.array);
15
28
  const helpers = useMemo(() => ({
16
29
  push: (item) => {
17
30
  arr.push(field, item);
31
+ maybeValidate();
18
32
  },
19
33
  swap: (indexA, indexB) => {
20
34
  arr.swap(field, indexA, indexB);
35
+ maybeValidate();
21
36
  },
22
37
  move: (from, to) => {
23
38
  arr.move(field, from, to);
39
+ maybeValidate();
24
40
  },
25
41
  insert: (index, value) => {
26
42
  arr.insert(field, index, value);
43
+ maybeValidate();
27
44
  },
28
- unshift: () => {
29
- arr.unshift(field);
45
+ unshift: (value) => {
46
+ arr.unshift(field, value);
47
+ maybeValidate();
30
48
  },
31
49
  remove: (index) => {
32
50
  arr.remove(field, index);
51
+ maybeValidate();
33
52
  },
34
53
  pop: () => {
35
54
  arr.pop(field);
55
+ maybeValidate();
36
56
  },
37
57
  replace: (index, value) => {
38
58
  arr.replace(field, index, value);
59
+ maybeValidate();
39
60
  },
40
- }), [arr, field]);
41
- return [value, helpers];
61
+ }), [arr, field, maybeValidate]);
62
+ const arrayValue = useMemo(() => value !== null && value !== void 0 ? value : [], [value]);
63
+ return [arrayValue, helpers, error];
42
64
  };
43
- export const FieldArrayContext = createContext(null);
44
- export const FieldArray = ({ name, children, formId }) => {
65
+ export function useFieldArray(name, { formId, validationBehavior } = {}) {
45
66
  const context = useInternalFormContext(formId, "FieldArray");
46
- const [value, helpers] = useFieldArray(context, name);
47
- invariant(value === undefined || value === null || Array.isArray(value), `FieldArray: defaultValue value for ${name} must be an array, null, or undefined`);
48
- const contextValue = useMemo(() => ({ defaultValues: value !== null && value !== void 0 ? value : [], name }), [name, value]);
49
- return (_jsx(FieldArrayContext.Provider, { value: contextValue, children: children(contextValue.defaultValues, helpers) }, void 0));
67
+ return useInternalFieldArray(context, name, validationBehavior);
68
+ }
69
+ export const FieldArray = ({ name, children, formId, validationBehavior, }) => {
70
+ const context = useInternalFormContext(formId, "FieldArray");
71
+ const [value, helpers, error] = useInternalFieldArray(context, name, validationBehavior);
72
+ return children(value, helpers, error);
50
73
  };
@@ -51,6 +51,10 @@ export declare type FormHelpers = {
51
51
  * _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
52
52
  */
53
53
  submit: () => void;
54
+ /**
55
+ * Returns the current form values as FormData
56
+ */
57
+ getValues: () => FormData;
54
58
  };
55
59
  /**
56
60
  * Returns helpers that can be used to update the form state.
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import { useInternalFormContext, useClearError, useSetTouched, useDefaultValuesForForm, useFieldErrorsForForm, useInternalIsSubmitting, useInternalHasBeenSubmitted, useTouchedFields, useInternalIsValid, useFieldErrors, useValidateField, useValidate, useSetFieldErrors, useResetFormElement, useSyncedDefaultValues, useFormActionProp, useFormSubactionProp, useSubmitForm, } from "../internal/hooks";
2
+ import { useInternalFormContext, useClearError, useSetTouched, useDefaultValuesForForm, useFieldErrorsForForm, useInternalIsSubmitting, useInternalHasBeenSubmitted, useTouchedFields, useInternalIsValid, useFieldErrors, useValidateField, useValidate, useSetFieldErrors, useResetFormElement, useSyncedDefaultValues, useFormActionProp, useFormSubactionProp, useSubmitForm, useFormValues, } from "../internal/hooks";
3
3
  /**
4
4
  * Returns information about the form.
5
5
  *
@@ -53,6 +53,7 @@ export const useFormHelpers = (formId) => {
53
53
  const setFieldErrors = useSetFieldErrors(formContext.formId);
54
54
  const reset = useResetFormElement(formContext.formId);
55
55
  const submit = useSubmitForm(formContext.formId);
56
+ const getValues = useFormValues(formContext.formId);
56
57
  return useMemo(() => ({
57
58
  setTouched,
58
59
  validateField,
@@ -61,6 +62,7 @@ export const useFormHelpers = (formId) => {
61
62
  clearAllErrors: () => setFieldErrors({}),
62
63
  reset,
63
64
  submit,
65
+ getValues,
64
66
  }), [
65
67
  clearError,
66
68
  reset,
@@ -69,5 +71,6 @@ export const useFormHelpers = (formId) => {
69
71
  submit,
70
72
  validate,
71
73
  validateField,
74
+ getValues,
72
75
  ]);
73
76
  };
@@ -74,6 +74,10 @@ export declare type FormContextValue = {
74
74
  * _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
75
75
  */
76
76
  submit: () => void;
77
+ /**
78
+ * Returns the current form values as FormData
79
+ */
80
+ getValues: () => FormData;
77
81
  };
78
82
  /**
79
83
  * Provides access to some of the internal state of the form.
@@ -8,7 +8,7 @@ export const useFormContext = (formId) => {
8
8
  // Try to access context so we get our error specific to this hook if it's not there
9
9
  const context = useInternalFormContext(formId, "useFormContext");
10
10
  const state = useFormState(formId);
11
- const { clearError: internalClearError, setTouched, validateField, clearAllErrors, validate, reset, submit, } = useFormHelpers(formId);
11
+ const { clearError: internalClearError, setTouched, validateField, clearAllErrors, validate, reset, submit, getValues, } = useFormHelpers(formId);
12
12
  const registerReceiveFocus = useRegisterReceiveFocus(context.formId);
13
13
  const clearError = useCallback((...names) => {
14
14
  names.forEach((name) => {
@@ -25,6 +25,7 @@ export const useFormContext = (formId) => {
25
25
  validate,
26
26
  reset,
27
27
  submit,
28
+ getValues,
28
29
  }), [
29
30
  clearAllErrors,
30
31
  clearError,
@@ -35,5 +36,6 @@ export const useFormContext = (formId) => {
35
36
  submit,
36
37
  validate,
37
38
  validateField,
39
+ getValues,
38
40
  ]);
39
41
  };