Skip to content

Commit 23f4f5f

Browse files
authored
fix: initValues should not be modified if preserve is false (#370)
* chore: update .gitignore * fix: initValues should not be modified if preserve is false * feat: custom cloneDeep * test: raise coverage * perf: getInitialValue always deep * test: add array test
1 parent 9287328 commit 23f4f5f

File tree

8 files changed

+192
-46
lines changed

8 files changed

+192
-46
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# misc
1919
.DS_Store
2020
.vscode
21+
.idea
2122

2223
# umi
2324
.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: 5 additions & 3 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

@@ -133,7 +134,9 @@ export class FormStore {
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: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export function containsNamePath(namePathList: InternalNamePath[], namePath: Int
4444
}
4545

4646
function isObject(obj: StoreValue) {
47-
return typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype;
47+
return (
48+
typeof obj === 'object' &&
49+
obj !== null &&
50+
(Object.getPrototypeOf(obj) === Object.prototype || Array.isArray(obj))
51+
);
4852
}
4953

5054
/**
@@ -62,9 +66,11 @@ function internalSetValues<T>(store: T, values: T): T {
6266
const prevValue = newStore[key];
6367
const value = values[key];
6468

65-
// If both are object (but target is not array), we use recursion to set deep value
66-
const recursive = isObject(prevValue) && isObject(value);
67-
newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : value;
69+
const recursive = isObject(value);
70+
const isArrayValue = Array.isArray(value);
71+
newStore[key] = recursive
72+
? internalSetValues(prevValue || (isArrayValue ? [] : {}), value || {})
73+
: value;
6874
});
6975

7076
return newStore;

tests/initialValue.test.js

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { mount } from 'enzyme';
33
import { resetWarned } from 'rc-util/lib/warning';
4-
import Form, { Field, useForm } from '../src';
4+
import Form, { Field, useForm, List } from '../src';
55
import { Input } from './common/InfoField';
66
import { changeValue, getField } from './common';
77

@@ -47,16 +47,8 @@ describe('Form.InitialValues', () => {
4747
path2: 'Bamboo',
4848
},
4949
});
50-
expect(
51-
getField(wrapper, 'username')
52-
.find('input')
53-
.props().value,
54-
).toEqual('Light');
55-
expect(
56-
getField(wrapper, ['path1', 'path2'])
57-
.find('input')
58-
.props().value,
59-
).toEqual('Bamboo');
50+
expect(getField(wrapper, 'username').find('input').props().value).toEqual('Light');
51+
expect(getField(wrapper, ['path1', 'path2']).find('input').props().value).toEqual('Bamboo');
6052
});
6153

6254
it('update and reset should use new initialValues', () => {
@@ -91,23 +83,15 @@ describe('Form.InitialValues', () => {
9183
expect(form.getFieldsValue()).toEqual({
9284
username: 'Bamboo',
9385
});
94-
expect(
95-
getField(wrapper, 'username')
96-
.find('input')
97-
.props().value,
98-
).toEqual('Bamboo');
86+
expect(getField(wrapper, 'username').find('input').props().value).toEqual('Bamboo');
9987

10088
// Should not change it
10189
wrapper.setProps({ initialValues: { username: 'Light' } });
10290
wrapper.update();
10391
expect(form.getFieldsValue()).toEqual({
10492
username: 'Bamboo',
10593
});
106-
expect(
107-
getField(wrapper, 'username')
108-
.find('input')
109-
.props().value,
110-
).toEqual('Bamboo');
94+
expect(getField(wrapper, 'username').find('input').props().value).toEqual('Bamboo');
11195

11296
// Should change it
11397
form.resetFields();
@@ -116,11 +100,68 @@ describe('Form.InitialValues', () => {
116100
expect(form.getFieldsValue()).toEqual({
117101
username: 'Light',
118102
});
119-
expect(
120-
getField(wrapper, 'username')
121-
.find('input')
122-
.props().value,
123-
).toEqual('Light');
103+
expect(getField(wrapper, 'username').find('input').props().value).toEqual('Light');
104+
});
105+
106+
it(`initialValues shouldn't be modified if preserve is false`, () => {
107+
const formValue = {
108+
test: 'test',
109+
users: [{ first: 'aaa', last: 'bbb' }],
110+
};
111+
112+
const Demo = () => {
113+
const [form] = Form.useForm();
114+
const [show, setShow] = useState(false);
115+
116+
return (
117+
<>
118+
<button onClick={() => setShow(prev => !prev)}>switch show</button>
119+
{show && (
120+
<Form form={form} initialValues={formValue} preserve={false}>
121+
<Field shouldUpdate>
122+
{() => (
123+
<Field name="test" preserve={false}>
124+
<Input />
125+
</Field>
126+
)}
127+
</Field>
128+
<List name="users">
129+
{fields => (
130+
<>
131+
{fields.map(({ key, name, ...restField }) => (
132+
<>
133+
<Field
134+
{...restField}
135+
name={[name, 'first']}
136+
rules={[{ required: true, message: 'Missing first name' }]}
137+
>
138+
<Input className="first-name-input" placeholder="First Name" />
139+
</Field>
140+
<Field
141+
{...restField}
142+
name={[name, 'last']}
143+
rules={[{ required: true, message: 'Missing last name' }]}
144+
>
145+
<Input placeholder="Last Name" />
146+
</Field>
147+
</>
148+
))}
149+
</>
150+
)}
151+
</List>
152+
</Form>
153+
)}
154+
</>
155+
);
156+
};
157+
158+
const wrapper = mount(<Demo />);
159+
wrapper.find('button').simulate('click');
160+
expect(formValue.users[0].last).toEqual('bbb');
161+
wrapper.find('button').simulate('click');
162+
expect(formValue.users[0].last).toEqual('bbb');
163+
wrapper.find('button').simulate('click');
164+
expect(wrapper.find('.first-name-input').first().find('input').instance().value).toEqual('aaa');
124165
});
125166

126167
describe('Field with initialValue', () => {
@@ -237,21 +278,12 @@ describe('Form.InitialValues', () => {
237278
expect(wrapper.find('input').props().value).toEqual('story');
238279

239280
// First reset will get nothing
240-
wrapper
241-
.find('button')
242-
.first()
243-
.simulate('click');
281+
wrapper.find('button').first().simulate('click');
244282
expect(wrapper.find('input').props().value).toEqual('');
245283

246284
// Change field initialValue and reset
247-
wrapper
248-
.find('button')
249-
.last()
250-
.simulate('click');
251-
wrapper
252-
.find('button')
253-
.first()
254-
.simulate('click');
285+
wrapper.find('button').last().simulate('click');
286+
wrapper.find('button').first().simulate('click');
255287
expect(wrapper.find('input').props().value).toEqual('light');
256288
});
257289

tests/utils.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { move, isSimilar, setValues } from '../src/utils/valueUtil';
22
import NameMap from '../src/utils/NameMap';
3+
import cloneDeep from '../src/utils/cloneDeep';
34

45
describe('utils', () => {
56
describe('arrayMove', () => {
@@ -71,4 +72,12 @@ describe('utils', () => {
7172
});
7273
});
7374
});
75+
76+
describe('clone deep', () => {
77+
it('should not deep clone Class', () => {
78+
const data = { a: new Date() };
79+
const clonedData = cloneDeep(data);
80+
expect(data.a === clonedData.a).toBeTruthy();
81+
});
82+
});
7483
});

0 commit comments

Comments
 (0)