Skip to content

Commit e6056ab

Browse files
authored
fix: shouldUpdate should trigger on field removed (#339)
* fix: shouldUpdate should trigger on field removed * test: fill reset test * test: target to dom node * test: more test case * fix: missing deps logic
1 parent 6a56807 commit e6056ab

File tree

6 files changed

+203
-80
lines changed

6 files changed

+203
-80
lines changed

docs/examples/basic.tsx

Lines changed: 32 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,36 @@ import React from 'react';
22
import Form, { Field, FormInstance } from 'rc-field-form';
33
import Input from './components/Input';
44

5-
const list = new Array(1111).fill(() => null);
6-
7-
interface FormValues {
8-
username?: string;
9-
password?: string;
10-
path1?: {
11-
path2?: string;
12-
};
13-
}
14-
15-
export default class Demo extends React.Component {
16-
formRef: any = React.createRef<FormInstance<FormValues>>();
17-
18-
onFinish = (values: FormValues) => {
19-
console.log('Submit:', values);
20-
21-
setTimeout(() => {
22-
this.formRef.current.setFieldsValue({ path1: { path2: '2333' } });
23-
}, 500);
24-
};
25-
26-
public render() {
27-
return (
28-
<div>
29-
<h3>State Form ({list.length} inputs)</h3>
30-
<Form<FormValues> ref={this.formRef} onFinish={this.onFinish}>
31-
<Field name="username">
32-
<Input placeholder="Username" />
33-
</Field>
34-
<Field name="password">
35-
<Input placeholder="Password" />
36-
</Field>
37-
<Field name="username">
38-
<Input placeholder="Shadow of Username" />
39-
</Field>
40-
<Field name={['path1', 'path2']}>
41-
<Input placeholder="nest" />
42-
</Field>
43-
<Field name={['renderProps']}>
44-
{control => (
45-
<div>
46-
I am render props
47-
<Input {...control} placeholder="render props" />
48-
</div>
49-
)}
50-
</Field>
51-
52-
<button type="submit">Submit</button>
53-
54-
<h4>Show additional field when `username` is `111`</h4>
55-
<Field<FormValues> dependencies={['username']}>
56-
{(control, meta, context) => {
57-
const { username } = context.getFieldsValue(true);
58-
console.log('my render!', username);
59-
return username === '111' && <Input {...control} placeholder="I am secret!" />;
60-
}}
61-
</Field>
62-
63-
{list.map((_, index) => (
64-
<Field key={index} name={`field_${index}`}>
65-
<Input placeholder={`field_${index}`} />
5+
export default () => {
6+
const [form] = Form.useForm();
7+
8+
return (
9+
<Form form={form} preserve={false}>
10+
<Field name="name">
11+
<Input placeholder="Username" />
12+
</Field>
13+
14+
<Field dependencies={['name']}>
15+
{() => {
16+
return form.getFieldValue('name') === '1' ? (
17+
<Field name="password">
18+
<Input placeholder="Password" />
19+
</Field>
20+
) : null;
21+
}}
22+
</Field>
23+
24+
<Field dependencies={['password']}>
25+
{() => {
26+
const password = form.getFieldValue('password');
27+
console.log('>>>', password);
28+
return password ? (
29+
<Field name="password2">
30+
<Input placeholder="Password 2" />
6631
</Field>
67-
))}
68-
</Form>
69-
</div>
70-
);
71-
}
72-
}
32+
) : null;
33+
}}
34+
</Field>
35+
</Form>
36+
);
37+
};

src/Field.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,20 @@ class Field extends React.Component<InternalFieldProps, FieldState> implements F
265265
}
266266
break;
267267

268+
/**
269+
* In case field with `preserve = false` nest deps like:
270+
* - A = 1 => show B
271+
* - B = 1 => show C
272+
* - Reset A, need clean B, C
273+
*/
274+
case 'remove': {
275+
if (shouldUpdate) {
276+
this.reRender();
277+
return;
278+
}
279+
break;
280+
}
281+
268282
case 'setField': {
269283
if (namePathMatch) {
270284
const { data } = info;

src/interface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ interface ResetInfo {
155155
type: 'reset';
156156
}
157157

158+
interface RemoveInfo {
159+
type: 'remove';
160+
}
161+
158162
interface SetFieldInfo {
159163
type: 'setField';
160164
data: FieldData;
@@ -174,6 +178,7 @@ export type NotifyInfo =
174178
| ValueUpdateInfo
175179
| ValidateFinishInfo
176180
| ResetInfo
181+
| RemoveInfo
177182
| SetFieldInfo
178183
| DependenciesUpdateInfo;
179184

src/useForm.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,14 @@ export class FormStore {
567567
!matchNamePath(field.getNamePath(), namePath),
568568
)
569569
) {
570-
this.store = setValue(this.store, namePath, defaultValue, true);
570+
const prevStore = this.store;
571+
this.store = setValue(prevStore, namePath, defaultValue, true);
572+
573+
// Notify that field is unmount
574+
this.notifyObservers(prevStore, [namePath], { type: 'remove' });
575+
576+
// Dependencies update
577+
this.triggerDependenciesUpdate(prevStore, namePath);
571578
}
572579
}
573580
};
@@ -608,6 +615,24 @@ export class FormStore {
608615
}
609616
};
610617

