remix-validated-form 4.5.2 → 4.6.0-beta.0

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 +0 -3
  3. package/browser/index.d.ts +1 -0
  4. package/browser/index.js +1 -0
  5. package/browser/internal/hooks.d.ts +1 -0
  6. package/browser/internal/hooks.js +3 -4
  7. package/browser/internal/state/controlledFields.d.ts +1 -0
  8. package/browser/internal/state/controlledFields.js +17 -29
  9. package/browser/internal/state/createFormStore.d.ts +31 -1
  10. package/browser/internal/state/createFormStore.js +177 -14
  11. package/browser/server.d.ts +2 -2
  12. package/browser/server.js +1 -1
  13. package/dist/remix-validated-form.cjs.js +12 -3
  14. package/dist/remix-validated-form.cjs.js.map +1 -1
  15. package/dist/remix-validated-form.es.js +361 -131
  16. package/dist/remix-validated-form.es.js.map +1 -1
  17. package/dist/remix-validated-form.umd.js +12 -3
  18. package/dist/remix-validated-form.umd.js.map +1 -1
  19. package/dist/types/index.d.ts +1 -0
  20. package/dist/types/internal/hooks.d.ts +1 -0
  21. package/dist/types/internal/state/arrayUtil.d.ts +12 -0
  22. package/dist/types/internal/state/controlledFields.d.ts +1 -0
  23. package/dist/types/internal/state/createFormStore.d.ts +31 -1
  24. package/dist/types/internal/state/fieldArray.d.ts +28 -0
  25. package/dist/types/server.d.ts +2 -2
  26. package/package.json +1 -3
  27. package/src/ValidatedForm.tsx +0 -3
  28. package/src/index.ts +6 -0
  29. package/src/internal/hooks.ts +9 -4
  30. package/src/internal/logic/nestedObjectToPathObject.ts +63 -0
  31. package/src/internal/state/arrayUtil.ts +399 -0
  32. package/src/internal/state/controlledFields.ts +39 -43
  33. package/src/internal/state/createFormStore.ts +288 -20
  34. package/src/internal/state/fieldArray.tsx +155 -0
  35. package/src/server.ts +1 -1
  36. package/vite.config.ts +1 -1
  37. package/dist/types/internal/state/controlledFieldStore.d.ts +0 -26
  38. package/src/internal/state/controlledFieldStore.ts +0 -112
@@ -1,7 +1,6 @@
1
1
  import { useCallback, useEffect } from "react";
2
2
  import { InternalFormContextValue } from "../formContext";
3
3
  import { useFieldDefaultValue } from "../hooks";
4
- import { useControlledFieldStore } from "./controlledFieldStore";
5
4
  import { useFormStore } from "./storeHooks";
6
5
  import { InternalFormId } from "./types";
7
6
 
