Skip to content

Commit 084e293

Browse files
authored
fix: deep clone array for initialValues (#377)
* Revert "revert: revert initialValues protect (#376)" This reverts commit 0638e32. * fix: deep clone array for initialValues * chore: code clean * chore: code clean * chore: code clean
1 parent 6170663 commit 084e293

File tree

9 files changed

+263
-45
lines changed

9 files changed

+263
-45
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# misc
1919
.DS_Store
2020
.vscode
21-
.idea/
21+
.idea
2222

2323
# umi
2424
.umi

docs/demo/initialValues.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## initialValues
2+
3+
4+
<code src="../examples/initialValues.tsx" />

docs/examples/initialValues.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* eslint-disable react/prop-types */
2+
3+
import React, { useState } from 'react';
4+
import Form from 'rc-field-form';
5+
import Input from './components/Input';
6+
7+
const { Field, List } = Form;
8+
9+
const formValue = {
10+
test: "test",
11+
users: [{ first: "aaa", last: "bbb" }]
12+
};
13+
14+
export default () => {
15+
const [form] = Form.useForm();
16+
const [show, setShow] = useState<boolean>(false);
17+
18+
return (
19+
<>
20+
<button onClick={() => setShow((prev) => !prev)}>switch show</button>
21+
{show && (
22+
<Form
23+
form={form}
24+
initialValues={formValue}
25+
preserve={false}
26+
onFinish={values => {
27+
console.log('Submit:', values);
28+
}}
29+
>
30+
<Field shouldUpdate>
31+
{() => (
32+
<Field name="test" preserve={false}>
33+
<Input/>
34+
</Field>
35+
)}
36+
</Field>
37+
<List name="users">
38+
{(fields) => (
39+
<>
40+
{fields.map(({ key, name, ...restField }) => (
41+
<>
42+
<Field
43+
{...restField}
44+
name={[name, "first"]}
45+
rules={[
46+
{ required: true, message: "Missing first name" }
47+
]}
48+
>
49+
<Input placeholder="First Name" />
50+
</Field>
51+
<Field
52+
{...restField}
53+
name={[name, "last"]}
54+
rules={[{ required: true, message: "Missing last name" }]}
55+
>
56+
<Input placeholder="Last Name" />
57+
</Field>
58+
</>
59+
))}
60+
</>
61+
)}
62+
</List>
63+
</Form>
64+
)}
65+
</>
66+
);
67+
};

src/useForm.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
setValue,
3636
setValues,
3737
} from './utils/valueUtil';
38+
import cloneDeep from './utils/cloneDeep';
3839

3940
type InvalidateFieldEntity = { INVALIDATE_NAME_PATH: InternalNamePath };
4041

