Skip to content

Commit ad4256d

Browse files
crazyairzombieJ
andauthored
Form.List name 不在 onFinish 取值中 (#761)
* feat: useWatch support dynamic names * fix: lint * feat: test * feat: test * feat: use strict * feat: test * feat: review * feat: review * feat: add test * feat: review * feat: review * feat: review * chore: adjust logic * chore: comment * chore: comment * chore: comment * chore: fix logic * chore: clean up --------- Co-authored-by: 二货机器人 <smith3816@gmail.com>
1 parent 299b6eb commit ad4256d

File tree

6 files changed

+220
-23
lines changed

6 files changed

+220
-23
lines changed

docs/demo/list-unmount.md

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

docs/examples/list-unmount.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { useState } from 'react';
2+
import Form from 'rc-field-form';
3+
import Input from './components/Input';
4+
import LabelField from './components/LabelField';
5+
6+
const Demo = () => {
7+
const [form] = Form.useForm();
8+
const [isShow, setIsShow] = useState(true);
9+
10+
return (
11+
<div>
12+
<Form
13+
form={form}
14+
onFinish={values => {
15+
console.log(JSON.stringify(values, null, 2));
16+
console.log(JSON.stringify(form.getFieldsValue({ strict: true }), null, 2));
17+
}}
18+
initialValues={{
19+
users: [
20+
{ name: 'a', age: '1' },
21+
{ name: 'b', age: '2' },
22+
],
23+
}}
24+
>
25+
<Form.Field shouldUpdate>{() => JSON.stringify(form.getFieldsValue(), null, 2)}</Form.Field>
26+
27+
<Form.List name="users">
28+
{fields => {
29+
return (
30+
<div>
31+
{fields.map(field => (
32+
<div key={field.key} style={{ display: 'flex', gap: 10 }}>
33+
<LabelField name={[field.name, 'name']}>
34+
<Input />
35+
</LabelField>
36+
{isShow && (
37+
<LabelField name={[field.name, 'age']}>
38+
<Input />
39+
</LabelField>
40+
)}
41+
</div>
42+
))}
43+
</div>
44+
);
45+
}}
46+
</Form.List>
47+
<button type="button" onClick={() => setIsShow(c => !c)}>
48+
隐藏
49+
</button>
50+
<button type="submit">Submit</button>
51+
</Form>
52+
</div>
53+
);
54+
};
55+
56+
export default Demo;

src/interface.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ export interface FieldEntity {
116116
dependencies?: NamePath[];
117117
initialValue?: any;
118118
};
119+
120+
/**
121+
* Mask as invalidate.
122+
* This will filled when Field is removed but not updated in render yet.
123+
*/
124+
INVALIDATE_NAME_PATH?: InternalNamePath;
119125
}
120126

