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

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 +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 +175 -8
  11. package/dist/remix-validated-form.cjs.js +4 -4
  12. package/dist/remix-validated-form.cjs.js.map +1 -1
  13. package/dist/remix-validated-form.es.js +336 -126
  14. package/dist/remix-validated-form.es.js.map +1 -1
  15. package/dist/remix-validated-form.umd.js +4 -4
  16. package/dist/remix-validated-form.umd.js.map +1 -1
  17. package/dist/types/index.d.ts +1 -0
  18. package/dist/types/internal/hooks.d.ts +1 -0
  19. package/dist/types/internal/state/arrayUtil.d.ts +12 -0
  20. package/dist/types/internal/state/controlledFields.d.ts +1 -0
  21. package/dist/types/internal/state/createFormStore.d.ts +31 -1
  22. package/dist/types/internal/state/fieldArray.d.ts +28 -0
  23. package/package.json +1 -1
  24. package/src/ValidatedForm.tsx +0 -3
  25. package/src/index.ts +6 -0
  26. package/src/internal/hooks.ts +9 -4
  27. package/src/internal/logic/nestedObjectToPathObject.ts +63 -0
  28. package/src/internal/state/arrayUtil.ts +399 -0
  29. package/src/internal/state/controlledFields.ts +39 -43
  30. package/src/internal/state/createFormStore.ts +286 -10
  31. package/src/internal/state/fieldArray.tsx +155 -0
  32. package/dist/types/internal/state/controlledFieldStore.d.ts +0 -26
  33. 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),
@@ -212,6 +270,225 @@ const createFormState = (
212
270
  getValues: () => new FormData(get().formElement ?? undefined),
213
271
 
214
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
+ },
215
492
  });
216
493
 
217
494
  export const useRootFormStore = create<FormStoreState>()(
@@ -229,7 +506,6 @@ export const useRootFormStore = create<FormStoreState>()(
229
506
  if (get().forms[formId]) return;
230
507
  set((state) => {
231
508
  state.forms[formId] = createFormState(
232
- formId,
233
509
  (setter) => set((state) => setter(state.forms[formId])),
234
510
  () => get().forms[formId]
235
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
+ };
@@ -1,26 +0,0 @@
1
- import { InternalFormId } from "./types";
2
- export declare type FieldState = {
3
- refCount: number;
4
- value: unknown;
5
- defaultValue?: unknown;
6
- hydrated: boolean;
7
- valueUpdatePromise: Promise<void> | undefined;
8
- resolveValueUpdate: (() => void) | undefined;
9
- };
10
- export declare type ControlledFieldState = {
11
- forms: {
12
- [formId: InternalFormId]: {
13
- [fieldName: string]: FieldState | undefined;
14
- };
15
- };
16
- register: (formId: InternalFormId, fieldName: string) => void;
17
- unregister: (formId: InternalFormId, fieldName: string) => void;
18
- getField: (formId: InternalFormId, fieldName: string) => FieldState | undefined;
19
- setValue: (formId: InternalFormId, fieldName: string, value: unknown) => void;
20
- hydrateWithDefault: (formId: InternalFormId, fieldName: string, defaultValue: unknown) => void;
21
- awaitValueUpdate: (formId: InternalFormId, fieldName: string) => Promise<void>;
22
- reset: (formId: InternalFormId) => void;
23
- };
24
- export declare const useControlledFieldStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<ControlledFieldState>, "setState"> & {
25
- setState(nextStateOrUpdater: ControlledFieldState | Partial<ControlledFieldState> | ((state: import("immer/dist/internal").WritableDraft<ControlledFieldState>) => void), shouldReplace?: boolean | undefined): void;
26
- }>;