@@ -129,11 +130,13 @@ export class FormStore {
129130
private setInitialValues = (initialValues: Store, init: boolean) => {
130131
this.initialValues = initialValues || {};
131132
if (init) {
132-
this.store = setValues({}, initialValues, this.store);
133+
this.store = setValues({}, this.store, initialValues);
133134
}
134135
};
135136

136-
private getInitialValue = (namePath: InternalNamePath) => getValue(this.initialValues, namePath);
137+
private getInitialValue = (namePath: InternalNamePath) => {
138+
return cloneDeep(getValue(this.initialValues, namePath));
139+
};
137140

138141
private setCallbacks = (callbacks: Callbacks) => {
139142
this.callbacks = callbacks;
@@ -549,14 +552,13 @@ export class FormStore {
549552
// un-register field callback
550553
return (isListField?: boolean, preserve?: boolean, subNamePath: InternalNamePath = []) => {
551554
this.fieldEntities = this.fieldEntities.filter(item => item !== entity);
552-
553555
// Clean up store value if not preserve
554556
const mergedPreserve = preserve !== undefined ? preserve : this.preserve;
555557

556558
if (mergedPreserve === false && (!isListField || subNamePath.length > 1)) {
557559
const namePath = entity.getNamePath();
558560

559-
const defaultValue = isListField ? undefined : getValue(this.initialValues, namePath);
561+
const defaultValue = isListField ? undefined : this.getInitialValue(namePath);
560562

561563
if (
562564
namePath.length &&

src/utils/cloneDeep.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
function cloneDeep(val) {
2+
if (Array.isArray(val)) {
3+
return cloneArrayDeep(val);
4+
} else if (typeof val === 'object' && val !== null) {
5+
return cloneObjectDeep(val);
6+
}
7+
return val;
8+
}
9+
10+
function cloneObjectDeep(val) {
11+
if (Object.getPrototypeOf(val) === Object.prototype) {
12+
const res = {};
13+
for (const key in val) {
14+
res[key] = cloneDeep(val[key]);
15+
}
16+
return res;
17+
}
18+
return val;
19+
}
20+
21+
function cloneArrayDeep(val) {
22+
return val.map(item => cloneDeep(item));
23+
}
24+
25+
export default cloneDeep;

src/utils/valueUtil.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import get from 'rc-util/lib/utils/get';
22
import set from 'rc-util/lib/utils/set';
33
import type { InternalNamePath, NamePath, Store, StoreValue, EventArgs } from '../interface';
44
import { toArray } from './typeUtil';
5+
import cloneDeep from '../utils/cloneDeep';
56

67
/**
78
* Convert name to internal supported format.
@@ -64,7 +65,8 @@ function internalSetValues<T>(store: T, values: T): T {
6465

6566
// If both are object (but target is not array), we use recursion to set deep value
6667
const recursive = isObject(prevValue) && isObject(value);
67-
newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : value;
68+
69+
newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : cloneDeep(value); // Clone deep for arrays
6870
});
6971

7072
return newStore;

tests/index.test.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,4 +726,81 @@ describe('Form.Basic', () => {
726726
wrapper.find('Input').simulate('change', { event: { target: { value: 'Light' } } });
727727
}).not.toThrowError();
728728
});
729+
730+
it('setFieldsValue for List should work', () => {
731+
const Demo = () => {
732+
const [form] = useForm();
733+
734+
const handelReset = () => {
735+
form.setFieldsValue({
736+
users: [],
737+
});
738+
};
739+
740+
const initialValues = {
741+
users: [{ name: '11' }, { name: '22' }],
742+
};
743+
744+
return (
745+
<Form
746+
form={form}
747+
initialValues={initialValues}
748+
name="dynamic_form_nest_item"
749+
autoComplete="off"
750+
>
751+
<Form.List name="users">
752+
{(fields, { add, remove }) => (
753+
<>
754+
{fields.map(({ key, name, ...restField }) => (
755+
<Field
756+
key={key}
757+
{...restField}
758+
name={[name, 'name']}
759+
rules={[{ required: true, message: 'Missing name' }]}
760+
>
761+
<Input placeholder="Name" />
762+
</Field>
763+
))}
764+
</>
765+
)}
766+
</Form.List>
767+
<Field>
768+
<button className="reset-btn" onClick={handelReset}>
769+
reset
770+
</button>
771+
</Field>
772+
</Form>
773+
);
774+
};
775+
776+
const wrapper = mount(<Demo />);
777+
expect(wrapper.find('input').first().getDOMNode().value).toBe('11');
778+
wrapper.find('.reset-btn').first().simulate('click');
779+
expect(wrapper.find('input').length).toBe(0);
780+
});
781+
782+
it('setFieldsValue should work for multiple Select', () => {
783+
const Select = ({ value, defaultValue }) => {
784+
return <div className="select-div">{(value || defaultValue || []).toString()}</div>;
785+
};
786+
787+
const Demo = () => {
788+
const [formInstance] = Form.useForm();
789+
790+
React.useEffect(() => {
791+
formInstance.setFieldsValue({ selector: ['K1', 'K2'] });
792+
}, [formInstance]);
793+
794+
return (
795+
<Form form={formInstance}>
796+
<Field initialValue="K1" name="selector">
797+
<Select />
798+
</Field>
799+
</Form>
800+
);
801+
};
802+
803+
const wrapper = mount(<Demo />);
804+
expect(wrapper.find('.select-div').text()).toBe('K1,K2');
805+
});
729806
});

0 commit comments

Comments
 (0)