Skip to content
This repository was archived by the owner on Jul 27, 2022. It is now read-only.

Commit 9d48ba0

Browse files
committed
fix(useFieldArray): field array in field array not working
1 parent e3eb173 commit 9d48ba0

File tree

11 files changed

+184
-66
lines changed

11 files changed

+184
-66
lines changed

.changeset/mighty-wasps-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-cool-form": patch
3+
---
4+
5+
fix(useFieldArray): nested field array not working

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ const App = () => {
111111

112112
✨ Pretty easy right? React Cool Form is more powerful than you think. Let's [explore it](https://react-cool-form.netlify.app) now!
113113

114-
## Articles / Blog Posts
114+
## Articles / Blog Posts
115115

116116
> 💡 If you have written any blog post or article about React Cool Form, please open a PR to add it here.
117117

app/src/Playground/index.tsx

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,92 @@
11
/* eslint-disable no-console */
22

3-
import { useForm } from "react-cool-form";
3+
import { useForm, useFieldArray } from "react-cool-form";
44

55
export default () => {
6-
const { form, runValidation } = useForm({
7-
// validate: () => ({ foo: "Required" }),
8-
focusOnError: ["foo"],
6+
const { form } = useForm({
7+
defaultValues: {
8+
foo: [
9+
{
10+
name: "Iron Man",
11+
arr: [{ name: "iron arr.0" }, { name: "iron arr.1" }],
12+
},
13+
],
14+
},
15+
onSubmit: (values) => console.log("LOG ===> Form data: ", values),
916
});
17+
const [fields, { push, insert, remove }] = useFieldArray("foo");
1018

1119
return (
12-
<>
13-
<form ref={form} noValidate>
14-
<input name="foo" required />
15-
<input name="bar" required />
16-
{/* <input type="submit" /> */}
17-
</form>
18-
<button type="button" onClick={() => runValidation(["bar"])}>
19-
Validate
20-
</button>
21-
</>
20+
<form ref={form}>
21+
<table>
22+
<thead>
23+
<tr>
24+
<th>Name</th>
25+
<th>Arr</th>
26+
<th>Actions</th>
27+
</tr>
28+
</thead>
29+
<tbody>
30+
{fields.map((key, i) => (
31+
<tr key={key}>
32+
<td>
33+
<input name={`foo[${i}].name`} />
34+
</td>
35+
<td>
36+
<Arr field={`foo[${i}]`} />
37+
</td>
38+
<td>
39+
<button type="button" onClick={() => remove(i)}>
40+
REMOVE
41+
</button>
42+
</td>
43+
</tr>
44+
))}
45+
</tbody>
46+
</table>
47+
<div>
48+
<button
49+
type="button"
50+
onClick={() => {
51+
push({ name: "Loki", arr: [{ name: "Your Savior Is Here" }] });
52+
}}
53+
>
54+
PUSH
55+
</button>
56+
<button
57+
type="button"
58+
onClick={() =>
59+
insert(0, {
60+
name: "Spider Man",
61+
arr: [{ name: "Your Friendly Neighborhood Spider-Man" }],
62+
})
63+
}
64+
>
65+
INSERT
66+
</button>
67+
</div>
68+
<input type="submit" />
69+
<input type="reset" />
70+
</form>
2271
);
2372
};
73+
74+
function Arr({ field }: any) {
75+
const [fields, { push }] = useFieldArray(`${field}.arr`);
76+
77+
return (
78+
<div>
79+
{fields.map((key, i) => (
80+
<input
81+
key={key}
82+
style={{ height: "20px", width: "100px" }}
83+
type="text"
84+
name={`${field}.arr[${i}].name`}
85+
/>
86+
))}
87+
<button type="button" onClick={() => push({ name: "xxx" })}>
88+
inner push
89+
</button>
90+
</div>
91+
);
92+
}

src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export type Fields = Map<
9292

9393
export type Parsers = ObjMap<Omit<FieldOptions, "validate">>;
9494

95-
export type FieldArray = ObjMap<{ fields: ObjMap; reset: () => void }>;
95+
export type FieldArray = Map<string, { fields: ObjMap; update: () => void }>;
9696

9797
interface EventOptions<V> {
9898
removeField: RemoveField;

src/useFieldArray.ts

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useState } from "react";
1+
import { useCallback, useEffect, useRef, useState } from "react";
22

33
import {
44
FieldArrayConfig,
@@ -18,6 +18,7 @@ import {
1818
compact,
1919
get,
2020
getIsDirty,
21+
getUid,
2122
invariant,
2223
isUndefined,
2324
set,
@@ -48,15 +49,19 @@ export default <T = any, V extends FormValues = FormValues>(
4849
runValidation,
4950
removeField,
5051
} = methods;
52+
const keysRef = useRef<string[]>([]);
5153

5254
const getFields = useCallback(
53-
(init = false): string[] => {
55+
(init = false) => {
5456
let fields = getState(name);
5557

5658
if (init && isUndefined(fields)) fields = defaultValue;
5759

5860
return Array.isArray(fields)
59-
? fields.map((_, index) => `${name}[${index}]`)
61+
? fields.map((_, index) => {
62+
keysRef.current[index] = keysRef.current[index] || getUid();
63+
return keysRef.current[index];
64+
})
6065
: [];
6166
},
6267
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -66,11 +71,13 @@ export default <T = any, V extends FormValues = FormValues>(
6671
const [fields, setFields] = useState<string[]>(getFields(true));
6772

6873
const updateFields = useCallback(() => {
69-
setFields(getFields());
70-
setNodesOrValues(getState("values"), {
71-
shouldSetValues: false,
72-
fields: Object.keys(fieldArrayRef.current[name].fields),
73-
});
74+
if (fieldArrayRef.current.has(name)) {
75+
setFields(getFields());
76+
setNodesOrValues(getState("values"), {
77+
shouldSetValues: false,
78+
fields: Object.keys(fieldArrayRef.current.get(name)!.fields),
79+
});
80+
}
7481
}, [fieldArrayRef, getFields, getState, name, setNodesOrValues]);
7582

7683
useEffect(() => {
@@ -83,16 +90,13 @@ export default <T = any, V extends FormValues = FormValues>(
8390
}
8491

8592
return () => {
86-
if (shouldRemoveField(name)) removeField(name);
93+
if (shouldRemoveField(name)) removeField(name, ["defaultValue"]);
8794
};
8895
// eslint-disable-next-line react-hooks/exhaustive-deps
8996
}, []);
9097

91-
if (!fieldArrayRef.current[name])
92-
fieldArrayRef.current[name] = {
93-
reset: updateFields,
94-
fields: {},
95-
};
98+
if (!fieldArrayRef.current.has(name))
99+
fieldArrayRef.current.set(name, { update: updateFields, fields: {} });
96100
if (validate) fieldValidatorsRef.current[name] = validate;
97101

98102
const setState = useCallback(
@@ -106,8 +110,8 @@ export default <T = any, V extends FormValues = FormValues>(
106110
let state = getState();
107111

108112
(["values", "touched", "errors", "dirty"] as Keys[]).forEach((key) => {
109-
const value = state[key][name];
110-
const fieldsLength = state.values[name]?.length;
113+
const value = get(state[key], name);
114+
const fieldsLength = get(state.values, name)?.length;
111115

112116
if (
113117
key === "values" ||
@@ -117,15 +121,12 @@ export default <T = any, V extends FormValues = FormValues>(
117121
)
118122
state = set(
119123
state,
120-
key,
121-
{
122-
...state[key],
123-
[name]: handler(
124-
Array.isArray(value) ? [...value] : [],
125-
key,
126-
fieldsLength ? fieldsLength - 1 : 0
127-
),
128-
},
124+
`${key}.${name}`,
125+
handler(
126+
Array.isArray(value) ? [...value] : [],
127+
key,
128+
fieldsLength ? fieldsLength - 1 : 0
129+
),
129130
true
130131
);
131132
});
@@ -143,6 +144,7 @@ export default <T = any, V extends FormValues = FormValues>(
143144
const handler: StateHandler = (f, type, lastIndex = 0) => {
144145
if (type === "values") {
145146
f.push(value);
147+
keysRef.current.push(getUid());
146148
} else if (
147149
(type === "touched" && shouldTouched) ||
148150
(type === "dirty" && shouldDirty)
@@ -163,6 +165,7 @@ export default <T = any, V extends FormValues = FormValues>(
163165
const handler: StateHandler = (f, type) => {
164166
if (type === "values") {
165167
f.splice(index, 0, value);
168+
keysRef.current.splice(index, 0, getUid());
166169
} else if (
167170
(type === "touched" && shouldTouched) ||
168171
(type === "dirty" && shouldDirty)
@@ -184,6 +187,7 @@ export default <T = any, V extends FormValues = FormValues>(
184187
(index) => {
185188
const handler: StateHandler = (f) => {
186189
f.splice(index, 1);
190+
keysRef.current.splice(index, 1);
187191
return compact(f).length ? f : [];
188192
};
189193
const value = (getState(name) || [])[index];
@@ -199,6 +203,10 @@ export default <T = any, V extends FormValues = FormValues>(
199203
(indexA, indexB) => {
200204
const handler: StateHandler = (f) => {
201205
[f[indexA], f[indexB]] = [f[indexB], f[indexA]];
206+
[keysRef.current[indexA], keysRef.current[indexB]] = [
207+
keysRef.current[indexB],
208+
keysRef.current[indexA],
209+
];
202210
return f;
203211
};
204212

@@ -211,6 +219,7 @@ export default <T = any, V extends FormValues = FormValues>(
211219
(from, to) => {
212220
const handler: StateHandler = (f) => {
213221
f.splice(to, 0, f.splice(from, 1)[0]);
222+
keysRef.current.splice(to, 0, f.splice(from, 1)[0]);
214223
return f;
215224
};
216225

src/useForm.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export default <V extends FormValues = FormValues>({
9090
const formRef = useRef<HTMLElement>();
9191
const fieldsRef = useRef<Fields>(new Map());
9292
const fieldParsersRef = useRef<Parsers>({});
93-
const fieldArrayRef = useRef<FieldArray>({});
93+
const fieldArrayRef = useRef<FieldArray>(new Map());
9494
const controlsRef = useRef<ObjMap>({});
9595
const formValidatorRef = useLatest(validate);
9696
const fieldValidatorsRef = useRef<ObjMap<FieldValidator<V>>>({});
@@ -183,7 +183,7 @@ export default <V extends FormValues = FormValues>({
183183
const fieldArrayName = isFieldArray(fieldArrayRef.current, name);
184184

185185
if (fieldArrayName)
186-
fieldArrayRef.current[fieldArrayName].fields[name] = true;
186+
fieldArrayRef.current.get(fieldArrayName)!.fields[name] = true;
187187

188188
acc.set(name, {
189189
...acc.get(name),
@@ -389,7 +389,7 @@ export default <V extends FormValues = FormValues>({
389389
}, [builtInValidationMode, runBuiltInValidation]);
390390

391391
const runFieldValidation = useCallback(
392-
async (name: string): Promise<any> => {
392+
async (name: string) => {
393393
const value = get(stateRef.current.values, name);
394394

395395
if (!fieldValidatorsRef.current[name] || isUndefined(value))
@@ -704,7 +704,7 @@ export default <V extends FormValues = FormValues>({
704704
setNodeValue(name, value);
705705

706706
isFieldArray(fieldArrayRef.current, name, (key) =>
707-
fieldArrayRef.current[key].reset()
707+
fieldArrayRef.current.get(key)!.update()
708708
);
709709

710710
if (shouldTouched) setTouched(name, true, { shouldValidate: false });
@@ -774,7 +774,7 @@ export default <V extends FormValues = FormValues>({
774774
setStateRef("", state);
775775
onResetRef.current(state.values, getOptions(), e);
776776

777-
Object.values(fieldArrayRef.current).forEach((field) => field.reset());
777+
fieldArrayRef.current.forEach((field) => field.update());
778778
},
779779
[getOptions, onResetRef, setNodesOrValues, setStateRef, stateRef]
780780
);
@@ -842,8 +842,8 @@ export default <V extends FormValues = FormValues>({
842842
? removeOnUnmounted
843843
: [
844844
...Array.from(fieldsRef.current.keys()),
845+
...Array.from(fieldArrayRef.current.keys()),
845846
...Object.keys(controlsRef.current),
846-
...Object.keys(fieldArrayRef.current),
847847
];
848848
names = isFunction(removeOnUnmounted) ? removeOnUnmounted(names) : names;
849849

@@ -882,10 +882,9 @@ export default <V extends FormValues = FormValues>({
882882

883883
delete fieldParsersRef.current[name];
884884
delete fieldValidatorsRef.current[name];
885-
delete fieldArrayRef.current[name];
886885
delete controlsRef.current[name];
887-
888-
if (fieldsRef.current.has(name)) fieldsRef.current.delete(name);
886+
fieldArrayRef.current.delete(name);
887+
fieldsRef.current.delete(name);
889888
},
890889
[handleUnset, stateRef]
891890
);

src/utils/getUid.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import getUid from "./getUid";
2+
3+
describe("getUid", () => {
4+
it("should work correctly", () => {
5+
// @ts-expect-error
6+
window.crypto = { getRandomValues: () => new Array(16).fill(0) };
7+
expect(getUid()).toBe("00000000-0000-4000-8000-000000000000");
8+
});
9+
});

src/utils/getUid.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* eslint-disable no-bitwise */
2+
3+
const hex: string[] = [];
4+
5+
for (let i = 0; i < 256; i += 1) hex[i] = (i < 16 ? "0" : "") + i.toString(16);
6+
7+
export default (): string => {
8+
const r = crypto.getRandomValues(new Uint8Array(16));
9+
10+
r[6] = (r[6] & 0x0f) | 0x40;
11+
r[8] = (r[8] & 0x3f) | 0x80;
12+
13+
return `${hex[r[0]] + hex[r[1]] + hex[r[2]] + hex[r[3]]}-${hex[r[4]]}${
14+
hex[r[5]]
15+
}-${hex[r[6]]}${hex[r[7]]}-${hex[r[8]]}${hex[r[9]]}-${hex[r[10]]}${
16+
hex[r[11]]
17+
}${hex[r[12]]}${hex[r[13]]}${hex[r[14]]}${hex[r[15]]}`;
18+
};

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export { default as filterErrors } from "./filterErrors";
66
export { default as get } from "./get";
77
export { default as getIsDirty } from "./getIsDirty";
88
export { default as getPath } from "./getPath";
9+
export { default as getUid } from "./getUid";
910
export { default as invariant } from "./invariant";
1011
export { default as isAsyncFunction } from "./isAsyncFunction";
1112
export { default as isCheckboxInput } from "./isCheckboxInput";

0 commit comments

Comments
 (0)