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.
- package/.turbo/turbo-build.log +8 -8
- package/browser/ValidatedForm.js +0 -3
- package/browser/index.d.ts +1 -0
- package/browser/index.js +1 -0
- package/browser/internal/hooks.d.ts +1 -0
- package/browser/internal/hooks.js +3 -4
- package/browser/internal/state/controlledFields.d.ts +1 -0
- package/browser/internal/state/controlledFields.js +17 -29
- package/browser/internal/state/createFormStore.d.ts +31 -1
- package/browser/internal/state/createFormStore.js +175 -8
- package/dist/remix-validated-form.cjs.js +4 -4
- package/dist/remix-validated-form.cjs.js.map +1 -1
- package/dist/remix-validated-form.es.js +336 -126
- package/dist/remix-validated-form.es.js.map +1 -1
- package/dist/remix-validated-form.umd.js +4 -4
- package/dist/remix-validated-form.umd.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal/hooks.d.ts +1 -0
- package/dist/types/internal/state/arrayUtil.d.ts +12 -0
- package/dist/types/internal/state/controlledFields.d.ts +1 -0
- package/dist/types/internal/state/createFormStore.d.ts +31 -1
- package/dist/types/internal/state/fieldArray.d.ts +28 -0
- package/package.json +1 -1
- package/src/ValidatedForm.tsx +0 -3
- package/src/index.ts +6 -0
- package/src/internal/hooks.ts +9 -4
- package/src/internal/logic/nestedObjectToPathObject.ts +63 -0
- package/src/internal/state/arrayUtil.ts +399 -0
- package/src/internal/state/controlledFields.ts +39 -43
- package/src/internal/state/createFormStore.ts +286 -10
- package/src/internal/state/fieldArray.tsx +155 -0
- package/dist/types/internal/state/controlledFieldStore.d.ts +0 -26
- 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 =
|
13
|
-
|
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
|
-
|
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
|
23
|
+
export const useRegisterControlledField = (
|
46
24
|
context: InternalFormContextValue,
|
47
25
|
field: string
|
48
26
|
) => {
|
49
|
-
const resolveUpdate =
|
50
|
-
|
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 =
|
57
|
-
|
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(
|
60
|
-
return () => unregister(
|
44
|
+
register(field);
|
45
|
+
return () => unregister(field);
|
61
46
|
}, [context.formId, field, register, unregister]);
|
47
|
+
};
|
62
48
|
|
63
|
-
|
64
|
-
|
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(
|
68
|
-
[
|
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 =
|
70
|
+
const setValue = useFormStore(
|
71
|
+
formId,
|
72
|
+
(state) => state.controlledFields.setValue
|
73
|
+
);
|
78
74
|
return useCallback(
|
79
|
-
(field: string, value: unknown) => setValue(
|
80
|
-
[
|
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 =
|
86
|
-
|
87
|
-
(
|
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
|
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
|
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
|
-
}>;
|