121127
export interface FieldError {
@@ -251,7 +257,13 @@ type RecursivePartial<T> =
251257

252258
export type FilterFunc = (meta: Meta | null) => boolean;
253259

254-
export type GetFieldsValueConfig = { strict?: boolean; filter?: FilterFunc };
260+
export type GetFieldsValueConfig = {
261+
/**
262+
* @deprecated `strict` is deprecated and not working anymore
263+
*/
264+
strict?: boolean;
265+
filter?: FilterFunc;
266+
};
255267

256268
export interface FormInstance<Values = any> {
257269
// Origin Form API

src/useForm.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
} from './utils/valueUtil';
4141
import type { BatchTask } from './BatchUpdate';
4242

43-
type InvalidateFieldEntity = { INVALIDATE_NAME_PATH: InternalNamePath };
43+
type FlexibleFieldEntity = Partial<FieldEntity>;
4444

4545
interface UpdateAction {
4646
type: 'updateValue';
@@ -282,9 +282,7 @@ export class FormStore {
282282
return cache;
283283
};
284284

285-
private getFieldEntitiesForNamePathList = (
286-
nameList?: NamePath[],
287-
): (FieldEntity | InvalidateFieldEntity)[] => {
285+
private getFieldEntitiesForNamePathList = (nameList?: NamePath[]): FlexibleFieldEntity[] => {
288286
if (!nameList) {
289287
return this.getFieldEntities(true);
290288
}
@@ -304,13 +302,11 @@ export class FormStore {
304302
// Fill args
305303
let mergedNameList: NamePath[] | true;
306304
let mergedFilterFunc: FilterFunc;
307-
let mergedStrict: boolean;
308305

309306
if (nameList === true || Array.isArray(nameList)) {
310307
mergedNameList = nameList;
311308
mergedFilterFunc = filterFunc;
312309
} else if (nameList && typeof nameList === 'object') {
313-
mergedStrict = nameList.strict;
314310
mergedFilterFunc = nameList.filter;
315311
}
316312

@@ -323,17 +319,15 @@ export class FormStore {
323319
);
324320

325321
const filteredNameList: NamePath[] = [];
326-
fieldEntities.forEach((entity: FieldEntity | InvalidateFieldEntity) => {
327-
const namePath =
328-
'INVALIDATE_NAME_PATH' in entity ? entity.INVALIDATE_NAME_PATH : entity.getNamePath();
322+
const listNamePaths: InternalNamePath[] = [];
323+
324+
fieldEntities.forEach((entity: FlexibleFieldEntity) => {
325+
const namePath = entity.INVALIDATE_NAME_PATH || entity.getNamePath();
329326

330327
// Ignore when it's a list item and not specific the namePath,
331328
// since parent field is already take in count
332-
if (mergedStrict) {
333-
if ((entity as FieldEntity).isList?.()) {
334-
return;
335-
}
336-
} else if (!mergedNameList && (entity as FieldEntity).isListField?.()) {
329+
if ((entity as FieldEntity).isList?.()) {
330+
listNamePaths.push(namePath);
337331
return;
338332
}
339333

@@ -347,7 +341,16 @@ export class FormStore {
347341
}
348342
});
349343

350-
return cloneByNamePathList(this.store, filteredNameList.map(getNamePath));
344+
let mergedValues = cloneByNamePathList(this.store, filteredNameList.map(getNamePath));
345+
346+
// We need fill the list as [] if Form.List is empty
347+
listNamePaths.forEach(namePath => {
348+
if (!getValue(mergedValues, namePath)) {
349+
mergedValues = setValue(mergedValues, namePath, []);
350+
}
351+
});
352+
353+
return mergedValues;
351354
};
352355

353356
private getFieldValue = (name: NamePath) => {
@@ -363,7 +366,7 @@ export class FormStore {
363366
const fieldEntities = this.getFieldEntitiesForNamePathList(nameList);
364367

365368
return fieldEntities.map((entity, index) => {
366-
if (entity && !('INVALIDATE_NAME_PATH' in entity)) {
369+
if (entity && !entity.INVALIDATE_NAME_PATH) {
367370
return {
368371
name: entity.getNamePath(),
369372
errors: entity.getErrors(),
@@ -781,7 +784,10 @@ export class FormStore {
781784

782785
if (onValuesChange) {
783786
const changedValues = cloneByNamePathList(this.store, [namePath]);
784-
onValuesChange(changedValues, this.getFieldsValue());
787+
const allValues = this.getFieldsValue();
788+
// Merge changedValues into allValues to ensure allValues contains the latest changes
789+
const mergedAllValues = merge(allValues, changedValues);
790+
onValuesChange(changedValues, mergedAllValues);
785791
}
786792

787793
this.triggerOnFieldsChange([namePath, ...childrenFields]);
@@ -910,6 +916,8 @@ export class FormStore {
910916
const namePathList: InternalNamePath[] | undefined = provideNameList
911917
? nameList.map(getNamePath)
912918
: [];
919+
// Same namePathList, but does not include Form.List name
920+
const finalValueNamePathList = [...namePathList];
913921

914922
// Collect result in promise list
915923
const promiseList: Promise<FieldError>[] = [];
@@ -921,9 +929,19 @@ export class FormStore {
921929
const { recursive, dirty } = options || {};
922930

923931
this.getFieldEntities(true).forEach((field: FieldEntity) => {
932+
const fieldNamePath = field.getNamePath();
933+
924934
// Add field if not provide `nameList`
925935
if (!provideNameList) {
926-
namePathList.push(field.getNamePath());
936+
if (
937+
// If is field, pass directly
938+
!field.isList() ||
939+
// If is list, do not add if already exist sub field in the namePathList
940+
!namePathList.some(name => matchNamePath(name, fieldNamePath, true))
941+
) {
942+
finalValueNamePathList.push(fieldNamePath);
943+
}
944+
namePathList.push(fieldNamePath);
927945
}
928946

929947
// Skip if without rule
@@ -936,7 +954,6 @@ export class FormStore {
936954
return;
937955
}
938956

939-
const fieldNamePath = field.getNamePath();
940957
validateNamePathList.add(fieldNamePath.join(TMP_SPLIT));
941958

942959
// Add field validate rule in to promise list
@@ -1000,7 +1017,7 @@ export class FormStore {
10001017
const returnPromise: Promise<Store | ValidateErrorEntity | string[]> = summaryPromise
10011018
.then((): Promise<Store | string[]> => {
10021019
if (this.lastValidatePromise === summaryPromise) {
1003-
return Promise.resolve(this.getFieldsValue(namePathList));
1020+
return Promise.resolve(this.getFieldsValue(finalValueNamePathList));
10041021
}
10051022
return Promise.reject<string[]>([]);
10061023
})

src/utils/valueUtil.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export function getNamePath(path: NamePath | null): InternalNamePath {
1616
return toArray(path);
1717
}
1818

19+
/**
20+
* Create a new store object that contains only the values referenced by
21+
* the provided list of name paths.
22+
*/
1923
export function cloneByNamePathList(store: Store, namePathList: InternalNamePath[]): Store {
2024
let newStore = {};
2125
namePathList.forEach(namePath => {

tests/list.test.tsx

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { fireEvent, render, act } from '@testing-library/react';
33
import { resetWarned } from '@rc-component/util/lib/warning';
44
import Form, { Field, List } from '../src';
@@ -569,7 +569,7 @@ describe('Form.List', () => {
569569
expect(currentMeta.errors).toEqual(['Bamboo Light']);
570570
});
571571

572-
it('Nest list remove should trigger correct onValuesChange', () => {
572+
it('Nest list remove index should trigger correct onValuesChange', () => {
573573
const onValuesChange = jest.fn();
574574

575575
const [container] = generateForm(
@@ -596,6 +596,7 @@ describe('Form.List', () => {
596596
},
597597
);
598598

599+
onValuesChange.mockReset();
599600
fireEvent.click(container.querySelector('button')!);
600601
expect(onValuesChange).toHaveBeenCalledWith(expect.anything(), { list: [{ first: 'light' }] });
601602
});
@@ -937,4 +938,108 @@ describe('Form.List', () => {
937938

938939
expect(formRef.current!.getFieldValue('list')).toEqual([{ user: '1' }, { user: '3' }]);
939940
});
941+
942+
it('list unmount', async () => {
943+
const onFinish = jest.fn();
944+
const formRef = React.createRef<FormInstance>();
945+
946+
const Demo = () => {
947+
const [isShow, setIsShow] = useState(true);
948+
return (
949+
<Form
950+
initialValues={{
951+
users: [
952+
{ name: 'a', age: '1' },
953+
{ name: 'b', age: '2' },
954+
],
955+
}}
956+
ref={formRef}
957+
onFinish={onFinish}
958+
>
959+
<Form.List name="users">
960+
{fields => {
961+
return fields.map(field => (
962+
<div key={field.key} style={{ display: 'flex', gap: 10 }}>
963+
<InfoField name={[field.name, 'name']}>
964+
<Input />
965+
</InfoField>
966+
{isShow && (
967+
<InfoField name={[field.name, 'age']}>
968+
<Input />
969+
</InfoField>
970+
)}
971+
</div>
972+
));
973+
}}
974+
</Form.List>
975+
<button data-testid="hide" type="button" onClick={() => setIsShow(c => !c)}>
976+
隐藏
977+
</button>
978+
<button type="submit" data-testid="submit">
979+
Submit
980+
</button>
981+
</Form>
982+
);
983+
};
984+
985+
const { queryByTestId } = render(<Demo />);
986+
fireEvent.click(queryByTestId('submit'));
987+
await act(async () => {
988+
await timeout();
989+
});
990+
expect(onFinish).toHaveBeenCalledWith({
991+
users: [
992+
{ name: 'a', age: '1' },
993+
{ name: 'b', age: '2' },
994+
],
995+
});
996+
expect(formRef.current?.getFieldsValue()).toEqual({
997+
users: [
998+
{ name: 'a', age: '1' },
999+
{ name: 'b', age: '2' },
1000+
],
1001+
});
1002+
onFinish.mockReset();
1003+
1004+
fireEvent.click(queryByTestId('hide'));
1005+
fireEvent.click(queryByTestId('submit'));
1006+
await act(async () => {
1007+
await timeout();
1008+
});
1009+
expect(onFinish).toHaveBeenCalledWith({ users: [{ name: 'a' }, { name: 'b' }] });
1010+
expect(formRef.current?.getFieldsValue()).toEqual({
1011+
users: [{ name: 'a' }, { name: 'b' }],
1012+
});
1013+
});
1014+
1015+
it('list rules', async () => {
1016+
const onFinishFailed = jest.fn();
1017+
1018+
const Demo = () => {
1019+
return (
1020+
<Form onFinishFailed={onFinishFailed}>
1021+
<Form.List name="users" rules={[{ validator: () => Promise.reject('error') }]}>
1022+
{fields => {
1023+
return fields.map(field => (
1024+
<InfoField name={[field.name, 'name']} key={field.key}>
1025+
<Input />
1026+
</InfoField>
1027+
));
1028+
}}
1029+
</Form.List>
1030+
<button type="submit" data-testid="submit">
1031+
Submit
1032+
</button>
1033+
</Form>
1034+
);
1035+
};
1036+
1037+
const { queryByTestId } = render(<Demo />);
1038+
fireEvent.click(queryByTestId('submit'));
1039+
await act(async () => {
1040+
await timeout();
1041+
});
1042+
1043+
expect(onFinishFailed).toHaveBeenCalled();
1044+
});
9401045
});

0 commit comments

Comments
 (0)