Skip to content

Commit f5f327e

Browse files
committed
fix: ensure add() works
1 parent 6925571 commit f5f327e

File tree

4 files changed

+117
-110
lines changed

4 files changed

+117
-110
lines changed

packages/components/form/FormItem.tsx

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -410,29 +410,21 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
410410
}, [shouldUpdate, form]);
411411

412412
useEffect(() => {
413-
// 记录填写 name 属性 formItem
414413
if (typeof name === 'undefined') return;
415414

416-
// FormList 下特殊处理
417-
if (formListName && isSameForm) {
418-
formListMapRef.current.set(fullPath, formItemRef);
419-
set(form?.store, fullPath, defaultInitialData);
420-
setFormValue(defaultInitialData);
421-
return () => {
422-
// eslint-disable-next-line react-hooks/exhaustive-deps
423-
formListMapRef.current.delete(fullPath);
424-
set(form?.store, fullPath, defaultInitialData);
425-
};
426-
}
415+
const isFormList = formListName && isSameForm;
416+
const mapRef = isFormList ? formListMapRef : formMapRef;
417+
if (!mapRef.current) return;
418+
419+
// 注册实例
420+
mapRef.current.set(fullPath, formItemRef);
427421

428-
if (!formMapRef) return;
429-
formMapRef.current.set(fullPath, formItemRef);
422+
// 初始化
430423
set(form?.store, fullPath, defaultInitialData);
431424
setFormValue(defaultInitialData);
432425

433426
return () => {
434-
// eslint-disable-next-line react-hooks/exhaustive-deps
435-
formMapRef.current.delete(fullPath);
427+
mapRef.current.delete(fullPath);
436428
set(form?.store, fullPath, defaultInitialData);
437429
};
438430
// eslint-disable-next-line react-hooks/exhaustive-deps

packages/components/form/FormList.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const FormList: React.FC<TdFormListProps> = (props) => {
2626
const fullPath = concatName(parentFullPath, name);
2727

2828
const initialData = useMemo(() => {
29+
const storeValue = get(form?.store, fullPath);
30+
if (storeValue) return storeValue;
31+
2932
let propsInitialData;
3033
if (props.initialData) {
3134
propsInitialData = props.initialData;
@@ -35,16 +38,10 @@ const FormList: React.FC<TdFormListProps> = (props) => {
3538
} else {
3639
propsInitialData = get(initialDataFromForm, fullPath);
3740
}
38-
return cloneDeep(propsInitialData);
39-
}, [fullPath, parentFullPath, initialDataFromForm, parentInitialData, props.initialData]);
41+
return cloneDeep(propsInitialData || []);
42+
}, [props.initialData, form?.store, fullPath, parentFullPath, parentInitialData, initialDataFromForm]);
4043

41-
const [formListValue, setFormListValue] = useState(() => {
42-
const value = get(form?.store, fullPath) || initialData || [];
43-
if (value.length && !get(form?.store, fullPath)) {
44-
set(form?.store, fullPath, value);
45-
}
46-
return value;
47-
});
44+
const [formListValue, setFormListValue] = useState(initialData);
4845

4946
const [fields, setFields] = useState<FormListField[]>(() =>
5047
formListValue.map((data, index) => ({
@@ -131,7 +128,9 @@ const FormList: React.FC<TdFormListProps> = (props) => {
131128

132129
useEffect(() => {
133130
if (!name || !formMapRef) return;
131+
// 初始化
134132
formMapRef.current.set(fullPath, formListRef);
133+
set(form?.store, fullPath, initialData);
135134
return () => {
136135
// eslint-disable-next-line react-hooks/exhaustive-deps
137136
formMapRef.current.delete(fullPath);

packages/components/form/__tests__/form-list.test.tsx

Lines changed: 92 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ const BasicForm = (props: FormProps & { operation }) => {
5353
);
5454
};
5555

56-
describe('Form List 组件测试', () => {
57-
test('form list 测试', async () => {
56+
describe('FormList 组件测试', () => {
57+
test('FormList basic API', async () => {
5858
const TestView = () => {
5959
const [form] = Form.useForm();
6060

@@ -143,7 +143,7 @@ describe('Form List 组件测试', () => {
143143
expect(queryByText('地区必填')).not.toBeTruthy();
144144
});
145145

146-
test('reset to initial data', async () => {
146+
test('FormList reset to initial data', async () => {
147147
const TestView = () => {
148148
const [form] = Form.useForm();
149149

@@ -165,14 +165,40 @@ describe('Form List 组件测试', () => {
165165
};
166166

167167
const { container, queryByText, getByPlaceholderText } = render(<TestView />);
168-
const resetBtn = queryByText('reset');
169168

170-
const removeBtn = container.querySelector('.test-remove-0');
171-
fireEvent.click(removeBtn);
169+
// 验证初始数据渲染正确
170+
expect((getByPlaceholderText('area-input-0') as HTMLInputElement).value).toBe('shenzhen');
171+
expect((getByPlaceholderText('area-input-1') as HTMLInputElement).value).toBe('beijing');
172+
173+
// 删除 beijing
174+
const removeBtn1 = container.querySelector('.test-remove-1');
175+
fireEvent.click(removeBtn1);
172176
await mockTimeout(() => true);
173-
expect((getByPlaceholderText('area-input-0') as HTMLInputElement).value).toBe('beijing');
177+
// 只剩 shenzhen
178+
expect((getByPlaceholderText('area-input-0') as HTMLInputElement).value).toBe('shenzhen');
179+
expect(container.querySelector('[placeholder="area-input-1"]')).toBeFalsy();
180+
181+
// 添加空数据
182+
const addBtn = container.querySelector('#test-add');
183+
fireEvent.click(addBtn);
184+
await mockTimeout(() => true);
185+
expect((getByPlaceholderText('area-input-0') as HTMLInputElement).value).toBe('shenzhen');
186+
expect(container.querySelector('[placeholder="area-input-1"]')).toBeTruthy();
187+
expect((getByPlaceholderText('area-input-1') as HTMLInputElement).value).toBe('');
188+
189+
// 再删除 shenzhen
190+
const removeBtn0 = container.querySelector('.test-remove-0');
191+
fireEvent.click(removeBtn0);
192+
await mockTimeout(() => true);
193+
expect((getByPlaceholderText('area-input-0') as HTMLInputElement).value).toBe('');
194+
expect(container.querySelector('[placeholder="area-input-1"]')).toBeFalsy();
195+
196+
// 点击 reset 重置
197+
const resetBtn = queryByText('reset');
174198
fireEvent.click(resetBtn);
175199
await mockTimeout(() => true);
200+
201+
// 恢复到初始化数据
176202
expect((getByPlaceholderText('area-input-0') as HTMLInputElement).value).toBe('shenzhen');
177203
expect((getByPlaceholderText('area-input-1') as HTMLInputElement).value).toBe('beijing');
178204
});
@@ -220,7 +246,7 @@ describe('Form List 组件测试', () => {
220246
expect(fn).toHaveBeenCalledTimes(1);
221247
});
222248

223-
test('Multiple nested FormList', async () => {
249+
test('FormList with nested structures', async () => {
224250
const TestView = () => {
225251
const [form] = Form.useForm();
226252

@@ -343,88 +369,74 @@ describe('Form List 组件测试', () => {
343369
<FormList name="users">
344370
{(userFields, { add: addUser, remove: removeUser }) => (
345371
<>
346-
{userFields.map(({ key: userKey, name: userName, ...userRestField }, userIndex) => (
372+
{userFields.map(({ key: userKey, name: userName }, userIndex) => (
347373
<FormItem key={userKey}>
348-
<FormItem
349-
{...userRestField}
350-
name={[userName, 'name']}
351-
label="用户名"
352-
rules={[{ required: true, type: 'error' }]}
353-
>
374+
<FormItem name={[userName, 'name']} label="用户名" rules={[{ required: true, type: 'error' }]}>
354375
<Input placeholder={`user-name-${userIndex}`} />
355376
</FormItem>
356377

357378
<FormList name={[userName, 'projects']}>
358379
{(projectFields, { add: addProject, remove: removeProject }) => (
359380
<>
360-
{projectFields.map(
361-
({ key: projectKey, name: projectName, ...projectRestField }, projectIndex) => (
362-
<FormItem key={projectKey}>
363-
<FormItem
364-
{...projectRestField}
365-
name={[projectName, 'projectName']}
366-
label="项目名称"
367-
rules={[{ required: true, type: 'error' }]}
368-
>
369-
<Input placeholder={`project-name-${userIndex}-${projectIndex}`} />
370-
</FormItem>
371-
372-
<FormList name={[projectName, 'tasks']}>
373-
{(taskFields, { add: addTask, remove: removeTask }) => (
374-
<>
375-
{taskFields.map(
376-
({ key: taskKey, name: taskName, ...taskRestField }, taskIndex) => (
377-
<FormItem key={taskKey}>
378-
<FormItem
379-
{...taskRestField}
380-
name={[taskName, 'taskName']}
381-
label="任务名称"
382-
rules={[{ required: true, type: 'error' }]}
383-
>
384-
<Input
385-
placeholder={`task-name-${userIndex}-${projectIndex}-${taskIndex}`}
386-
/>
387-
</FormItem>
388-
<FormItem
389-
{...taskRestField}
390-
name={[taskName, 'status']}
391-
label="状态"
392-
rules={[{ required: true, type: 'error' }]}
393-
>
394-
<Input
395-
placeholder={`task-status-${userIndex}-${projectIndex}-${taskIndex}`}
396-
/>
397-
</FormItem>
398-
<FormItem>
399-
<MinusCircleIcon
400-
className={`test-remove-task-${userIndex}-${projectIndex}-${taskIndex}`}
401-
onClick={() => removeTask(taskName)}
402-
/>
403-
</FormItem>
404-
</FormItem>
405-
),
406-
)}
407-
<FormItem>
408-
<Button
409-
id={`test-add-task-${userIndex}-${projectIndex}`}
410-
onClick={() => addTask({ taskName: 'New Task', status: 'pending' })}
381+
{projectFields.map(({ key: projectKey, name: projectName }, projectIndex) => (
382+
<FormItem key={projectKey}>
383+
<FormItem
384+
name={[projectName, 'projectName']}
385+
label="项目名称"
386+
rules={[{ required: true, type: 'error' }]}
387+
>
388+
<Input placeholder={`project-name-${userIndex}-${projectIndex}`} />
389+
</FormItem>
390+
391+
<FormList name={[projectName, 'tasks']}>
392+
{(taskFields, { add: addTask, remove: removeTask }) => (
393+
<>
394+
{taskFields.map(({ key: taskKey, name: taskName }, taskIndex) => (
395+
<FormItem key={taskKey}>
396+
<FormItem
397+
name={[taskName, 'taskName']}
398+
label="任务名称"
399+
rules={[{ required: true, type: 'error' }]}
400+
>
401+
<Input placeholder={`task-name-${userIndex}-${projectIndex}-${taskIndex}`} />
402+
</FormItem>
403+
<FormItem
404+
name={[taskName, 'status']}
405+
label="状态"
406+
rules={[{ required: true, type: 'error' }]}
411407
>
412-
Add Task
413-
</Button>
408+
<Input
409+
placeholder={`task-status-${userIndex}-${projectIndex}-${taskIndex}`}
410+
/>
411+
</FormItem>
412+
<FormItem>
413+
<MinusCircleIcon
414+
className={`test-remove-task-${userIndex}-${projectIndex}-${taskIndex}`}
415+
onClick={() => removeTask(taskName)}
416+
/>
417+
</FormItem>
414418
</FormItem>
415-
</>
416-
)}
417-
</FormList>
418-
419-
<FormItem>
420-
<MinusCircleIcon
421-
className={`test-remove-project-${userIndex}-${projectIndex}`}
422-
onClick={() => removeProject(projectName)}
423-
/>
424-
</FormItem>
419+
))}
420+
<FormItem>
421+
<Button
422+
id={`test-add-task-${userIndex}-${projectIndex}`}
423+
onClick={() => addTask({ taskName: 'New Task', status: 'pending' })}
424+
>
425+
Add Task
426+
</Button>
427+
</FormItem>
428+
</>
429+
)}
430+
</FormList>
431+
432+
<FormItem>
433+
<MinusCircleIcon
434+
className={`test-remove-project-${userIndex}-${projectIndex}`}
435+
onClick={() => removeProject(projectName)}
436+
/>
425437
</FormItem>
426-
),
427-
)}
438+
</FormItem>
439+
))}
428440
<FormItem>
429441
<Button
430442
id={`test-add-project-${userIndex}`}

packages/components/form/hooks/useFormItemInitialData.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect } from 'react';
2-
import { cloneDeep, get, isEmpty, unset } from 'lodash-es';
2+
import { get, has, isEmpty, unset } from 'lodash-es';
33

44
// 兼容特殊数据结构和受控 key
55
import Tree from '../../tree/Tree';
@@ -52,7 +52,7 @@ export default function useFormItemInitialData(
5252
}
5353
}, [hadReadFloatingFormData, floatingFormDataRef, formListName, name]);
5454

55-
const defaultInitialData = cloneDeep(getDefaultInitialData(children, initialData));
55+
const defaultInitialData = getDefaultInitialData(children, initialData);
5656

5757
// 优先级:floatFormData > FormItem.initialData > FormList.initialData > Form.initialData
5858
function getDefaultInitialData(children: FormItemProps['children'], initialData: FormItemProps['initialData']) {
@@ -71,9 +71,13 @@ export default function useFormItemInitialData(
7171
}
7272

7373
if (formListName && Array.isArray(fullPath)) {
74-
const storeValue = get(form.store, fullPath);
75-
if (typeof storeValue !== 'undefined') {
76-
return storeValue;
74+
const pathPrefix = fullPath.slice(0, -1);
75+
const pathExisted = has(form.store, pathPrefix);
76+
if (pathExisted) {
77+
// 只要路径存在,哪怕值为 undefined 也取 store 里的值
78+
// 兼容 add() 或者 add({}) 导致的空对象场景
79+
// https://github.com/Tencent/tdesign-react/issues/2329
80+
return get(form.store, fullPath);
7781
}
7882
}
7983

0 commit comments

Comments
 (0)