remix-validated-form 4.5.0 → 4.5.3

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 (38) hide show
  1. package/.turbo/turbo-build.log +8 -9
  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/server.d.ts +2 -2
  14. package/browser/server.js +1 -1
  15. package/browser/unreleased/formStateHooks.d.ts +4 -0
  16. package/browser/unreleased/formStateHooks.js +4 -1
  17. package/browser/userFacingFormContext.d.ts +4 -0
  18. package/browser/userFacingFormContext.js +3 -1
  19. package/dist/remix-validated-form.cjs.js +12 -3
  20. package/dist/remix-validated-form.cjs.js.map +1 -1
  21. package/dist/remix-validated-form.es.js +45 -11
  22. package/dist/remix-validated-form.es.js.map +1 -1
  23. package/dist/remix-validated-form.umd.js +12 -3
  24. package/dist/remix-validated-form.umd.js.map +1 -1
  25. package/dist/types/internal/hooks.d.ts +1 -0
  26. package/dist/types/internal/state/createFormStore.d.ts +1 -0
  27. package/dist/types/server.d.ts +2 -2
  28. package/dist/types/unreleased/formStateHooks.d.ts +4 -0
  29. package/dist/types/userFacingFormContext.d.ts +4 -0
  30. package/package.json +2 -4
  31. package/src/ValidatedForm.tsx +5 -4
  32. package/src/internal/hooks.ts +3 -0
  33. package/src/internal/state/createFormStore.ts +12 -1
  34. package/src/server.ts +2 -2
  35. package/src/unreleased/formStateHooks.ts +8 -0
  36. package/src/userFacingFormContext.ts +7 -0
  37. package/src/validation/validation.test.ts +7 -7
  38. package/vite.config.ts +1 -1
@@ -1,18 +1,17 @@
1
1
  $ vite build
2
2
  vite v2.9.5 building for production...
3
3
  transforming...
4
- ✓ 320 modules transformed.
4
+ ✓ 482 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.86 KiB / gzip: 17.19 KiB
7
+ dist/remix-validated-form.cjs.js.map 254.97 KiB
8
+ dist/remix-validated-form.es.js  101.66 KiB / gzip: 23.89 KiB
9
+ dist/remix-validated-form.es.js.map 263.01 KiB
10
+ dist/remix-validated-form.umd.js  46.07 KiB / gzip: 17.30 KiB
11
+ dist/remix-validated-form.umd.js.map 254.95 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 2804ms.
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'
18
- No name was provided for external module '@remix-run/server-runtime' in output.globals – guessing 'serverRuntime'
@@ -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
  };
@@ -1,5 +1,5 @@
1
1
  import { FORM_DEFAULTS_FIELD } from "./internal/constants";
2
- import { ValidatorError } from "./validation/types";
2
+ import { ValidatorError, ValidationErrorResponseData } from "./validation/types";
3
3
  /**
4
4
  * Takes the errors from a `Validator` and returns a `Response`.
5
5
  * When you return this from your action, `ValidatedForm` on the frontend will automatically
@@ -14,7 +14,7 @@ import { ValidatorError } from "./validation/types";
14
14
  * if (result.error) return validationError(result.error, result.submittedData);
15
15
  * ```
16
16
  */
17
- export declare function validationError(error: ValidatorError, repopulateFields?: unknown, init?: ResponseInit): Response;
17
+ export declare function validationError(error: ValidatorError, repopulateFields?: unknown, init?: ResponseInit): import("@remix-run/server-runtime").TypedResponse<ValidationErrorResponseData>;
18
18
  export declare type FormDefaults = {
19
19
  [formDefaultsKey: `${typeof FORM_DEFAULTS_FIELD}_${string}`]: any;
20
20
  };
package/browser/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { json } from "@remix-run/server-runtime";
1
+ import { json } from "remix";
2
2
  import { formDefaultValuesKey, } from "./internal/constants";
3
3
  /**
4
4
  * Takes the errors from a `Validator` and returns a `Response`.
@@ -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
  };