618+
/**
619+
* Notify dependencies children with parent update
620+
* We need delay to trigger validate in case Field is under render props
621+
*/
622+
private triggerDependenciesUpdate = (prevStore: Store, namePath: InternalNamePath) => {
623+
const childrenFields = this.getDependencyChildrenFields(namePath);
624+
if (childrenFields.length) {
625+
this.validateFields(childrenFields);
626+
}
627+
628+
this.notifyObservers(prevStore, childrenFields, {
629+
type: 'dependenciesUpdate',
630+
relatedFields: [namePath, ...childrenFields],
631+
});
632+
633+
return childrenFields;
634+
};
635+
611636
private updateValue = (name: NamePath, value: StoreValue) => {
612637
const namePath = getNamePath(name);
613638
const prevStore = this.store;
@@ -618,17 +643,8 @@ export class FormStore {
618643
source: 'internal',
619644
});
620645

621-
// Notify dependencies children with parent update
622-
// We need delay to trigger validate in case Field is under render props
623-
const childrenFields = this.getDependencyChildrenFields(namePath);
624-
if (childrenFields.length) {
625-
this.validateFields(childrenFields);
626-
}
627-
628-
this.notifyObservers(prevStore, childrenFields, {
629-
type: 'dependenciesUpdate',
630-
relatedFields: [namePath, ...childrenFields],
631-
});
646+
// Dependencies update
647+
const childrenFields = this.triggerDependenciesUpdate(prevStore, namePath);
632648

633649
// trigger callback function
634650
const { onValuesChange } = this.callbacks;

src/utils/valueUtil.ts

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

66
/**

tests/preserve.test.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,5 +315,128 @@ describe('Form.Preserve', () => {
315315

316316
wrapper.unmount();
317317
});
318+
319+
// https://github.com/ant-design/ant-design/issues/31297
320+
describe('A -> B -> C should keep trigger refresh', () => {
321+
it('shouldUpdate', () => {
322+
const DepDemo = () => {
323+
const [form] = Form.useForm();
324+
325+
return (
326+
<Form form={form} preserve={false}>
327+
<Form.Field name="name">
328+
<Input id="name" placeholder="Username" />
329+
</Form.Field>
330+
331+
<Form.Field shouldUpdate>
332+
{() => {
333+
return form.getFieldValue('name') === '1' ? (
334+
<Form.Field name="password">
335+
<Input id="password" placeholder="Password" />
336+
</Form.Field>
337+
) : null;
338+
}}
339+
</Form.Field>
340+
341+
<Form.Field shouldUpdate>
342+
{() => {
343+
const password = form.getFieldValue('password');
344+
return password ? (
345+
<Form.Field name="password2">
346+
<Input id="password2" placeholder="Password 2" />
347+
</Form.Field>
348+
) : null;
349+
}}
350+
</Form.Field>
351+
</Form>
352+
);
353+
};
354+
355+
const wrapper = mount(<DepDemo />);
356+
357+
// Input name to show password
358+
wrapper
359+
.find('#name')
360+
.last()
361+
.simulate('change', { target: { value: '1' } });
362+
expect(wrapper.exists('#password')).toBeTruthy();
363+
expect(wrapper.exists('#password2')).toBeFalsy();
364+
365+
// Input password to show password2
366+
wrapper
367+
.find('#password')
368+
.last()
369+
.simulate('change', { target: { value: '1' } });
370+
expect(wrapper.exists('#password2')).toBeTruthy();
371+
372+
// Change name to hide password
373+
wrapper
374+
.find('#name')
375+
.last()
376+
.simulate('change', { target: { value: '2' } });
377+
expect(wrapper.exists('#password')).toBeFalsy();
378+
expect(wrapper.exists('#password2')).toBeFalsy();
379+
});
380+
381+
it('dependencies', () => {
382+
const DepDemo = () => {
383+
const [form] = Form.useForm();
384+
385+
return (
386+
<Form form={form} preserve={false}>
387+
<Form.Field name="name">
388+
<Input id="name" placeholder="Username" />
389+
</Form.Field>
390+
391+
<Form.Field dependencies={['name']}>
392+
{() => {
393+
return form.getFieldValue('name') === '1' ? (
394+
<Form.Field name="password">
395+
<Input id="password" placeholder="Password" />
396+
</Form.Field>
397+
) : null;
398+
}}
399+
</Form.Field>
400+
401+
<Form.Field dependencies={['password']}>
402+
{() => {
403+
const password = form.getFieldValue('password');
404+
return password ? (
405+
<Form.Field name="password2">
406+
<Input id="password2" placeholder="Password 2" />
407+
</Form.Field>
408+
) : null;
409+
}}
410+
</Form.Field>
411+
</Form>
412+
);
413+
};
414+
415+
const wrapper = mount(<DepDemo />);
416+
417+
// Input name to show password
418+
wrapper
419+
.find('#name')
420+
.last()
421+
.simulate('change', { target: { value: '1' } });
422+
expect(wrapper.exists('#password')).toBeTruthy();
423+
expect(wrapper.exists('#password2')).toBeFalsy();
424+
425+
// Input password to show password2
426+
wrapper
427+
.find('#password')
428+
.last()
429+
.simulate('change', { target: { value: '1' } });
430+
expect(wrapper.exists('#password2')).toBeTruthy();
431+
432+
// Change name to hide password
433+
wrapper
434+
.find('#name')
435+
.last()
436+
.simulate('change', { target: { value: '2' } });
437+
expect(wrapper.exists('#password')).toBeFalsy();
438+
expect(wrapper.exists('#password2')).toBeFalsy();
439+
});
440+
});
318441
});
319442
/* eslint-enable no-template-curly-in-string */

0 commit comments

Comments
 (0)