Skip to content

Commit aade667

Browse files
committed
fix: correct move operation to keep field.name in sync
1 parent d085c17 commit aade667

File tree

6 files changed

+82
-125
lines changed

6 files changed

+82
-125
lines changed

packages/components/form/Form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useEffect, useImperativeHandle, useRef } from 'react';
21
import classNames from 'classnames';
32
import { cloneDeep } from 'lodash-es';
3+
import React, { useEffect, useImperativeHandle, useRef } from 'react';
44
import forwardRefWithStatics from '../_util/forwardRefWithStatics';
55
import noop from '../_util/noop';
66
import useConfig from '../hooks/useConfig';

packages/components/form/FormContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const FormListContext = React.createContext<{
5353
name: NamePath;
5454
fullPath?: NamePath;
5555
rules: TdFormListProps['rules'];
56-
formListMapRef: React.RefObject<Map<any, React.RefObject<FormItemInstance>>>;
56+
formListMapRef: React.RefObject<Map<NamePath, React.RefObject<FormItemInstance>>>;
5757
initialData: TdFormListProps['initialData'];
5858
form?: InternalFormInstance;
5959
}>({

packages/components/form/FormItem.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { parseMessage, validate as validateModal } from './formModel';
1616
import { HOOK_MARK } from './hooks/useForm';
1717
import useFormItemInitialData, { ctrlKeyMap } from './hooks/useFormItemInitialData';
1818
import useFormItemStyle from './hooks/useFormItemStyle';
19-
import { calcFieldValue, concatNamePath } from './utils';
19+
import { calcFieldValue, concatName } from './utils';
2020

2121
import type { StyledProps } from '../common';
2222
import type {
@@ -106,7 +106,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
106106
const { getDefaultInitialData } = useFormItemInitialData(name);
107107

108108
const { fullPath: parentFullPath } = useFormListContext();
109-
const fullPath = concatNamePath(parentFullPath, name);
109+
const fullPath = concatName(parentFullPath, name);
110110

111111
const [, forceUpdate] = useState({}); // custom render state
112112
const [freeShowErrorMessage, setFreeShowErrorMessage] = useState(undefined);
@@ -434,8 +434,8 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
434434
formMapRef.current.set(fullPath, formItemRef);
435435
return () => {
436436
// eslint-disable-next-line react-hooks/exhaustive-deps
437-
formMapRef.current.delete(fullPath);
438-
unset(form?.store, name);
437+
// formMapRef.current.delete(fullPath);
438+
// unset(form?.store, name);
439439
};
440440
// eslint-disable-next-line react-hooks/exhaustive-deps
441441
}, [snakeName, formListName]);

packages/components/form/FormList.tsx

Lines changed: 54 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
2-
import { cloneDeep, get, isEqual, merge, set, unset } from 'lodash-es';
31
import log from '@tdesign/common-js/log/index';
2+
import { castArray, cloneDeep, get, isEqual, merge, set, unset } from 'lodash-es';
3+
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
44
import { FormListContext, useFormContext, useFormListContext } from './FormContext';
55
import { HOOK_MARK } from './hooks/useForm';
6-
import { calcFieldValue, concatNamePath, normalizeNamePath } from './utils';
6+
import { calcFieldValue, concatName, convertNameToArray, swap } from './utils';
77

88
import type { FormItemInstance } from './FormItem';
99
import type { FormListField, FormListFieldOperation, NamePath, TdFormListProps } from './type';
@@ -23,8 +23,8 @@ const FormList: React.FC<TdFormListProps> = (props) => {
2323
const { fullPath: parentFullPath, initialData: parentInitialData } = useFormListContext();
2424

2525
const fullPath = useMemo(() => {
26-
const normalizedName = normalizeNamePath(name);
27-
const normalizedParentPath = normalizeNamePath(parentFullPath);
26+
const normalizedName = convertNameToArray(name);
27+
const normalizedParentPath = convertNameToArray(parentFullPath);
2828
// 如果没有父路径,直接使用 name
2929
if (normalizedParentPath.length === 0) {
3030
return normalizedName;
@@ -36,34 +36,36 @@ const FormList: React.FC<TdFormListProps> = (props) => {
3636
normalizedParentPath.every((segment, index) => normalizedName[index] === segment);
3737
if (isAbsolutePath) return normalizedName;
3838
// 如果是相对路径,与父路径拼接
39-
return concatNamePath(parentFullPath, name);
39+
return concatName(parentFullPath, name);
4040
}, [parentFullPath, name]);
4141

4242
const initialData = useMemo(() => {
4343
let propsInitialData;
4444
if (props.initialData) {
4545
propsInitialData = props.initialData;
4646
} else if (parentFullPath && parentInitialData) {
47-
const relativePath = fullPath.slice(normalizeNamePath(parentFullPath).length);
47+
const relativePath = fullPath.slice(convertNameToArray(parentFullPath).length);
4848
propsInitialData = get(parentInitialData, relativePath);
4949
} else {
5050
propsInitialData = get(initialDataFromForm, fullPath);
5151
}
52-
return propsInitialData;
52+
return cloneDeep(propsInitialData);
5353
}, [fullPath, parentFullPath, initialDataFromForm, parentInitialData, props.initialData]);
5454

5555
const [formListValue, setFormListValue] = useState(() => get(form?.store, fullPath) || initialData || []);
5656
const [fields, setFields] = useState<FormListField[]>(() =>
57-
formListValue?.map((data, index) => ({
57+
formListValue.map((data, index) => ({
5858
data: { ...data },
5959
key: (globalKey += 1),
6060
name: index,
6161
isListField: true,
6262
})),
6363
);
6464

65-
const formListMapRef = useRef<Map<NamePath, React.RefObject<FormItemInstance>>>(new Map());
65+
// 暴露给 Form 的引用当前 FormList 实例
6666
const formListRef = useRef<FormItemInstance>(null);
67+
// 存储当前 FormList 下所有的 FormItem 实例
68+
const formListMapRef = useRef<Map<NamePath, React.RefObject<FormItemInstance>>>(new Map());
6769

6870
const snakeName = []
6971
.concat(name)
@@ -72,88 +74,56 @@ const FormList: React.FC<TdFormListProps> = (props) => {
7274

7375
const isMounted = useRef(false);
7476

75-
const buildDefaultFieldMap = () => {
76-
if (formListMapRef.current.size <= 0) return {};
77-
const defaultValues: Record<string, any> = {};
78-
formListMapRef.current.forEach((_, itemPath) => {
79-
const itemPathArray = normalizeNamePath(itemPath);
80-
if (itemPathArray.length !== normalizeNamePath(fullPath).length + 2) return;
81-
const fieldName = itemPathArray[itemPathArray.length - 1];
82-
// add 没有传参时,构建一个包含所有子字段名称的对象,用 undefined 作为值,仅用于占位,确保回调给用户的数据结构完整
83-
defaultValues[fieldName] = undefined;
84-
});
85-
return defaultValues;
77+
const updateFormList = (newFields: any, newFormListValue: any) => {
78+
const normalizedFields = newFields.map((f, index) => ({
79+
...f,
80+
name: index, // 重新规范 name 索引
81+
}));
82+
setFields(normalizedFields);
83+
setFormListValue(newFormListValue);
84+
set(form?.store, fullPath, newFormListValue);
85+
const changeValue = calcFieldValue(fullPath, newFormListValue);
86+
onFormItemValueChange?.(changeValue);
8687
};
8788

8889
const operation: FormListFieldOperation = {
8990
add(defaultValue?: any, insertIndex?: number) {
90-
const cloneFields = [...fields];
91-
const index = insertIndex ?? cloneFields.length;
92-
cloneFields.splice(index, 0, {
91+
const newFields = cloneDeep(fields);
92+
const index = insertIndex ?? newFields.length;
93+
94+
newFields.splice(index, 0, {
9395
key: (globalKey += 1),
9496
name: index,
9597
isListField: true,
9698
});
97-
cloneFields.forEach((field, index) => Object.assign(field, { name: index }));
98-
setFields(cloneFields);
99-
100-
const nextFormListValue = cloneDeep(formListValue);
101-
102-
let finalValue = defaultValue;
103-
if (finalValue === undefined) {
104-
finalValue = buildDefaultFieldMap();
105-
}
106-
if (finalValue) {
107-
nextFormListValue?.splice(index, 0, cloneDeep(finalValue));
108-
setFormListValue(nextFormListValue);
109-
}
110-
111-
set(form?.store, fullPath, nextFormListValue);
112-
const newPath = [...normalizeNamePath(fullPath), index];
113-
const fieldValue = calcFieldValue(newPath, finalValue);
114-
onFormItemValueChange?.(fieldValue);
99+
100+
const newFormListValue = cloneDeep(formListValue).splice(index, 0, cloneDeep(defaultValue));
101+
102+
updateFormList(newFields, newFormListValue);
115103
},
116104
remove(index: number | number[]) {
117-
const indices = Array.isArray(index) ? index : [index];
118-
119-
const nextFields = fields
120-
.filter((item) => !indices.includes(item.name))
121-
.map((field, i) => ({ ...field, name: i }));
122-
setFields(nextFields);
123-
124-
const nextFormListValue = cloneDeep(formListValue).filter((_, idx) => !indices.includes(idx));
125-
setFormListValue(nextFormListValue);
126-
if (nextFormListValue.length) {
127-
set(form?.store, fullPath, nextFormListValue);
128-
}
129-
const fieldValue = calcFieldValue(fullPath, nextFormListValue);
130-
onFormItemValueChange?.(fieldValue);
105+
const indices = castArray(index);
106+
107+
const newFields = fields.filter((f) => !indices.includes(f.name));
108+
const newFormListValue = cloneDeep(formListValue).filter((_, i) => !indices.includes(i));
109+
110+
updateFormList(newFields, newFormListValue);
131111
},
112+
132113
move(from: number, to: number) {
133-
const cloneFields = [...fields];
134-
const fromItem = { ...cloneFields[from] };
135-
const toItem = { ...cloneFields[to] };
136-
cloneFields[to] = fromItem;
137-
cloneFields[from] = toItem;
138-
set(form?.store, fullPath, []);
139-
setFields(cloneFields);
140-
},
141-
};
114+
const newFields = cloneDeep(fields);
115+
const newFormListValue = cloneDeep(formListValue);
142116

143-
const handleFieldUpdateTasks = (fieldData: any[], callback: Function) => {
144-
Array.from(formListMapRef.current.values()).forEach((formItemRef) => {
145-
if (!formItemRef.current) return;
146-
const { name: childName } = formItemRef.current;
147-
const data = get(fieldData, childName);
148-
if (data !== undefined) callback(formItemRef, data);
149-
});
150-
const fieldValue = calcFieldValue(fullPath, fieldData);
151-
onFormItemValueChange?.(fieldValue);
117+
swap(newFields, from, to);
118+
swap(newFormListValue, from, to);
119+
120+
updateFormList(newFields, newFormListValue);
121+
},
152122
};
153123

154124
function setListFields(fieldData: any[], callback: Function) {
155-
const currList = get(form?.store, fullPath) || [];
156-
if (isEqual(currList, fieldData)) return;
125+
const currList = get(form?.store, fullPath);
126+
if (isEqual([currList], fieldData)) return;
157127

158128
const newFields = fieldData.map((_, index) => {
159129
const currField = fields[index];
@@ -167,15 +137,19 @@ const FormList: React.FC<TdFormListProps> = (props) => {
167137
};
168138
});
169139

170-
setFields(newFields);
171-
set(form?.store, fullPath, fieldData);
172-
handleFieldUpdateTasks(fieldData, callback);
140+
Array.from(formListMapRef.current.values()).forEach((formItemRef) => {
141+
if (!formItemRef.current) return;
142+
const { name: childName } = formItemRef.current;
143+
const data = get(fieldData, childName);
144+
if (data !== undefined) callback(formItemRef, data);
145+
});
146+
147+
updateFormList(newFields, fieldData);
173148
}
174149

175150
useEffect(() => {
176151
if (!name || !formMapRef) return;
177152
formMapRef.current.set(fullPath, formListRef);
178-
179153
return () => {
180154
// eslint-disable-next-line react-hooks/exhaustive-deps
181155
formMapRef.current.delete(fullPath);

packages/components/form/hooks/useInstance.tsx

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { cloneDeep, get, isEmpty, isFunction, merge, set } from 'lodash-es';
22
import log from '@tdesign/common-js/log/index';
33
import useConfig from '../../hooks/useConfig';
4-
import { calcFieldValue, findFormItem, findFormItemDeep, objectToArray, travelMapFromObject } from '../utils';
4+
import { calcFieldValue, findFormItem, objectToArray, travelMapFromObject } from '../utils';
55

66
import type { FormItemInstance } from '../FormItem';
77
import type {
@@ -110,10 +110,7 @@ export default function useInstance(
110110
// 对外方法,获取对应 formItem 的值
111111
function getFieldValue(name: NamePath) {
112112
if (!name) return null;
113-
let formItemRef = findFormItem(name, formMapRef);
114-
if (!formItemRef) {
115-
formItemRef = findFormItemDeep(name, formMapRef);
116-
}
113+
const formItemRef = findFormItem(name, formMapRef);
117114
return formItemRef?.current?.getValue?.();
118115
}
119116

@@ -155,10 +152,7 @@ export default function useInstance(
155152
const nameLists = objectToArray(fields);
156153
nameLists.forEach((nameList) => {
157154
const fieldValue = get(fields, nameList);
158-
let formItemRef = findFormItem(nameList, formMapRef);
159-
if (!formItemRef) {
160-
formItemRef = findFormItemDeep(nameList, formMapRef);
161-
}
155+
const formItemRef = findFormItem(nameList, formMapRef);
162156
if (formItemRef?.current) {
163157
formItemRef.current.setValue?.(fieldValue);
164158
} else {
@@ -173,10 +167,7 @@ export default function useInstance(
173167

174168
fields.forEach((field) => {
175169
const { name, ...restFields } = field;
176-
let formItemRef = findFormItem(name, formMapRef);
177-
if (!formItemRef) {
178-
formItemRef = findFormItemDeep(name, formMapRef);
179-
}
170+
const formItemRef = findFormItem(name, formMapRef);
180171
formItemRef?.current?.setField(restFields);
181172
});
182173
}
@@ -190,10 +181,7 @@ export default function useInstance(
190181
} else {
191182
const { type = 'initial', fields = [] } = params;
192183
fields.forEach((name) => {
193-
let formItemRef = findFormItem(name, formMapRef);
194-
if (!formItemRef) {
195-
formItemRef = findFormItemDeep(name, formMapRef);
196-
}
184+
const formItemRef = findFormItem(name, formMapRef);
197185
formItemRef?.current?.resetField(type);
198186
});
199187
}
@@ -213,12 +201,8 @@ export default function useInstance(
213201
});
214202
} else {
215203
if (!Array.isArray(fields)) throw new TypeError('The parameter of "clearValidate" must be an array');
216-
217204
fields.forEach((name) => {
218-
let formItemRef = findFormItem(name, formMapRef);
219-
if (!formItemRef) {
220-
formItemRef = findFormItemDeep(name, formMapRef);
221-
}
205+
const formItemRef = findFormItem(name, formMapRef);
222206
formItemRef?.current?.resetValidate();
223207
});
224208
}
@@ -242,10 +226,7 @@ export default function useInstance(
242226
? [...formMapRef.current.values()]
243227
: fields
244228
.map((name) => {
245-
let formItemRef = findFormItem(name, formMapRef);
246-
if (!formItemRef) {
247-
formItemRef = findFormItemDeep(name, formMapRef);
248-
}
229+
const formItemRef = findFormItem(name, formMapRef);
249230
return formItemRef;
250231
})
251232
.filter(Boolean);

0 commit comments

Comments
 (0)