@@ -9,63 +8,57 @@ export const useControlledFieldValue = (
9
8
  context: InternalFormContextValue,
10
9
  field: string
11
10
  ) => {
12
- const value = useControlledFieldStore(
13
- (state) => state.getField(context.formId, field)?.value
11
+ const value = useFormStore(context.formId, (state) =>
12
+ state.controlledFields.getValue(field)
14
13
  );
15
-
16
14
  const isFormHydrated = useFormStore(
17
15
  context.formId,
18
16
  (state) => state.isHydrated
19
17
  );
20
18
  const defaultValue = useFieldDefaultValue(field, context);
21
19
 
22
- const isFieldHydrated = useControlledFieldStore(
23
- (state) => state.getField(context.formId, field)?.hydrated ?? false
24
- );
25
- const hydrateWithDefault = useControlledFieldStore(
26
- (state) => state.hydrateWithDefault
27
- );
28
-
29
- useEffect(() => {
30
- if (isFormHydrated && !isFieldHydrated) {
31
- hydrateWithDefault(context.formId, field, defaultValue);
32
- }
33
- }, [
34
- context.formId,
35
- defaultValue,
36
- field,
37
- hydrateWithDefault,
38
- isFieldHydrated,
39
- isFormHydrated,
40
- ]);
41
-
42
- return isFieldHydrated ? value : defaultValue;
20
+ return isFormHydrated ? value : defaultValue;
43
21
  };
44
22
 
45
- export const useControllableValue = (
23
+ export const useRegisterControlledField = (
46
24
  context: InternalFormContextValue,
47
25
  field: string
48
26
  ) => {
49
- const resolveUpdate = useControlledFieldStore(
50
- (state) => state.getField(context.formId, field)?.resolveValueUpdate
27
+ const resolveUpdate = useFormStore(
28
+ context.formId,
29
+ (state) => state.controlledFields.valueUpdateResolvers[field]
51
30
  );
52
31
  useEffect(() => {
53
32
  resolveUpdate?.();
54
33
  }, [resolveUpdate]);
55
34
 
56
- const register = useControlledFieldStore((state) => state.register);
57
- const unregister = useControlledFieldStore((state) => state.unregister);
35
+ const register = useFormStore(
36
+ context.formId,
37
+ (state) => state.controlledFields.register
38
+ );
39
+ const unregister = useFormStore(
40
+ context.formId,
41
+ (state) => state.controlledFields.unregister
42
+ );
58
43
  useEffect(() => {
59
- register(context.formId, field);
60
- return () => unregister(context.formId, field);
44
+ register(field);
45
+ return () => unregister(field);
61
46
  }, [context.formId, field, register, unregister]);
47
+ };
62
48
 
63
- const setControlledFieldValue = useControlledFieldStore(
64
- (state) => state.setValue
49
+ export const useControllableValue = (
50
+ context: InternalFormContextValue,
51
+ field: string
52
+ ) => {
53
+ useRegisterControlledField(context, field);
54
+
55
+ const setControlledFieldValue = useFormStore(
56
+ context.formId,
57
+ (state) => state.controlledFields.setValue
65
58
  );
66
59
  const setValue = useCallback(
67
- (value: unknown) => setControlledFieldValue(context.formId, field, value),
68
- [context.formId, field, setControlledFieldValue]
60
+ (value: unknown) => setControlledFieldValue(field, value),
61
+ [field, setControlledFieldValue]
69
62
  );
70
63
 
71
64
  const value = useControlledFieldValue(context, field);
@@ -74,17 +67,20 @@ export const useControllableValue = (
74
67
  };
75
68
 
76
69
  export const useUpdateControllableValue = (formId: InternalFormId) => {
77
- const setValue = useControlledFieldStore((state) => state.setValue);
70
+ const setValue = useFormStore(
71
+ formId,
72
+ (state) => state.controlledFields.setValue
73
+ );
78
74
  return useCallback(
79
- (field: string, value: unknown) => setValue(formId, field, value),
80
- [formId, setValue]
75
+ (field: string, value: unknown) => setValue(field, value),
76
+ [setValue]
81
77
  );
82
78
  };
83
79
 
84
80
  export const useAwaitValue = (formId: InternalFormId) => {
85
- const awaitValue = useControlledFieldStore((state) => state.awaitValueUpdate);
86
- return useCallback(
87
- (field: string) => awaitValue(formId, field),
88
- [awaitValue, formId]
81
+ const awaitValue = useFormStore(
82
+ formId,
83
+ (state) => state.controlledFields.awaitValueUpdate
89
84
  );
85
+ return useCallback((field: string) => awaitValue(field), [awaitValue]);
90
86
  };
@@ -1,4 +1,6 @@
1
1
  import { WritableDraft } from "immer/dist/internal";
2
+ import lodashGet from "lodash/get";
3
+ import lodashSet from "lodash/set";
2
4
  import invariant from "tiny-invariant";
3
5
  import create, { GetState } from "zustand";
4
6
  import { immer } from "zustand/middleware/immer";
@@ -8,7 +10,7 @@ import {
8
10
  ValidationResult,
9
11
  Validator,
10
12
  } from "../../validation/types";
11
- import { useControlledFieldStore } from "./controlledFieldStore";
13
+ import * as arrayUtil from "./arrayUtil";
12
14
  import { InternalFormId } from "./types";
13
15
 
14
16
  export type SyncedFormProps = {
@@ -35,6 +37,7 @@ export type FormState = {
35
37
  touchedFields: TouchedFields;
36
38
  formProps?: SyncedFormProps;
37
39
  formElement: HTMLFormElement | null;
40
+ currentDefaultValues: Record<string, any>;
38
41
 
39
42
  isValid: () => boolean;
40
43
  startSubmit: () => void;
@@ -45,13 +48,37 @@ export type FormState = {
45
48
  clearFieldError: (field: string) => void;
46
49
  reset: () => void;
47
50
  syncFormProps: (props: SyncedFormProps) => void;
48
- setHydrated: () => void;
49
51
  setFormElement: (formElement: HTMLFormElement | null) => void;
50
52
  validateField: (fieldName: string) => Promise<string | null>;
51
53
  validate: () => Promise<ValidationResult<unknown>>;
52
54
  resetFormElement: () => void;
53
55
  submit: () => void;
54
56
  getValues: () => FormData;
57
+
58
+ controlledFields: {
59
+ values: { [fieldName: string]: any };
60
+ refCounts: { [fieldName: string]: number };
61
+ valueUpdatePromises: { [fieldName: string]: Promise<void> };
62
+ valueUpdateResolvers: { [fieldName: string]: () => void };
63
+
64
+ register: (fieldName: string) => void;
65
+ unregister: (fieldName: string) => void;
66
+ setValue: (fieldName: string, value: unknown) => void;
67
+ kickoffValueUpdate: (fieldName: string) => void;
68
+ getValue: (fieldName: string) => unknown;
69
+ awaitValueUpdate: (fieldName: string) => Promise<void>;
70
+
71
+ array: {
72
+ push: (fieldName: string, value: unknown) => void;
73
+ swap: (fieldName: string, indexA: number, indexB: number) => void;
74
+ move: (fieldName: string, fromIndex: number, toIndex: number) => void;
75
+ insert: (fieldName: string, index: number, value: unknown) => void;
76
+ unshift: (fieldName: string, value: unknown) => void;
77
+ remove: (fieldName: string, index: number) => void;
78
+ pop: (fieldName: string) => void;
79
+ replace: (fieldName: string, index: number, value: unknown) => void;
80
+ };
81
+ };
55
82
  };
56
83
 
57
84
  const noOp = () => {};
@@ -69,10 +96,10 @@ const defaultFormState: FormState = {
69
96
  setFieldError: noOp,
70
97
  setFieldErrors: noOp,
71
98
  clearFieldError: noOp,
99
+ currentDefaultValues: {},
72
100
 
73
101
  reset: () => noOp,
74
102
  syncFormProps: noOp,
75
- setHydrated: noOp,
76
103
  setFormElement: noOp,
77
104
  validateField: async () => null,
78
105
 
@@ -86,10 +113,36 @@ const defaultFormState: FormState = {
86
113
 
87
114
  resetFormElement: noOp,
88
115
  getValues: () => new FormData(),
116
+
117
+ controlledFields: {
118
+ values: {},
119
+ refCounts: {},
120
+ valueUpdatePromises: {},
121
+ valueUpdateResolvers: {},
122
+
123
+ register: noOp,
124
+ unregister: noOp,
125
+ setValue: noOp,
126
+ getValue: noOp,
127
+ kickoffValueUpdate: noOp,
128
+ awaitValueUpdate: async () => {
129
+ throw new Error("AwaitValueUpdate called before form was initialized.");
130
+ },
131
+
132
+ array: {
133
+ push: noOp,
134
+ swap: noOp,
135
+ move: noOp,
136
+ insert: noOp,
137
+ unshift: noOp,
138
+ remove: noOp,
139
+ pop: noOp,
140
+ replace: noOp,
141
+ },
142
+ },
89
143
  };
90
144
 
91
145
  const createFormState = (
92
- formId: InternalFormId,
93
146
  set: (setter: (draft: WritableDraft<FormState>) => void) => void,
94
147
  get: GetState<FormState>
95
148
  ): FormState => ({
@@ -100,6 +153,7 @@ const createFormState = (
100
153
  touchedFields: {},
101
154
  fieldErrors: {},
102
155
  formElement: null,
156
+ currentDefaultValues: {},
103
157
 
104
158
  isValid: () => Object.keys(get().fieldErrors).length === 0,
105
159
  startSubmit: () =>
@@ -132,16 +186,20 @@ const createFormState = (
132
186
  state.fieldErrors = {};
133
187
  state.touchedFields = {};
134
188
  state.hasBeenSubmitted = false;
189
+ const nextDefaults = state.formProps?.defaultValues ?? {};
190
+ state.controlledFields.values = nextDefaults;
191
+ state.currentDefaultValues = nextDefaults;
135
192
  }),
136
193
  syncFormProps: (props: SyncedFormProps) =>
137
194
  set((state) => {
195
+ if (!state.isHydrated) {
196
+ state.controlledFields.values = props.defaultValues;
197
+ state.currentDefaultValues = props.defaultValues;
198
+ }
199
+
138
200
  state.formProps = props;
139
201
  state.isHydrated = true;
140
202
  }),
141
- setHydrated: () =>
142
- set((state) => {
143
- state.isHydrated = true;
144
- }),
145
203
  setFormElement: (formElement: HTMLFormElement | null) => {
146
204
  // This gets called frequently, so we want to avoid calling set() every time
147
205
  // Or else we wind up with an infinite loop
@@ -165,7 +223,7 @@ const createFormState = (
165
223
  "Cannot validator. This is probably a bug in remix-validated-form."
166
224
  );
167
225
 
168
- await useControlledFieldStore.getState().awaitValueUpdate?.(formId, field);
226
+ await get().controlledFields.awaitValueUpdate?.(field);
169
227
 
170
228
  const { error } = await validator.validateField(
171
229
  new FormData(formElement),
@@ -206,20 +264,231 @@ const createFormState = (
206
264
  "Cannot find reference to form. This is probably a bug in remix-validated-form."
207
265
  );
208
266
 
209
- formElement.submit();
267
+ formElement.requestSubmit();
210
268
  },
211
269
 
212
- getValues: () => {
213
- const formElement = get().formElement;
214
- invariant(
215
- formElement,
216
- "Cannot find reference to form. This is probably a bug in remix-validated-form."
217
- );
218
-
219
- return new FormData(formElement);
220
- },
270
+ getValues: () => new FormData(get().formElement ?? undefined),
221
271
 
222
272
  resetFormElement: () => get().formElement?.reset(),
273
+
274
+ controlledFields: {
275
+ values: {},
276
+ refCounts: {},
277
+ valueUpdatePromises: {},
278
+ valueUpdateResolvers: {},
279
+
280
+ register: (fieldName) => {
281
+ set((state) => {
282
+ const current = state.controlledFields.refCounts[fieldName] ?? 0;
283
+ state.controlledFields.refCounts[fieldName] = current + 1;
284
+ });
285
+ },
286
+ unregister: (fieldName) => {
287
+ // For this helper in particular, we may run into a case where state is undefined.
288
+ // When the whole form unmounts, the form state may be cleaned up before the fields are.
289
+ if (get() === null || get() === undefined) return;
290
+ set((state) => {
291
+ const current = state.controlledFields.refCounts[fieldName] ?? 0;
292
+ if (current > 1) {
293
+ state.controlledFields.refCounts[fieldName] = current - 1;
294
+ return;
295
+ }
296
+
297
+ const isNested = Object.keys(state.controlledFields.refCounts).some(
298
+ (key) => fieldName.startsWith(key) && key !== fieldName
299
+ );
300
+
301
+ // When nested within a field array, we should leave resetting up to the field array
302
+ if (!isNested) {
303
+ lodashSet(
304
+ state.controlledFields.values,
305
+ fieldName,
306
+ lodashGet(state.formProps?.defaultValues, fieldName)
307
+ );
308
+ lodashSet(
309
+ state.currentDefaultValues,
310
+ fieldName,
311
+ lodashGet(state.formProps?.defaultValues, fieldName)
312
+ );
313
+ }
314
+
315
+ delete state.controlledFields.refCounts[fieldName];
316
+ });
317
+ },
318
+ getValue: (fieldName) =>
319
+ lodashGet(get().controlledFields.values, fieldName),
320
+ setValue: (fieldName, value) => {
321
+ set((state) => {
322
+ lodashSet(state.controlledFields.values, fieldName, value);
323
+ });
324
+ get().controlledFields.kickoffValueUpdate(fieldName);
325
+ },
326
+ kickoffValueUpdate: (fieldName) => {
327
+ const clear = () =>
328
+ set((state) => {
329
+ delete state.controlledFields.valueUpdateResolvers[fieldName];
330
+ delete state.controlledFields.valueUpdatePromises[fieldName];
331
+ });
332
+ set((state) => {
333
+ const promise = new Promise<void>((resolve) => {
334
+ state.controlledFields.valueUpdateResolvers[fieldName] = resolve;
335
+ }).then(clear);
336
+ state.controlledFields.valueUpdatePromises[fieldName] = promise;
337
+ });
338
+ },
339
+
340
+ awaitValueUpdate: async (fieldName) => {
341
+ await get().controlledFields.valueUpdatePromises[fieldName];
342
+ },
343
+
344
+ array: {
345
+ push: (fieldName, item) => {
346
+ set((state) => {
347
+ arrayUtil
348
+ .getArray(state.controlledFields.values, fieldName)
349
+ .push(item);
350
+ arrayUtil.getArray(state.currentDefaultValues, fieldName).push(item);
351
+ // New item added to the end, no need to update touched or error
352
+ });
353
+ get().controlledFields.kickoffValueUpdate(fieldName);
354
+ },
355
+
356
+ swap: (fieldName, indexA, indexB) => {
357
+ set((state) => {
358
+ arrayUtil.swap(
359
+ arrayUtil.getArray(state.controlledFields.values, fieldName),
360
+ indexA,
361
+ indexB
362
+ );
363
+ arrayUtil.swap(
364
+ arrayUtil.getArray(state.currentDefaultValues, fieldName),
365
+ indexA,
366
+ indexB
367
+ );
368
+ arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
369
+ arrayUtil.swap(array, indexA, indexB)
370
+ );
371
+ arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
372
+ arrayUtil.swap(array, indexA, indexB)
373
+ );
374
+ });
375
+ get().controlledFields.kickoffValueUpdate(fieldName);
376
+ },
377
+
378
+ move: (fieldName, from, to) => {
379
+ set((state) => {
380
+ arrayUtil.move(
381
+ arrayUtil.getArray(state.controlledFields.values, fieldName),
382
+ from,
383
+ to
384
+ );
385
+ arrayUtil.move(
386
+ arrayUtil.getArray(state.currentDefaultValues, fieldName),
387
+ from,
388
+ to
389
+ );
390
+ arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
391
+ arrayUtil.move(array, from, to)
392
+ );
393
+ arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
394
+ arrayUtil.move(array, from, to)
395
+ );
396
+ });
397
+ get().controlledFields.kickoffValueUpdate(fieldName);
398
+ },
399
+ insert: (fieldName, index, item) => {
400
+ set((state) => {
401
+ arrayUtil.insert(
402
+ arrayUtil.getArray(state.controlledFields.values, fieldName),
403
+ index,
404
+ item
405
+ );
406
+ arrayUtil.insert(
407
+ arrayUtil.getArray(state.currentDefaultValues, fieldName),
408
+ index,
409
+ item
410
+ );
411
+ // Even though this is a new item, we need to push around other items.
412
+ arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
413
+ arrayUtil.insert(array, index, false)
414
+ );
415
+ arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
416
+ arrayUtil.insert(array, index, undefined)
417
+ );
418
+ });
419
+ get().controlledFields.kickoffValueUpdate(fieldName);
420
+ },
421
+ remove: (fieldName, index) => {
422
+ set((state) => {
423
+ arrayUtil.remove(
424
+ arrayUtil.getArray(state.controlledFields.values, fieldName),
425
+ index
426
+ );
427
+ arrayUtil.remove(
428
+ arrayUtil.getArray(state.currentDefaultValues, fieldName),
429
+ index
430
+ );
431
+ arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
432
+ arrayUtil.remove(array, index)
433
+ );
434
+ arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
435
+ arrayUtil.remove(array, index)
436
+ );
437
+ });
438
+ get().controlledFields.kickoffValueUpdate(fieldName);
439
+ },
440
+ pop: (fieldName) => {
441
+ set((state) => {
442
+ arrayUtil.getArray(state.controlledFields.values, fieldName).pop();
443
+ arrayUtil.getArray(state.currentDefaultValues, fieldName).pop();
444
+ arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
445
+ array.pop()
446
+ );
447
+ arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
448
+ array.pop()
449
+ );
450
+ });
451
+ get().controlledFields.kickoffValueUpdate(fieldName);
452
+ },
453
+ unshift: (fieldName, value) => {
454
+ set((state) => {
455
+ arrayUtil
456
+ .getArray(state.controlledFields.values, fieldName)
457
+ .unshift(value);
458
+ arrayUtil
459
+ .getArray(state.currentDefaultValues, fieldName)
460
+ .unshift(value);
461
+ arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
462
+ array.unshift(false)
463
+ );
464
+ arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
465
+ array.unshift(undefined)
466
+ );
467
+ });
468
+ },
469
+ replace: (fieldName, index, item) => {
470
+ set((state) => {
471
+ arrayUtil.replace(
472
+ arrayUtil.getArray(state.controlledFields.values, fieldName),
473
+ index,
474
+ item
475
+ );
476
+ arrayUtil.replace(
477
+ arrayUtil.getArray(state.currentDefaultValues, fieldName),
478
+ index,
479
+ item
480
+ );
481
+ arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
482
+ arrayUtil.replace(array, index, item)
483
+ );
484
+ arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
485
+ arrayUtil.replace(array, index, item)
486
+ );
487
+ });
488
+ get().controlledFields.kickoffValueUpdate(fieldName);
489
+ },
490
+ },
491
+ },
223
492
  });
