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.
- package/.turbo/turbo-build.log +8 -9
- 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 +177 -14
- package/browser/server.d.ts +2 -2
- package/browser/server.js +1 -1
- package/dist/remix-validated-form.cjs.js +12 -3
- package/dist/remix-validated-form.cjs.js.map +1 -1
- package/dist/remix-validated-form.es.js +361 -131
- package/dist/remix-validated-form.es.js.map +1 -1
- package/dist/remix-validated-form.umd.js +12 -3
- 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/dist/types/server.d.ts +2 -2
- package/package.json +1 -3
- 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 +288 -20
- package/src/internal/state/fieldArray.tsx +155 -0
- package/src/server.ts +1 -1
- package/vite.config.ts +1 -1
- 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),
|
@@ -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.
|
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
package/vite.config.ts
CHANGED