224
493
 
225
494
  export const useRootFormStore = create<FormStoreState>()(
@@ -237,7 +506,6 @@ export const useRootFormStore = create<FormStoreState>()(
237
506
  if (get().forms[formId]) return;
238
507
  set((state) => {
239
508
  state.forms[formId] = createFormState(
240
- formId,
241
509
  (setter) => set((state) => setter(state.forms[formId])),
242
510
  () => get().forms[formId]
243
511
  ) as WritableDraft<FormState>;
@@ -0,0 +1,155 @@
1
+ import React, { useMemo } from "react";
2
+ import { useCallback } from "react";
3
+ import invariant from "tiny-invariant";
4
+ import { InternalFormContextValue } from "../formContext";
5
+ import {
6
+ useFieldDefaultValue,
7
+ useFieldError,
8
+ useInternalFormContext,
9
+ useInternalHasBeenSubmitted,
10
+ useValidateField,
11
+ } from "../hooks";
12
+ import { useRegisterControlledField } from "./controlledFields";
13
+ import { useFormStore } from "./storeHooks";
14
+
15
+ export type FieldArrayValidationBehavior = "onChange" | "onSubmit";
16
+
17
+ export type FieldArrayValidationBehaviorOptions = {
18
+ initial: FieldArrayValidationBehavior;
19
+ whenSubmitted: FieldArrayValidationBehavior;
20
+ };
21
+
22
+ const useInternalFieldArray = (
23
+ context: InternalFormContextValue,
24
+ field: string,
25
+ validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>
26
+ ) => {
27
+ const value = useFieldDefaultValue(field, context);
28
+ useRegisterControlledField(context, field);
29
+ const hasBeenSubmitted = useInternalHasBeenSubmitted(context.formId);
30
+ const validateField = useValidateField(context.formId);
31
+ const error = useFieldError(field, context);
32
+
33
+ const resolvedValidationBehavior: FieldArrayValidationBehaviorOptions = {
34
+ initial: "onSubmit",
35
+ whenSubmitted: "onChange",
36
+ ...validationBehavior,
37
+ };
38
+
39
+ const behavior = hasBeenSubmitted
40
+ ? resolvedValidationBehavior.whenSubmitted
41
+ : resolvedValidationBehavior.initial;
42
+
43
+ const maybeValidate = useCallback(() => {
44
+ if (behavior === "onChange") {
45
+ validateField(field);
46
+ }
47
+ }, [behavior, field, validateField]);
48
+
49
+ invariant(
50
+ value === undefined || value === null || Array.isArray(value),
51
+ `FieldArray: defaultValue value for ${field} must be an array, null, or undefined`
52
+ );
53
+
54
+ const arr = useFormStore(
55
+ context.formId,
56
+ (state) => state.controlledFields.array
57
+ );
58
+
59
+ const helpers = useMemo(
60
+ () => ({
61
+ push: (item: any) => {
62
+ arr.push(field, item);
63
+ maybeValidate();
64
+ },
65
+ swap: (indexA: number, indexB: number) => {
66
+ arr.swap(field, indexA, indexB);
67
+ maybeValidate();
68
+ },
69
+ move: (from: number, to: number) => {
70
+ arr.move(field, from, to);
71
+ maybeValidate();
72
+ },
73
+ insert: (index: number, value: any) => {
74
+ arr.insert(field, index, value);
75
+ maybeValidate();
76
+ },
77
+ unshift: (value: any) => {
78
+ arr.unshift(field, value);
79
+ maybeValidate();
80
+ },
81
+ remove: (index: number) => {
82
+ arr.remove(field, index);
83
+ maybeValidate();
84
+ },
85
+ pop: () => {
86
+ arr.pop(field);
87
+ maybeValidate();
88
+ },
89
+ replace: (index: number, value: any) => {
90
+ arr.replace(field, index, value);
91
+ maybeValidate();
92
+ },
93
+ }),
94
+ [arr, field, maybeValidate]
95
+ );
96
+
97
+ const arrayValue = useMemo(() => value ?? [], [value]);
98
+
99
+ return [arrayValue, helpers, error] as const;
100
+ };
101
+
102
+ export type FieldArrayHelpers<Item = any> = {
103
+ push: (item: Item) => void;
104
+ swap: (indexA: number, indexB: number) => void;
105
+ move: (from: number, to: number) => void;
106
+ insert: (index: number, value: Item) => void;
107
+ unshift: (value: Item) => void;
108
+ remove: (index: number) => void;
109
+ pop: () => void;
110
+ replace: (index: number, value: Item) => void;
111
+ };
112
+
113
+ export type UseFieldArrayOptions = {
114
+ formId?: string;
115
+ validationBehavior?: Partial<FieldArrayValidationBehaviorOptions>;
116
+ };
117
+
118
+ export function useFieldArray<Item = any>(
119
+ name: string,
120
+ { formId, validationBehavior }: UseFieldArrayOptions = {}
121
+ ) {
122
+ const context = useInternalFormContext(formId, "FieldArray");
123
+
124
+ return useInternalFieldArray(context, name, validationBehavior) as [
125
+ itemDefaults: Item[],
126
+ helpers: FieldArrayHelpers,
127
+ error: string | undefined
128
+ ];
129
+ }
130
+
131
+ export type FieldArrayProps = {
132
+ name: string;
133
+ children: (
134
+ itemDefaults: any[],
135
+ helpers: FieldArrayHelpers,
136
+ error: string | undefined
137
+ ) => React.ReactNode;
138
+ formId?: string;
139
+ validationBehavior?: FieldArrayValidationBehaviorOptions;
140
+ };
141
+
142
+ export const FieldArray = ({
143
+ name,
144
+ children,
145
+ formId,
146
+ validationBehavior,
147
+ }: FieldArrayProps) => {
148
+ const context = useInternalFormContext(formId, "FieldArray");
149
+ const [value, helpers, error] = useInternalFieldArray(
150
+ context,
151
+ name,
152
+ validationBehavior
153
+ );
154
+ return children(value, helpers, error);
155
+ };
package/src/server.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { json } from "@remix-run/server-runtime";
1
+ import { json } from "remix";
2
2
  import {
3
3
  formDefaultValuesKey,
4
4
  FORM_DEFAULTS_FIELD,
package/vite.config.ts CHANGED
@@ -2,6 +2,6 @@ import { makeConfig } from "vite-config";
2
2
 
3
3
  export default makeConfig({
4
4
  lib: "remix-validated-form",
5
- external: ["react", "@remix-run/react", "@remix-run/server-runtime"],
5
+ external: ["react", "@remix-run/react"],
6
6
  dir: __dirname,
7
7
  });