Skip to content

Commit da55f8c

Browse files
committed
fix: remove unset
1 parent 1632b05 commit da55f8c

File tree

7 files changed

+306
-26
lines changed

7 files changed

+306
-26
lines changed

packages/components/form/FormItem.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
CloseCircleFilledIcon as TdCloseCircleFilledIcon,
55
ErrorCircleFilledIcon as TdErrorCircleFilledIcon,
66
} from 'tdesign-icons-react';
7-
import { get, isEqual, isFunction, isObject, isString, set, unset } from 'lodash-es';
7+
import { get, isEqual, isFunction, isObject, isString, set } from 'lodash-es';
88

99
import useConfig from '../hooks/useConfig';
1010
import useDefaultProps from '../hooks/useDefaultProps';
@@ -423,7 +423,6 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
423423
return () => {
424424
// eslint-disable-next-line react-hooks/exhaustive-deps
425425
formListMapRef.current.delete(fullPath);
426-
unset(form?.store, fullPath);
427426
};
428427
}
429428
if (!formMapRef) return;
@@ -432,7 +431,6 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
432431
return () => {
433432
// eslint-disable-next-line react-hooks/exhaustive-deps
434433
formMapRef.current.delete(fullPath);
435-
unset(form?.store, fullPath);
436434
};
437435
// eslint-disable-next-line react-hooks/exhaustive-deps
438436
}, [snakeName, formListName]);

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

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fireEvent, mockTimeout, render, vi } from '@test/utils';
44

55
import Button from '../../button';
66
import Input from '../../input';
7+
import Radio from '../../radio';
78
import FormList from '../FormList';
89
import Form, { type FormProps } from '../index';
910

@@ -605,4 +606,199 @@ describe('Form List 组件测试', () => {
605606
await mockTimeout();
606607
expect(queryByText('用户名必填')).not.toBeTruthy();
607608
});
609+
610+
test('FormList with shouldUpdate', async () => {
611+
const TestView = () => {
612+
const [form] = Form.useForm();
613+
614+
const INIT_DATA = {
615+
services: [
616+
{
617+
modelName: 'modelA',
618+
routes: [
619+
{ type: 'weight', weight: 50, abtest: 'cid' },
620+
{ type: 'abtest', weight: 30, abtest: 'uid' },
621+
],
622+
},
623+
],
624+
};
625+
626+
return (
627+
<Form form={form} initialData={INIT_DATA}>
628+
<FormList name="services">
629+
{(fields) => (
630+
<>
631+
{fields.map(({ key, name: serviceName }) => (
632+
<div key={key}>
633+
<FormList name={[serviceName, 'routes']}>
634+
{(routeFields, { add: addRoute }) => (
635+
<div>
636+
{routeFields.map((f) => (
637+
<div key={f.key} data-route-index={f.name}>
638+
<FormItem name={[f.name, 'type']} label="类型">
639+
<Radio.Group
640+
options={[
641+
{ label: '权重', value: 'weight' },
642+
{ label: 'ABTest', value: 'abtest' },
643+
]}
644+
/>
645+
</FormItem>
646+
647+
<FormItem
648+
shouldUpdate={(p, n) =>
649+
p.services?.[serviceName]?.routes?.[f.name]?.type !==
650+
n.services?.[serviceName]?.routes?.[f.name]?.type
651+
}
652+
>
653+
{({ getFieldValue }) => {
654+
const type = getFieldValue(['services', serviceName, 'routes', f.name, 'type']);
655+
if (type === 'weight') {
656+
return (
657+
<FormItem key={type} name={[f.name, 'weight']} label="权重">
658+
<Input placeholder={`route-weight-${serviceName}-${f.name}`} />
659+
</FormItem>
660+
);
661+
}
662+
if (type === 'abtest') {
663+
return (
664+
<FormItem key={type} name={[f.name, 'abtest']} label="分流Key">
665+
<Input placeholder={`route-abtest-${serviceName}-${f.name}`} />
666+
</FormItem>
667+
);
668+
}
669+
return null;
670+
}}
671+
</FormItem>
672+
</div>
673+
))}
674+
<Button id={`test-add-route-${serviceName}-default`} onClick={() => addRoute()}>
675+
新增默认路由
676+
</Button>
677+
<Button
678+
id={`test-add-route-${serviceName}-specified`}
679+
onClick={() => addRoute(INIT_DATA.services[0].routes[0])}
680+
>
681+
新增指定路由
682+
</Button>
683+
</div>
684+
)}
685+
</FormList>
686+
</div>
687+
))}
688+
</>
689+
)}
690+
</FormList>
691+
</Form>
692+
);
693+
};
694+
695+
const { container, getByPlaceholderText } = render(<TestView />);
696+
697+
// Test initial data - first route (type: weight)
698+
const weightRadio0 = container.querySelector('[data-route-index="0"] input[value="weight"]') as HTMLInputElement;
699+
expect(weightRadio0.checked).toBe(true);
700+
expect((getByPlaceholderText('route-weight-0-0') as HTMLInputElement).value).toBe('50');
701+
expect(container.querySelector('[placeholder="route-abtest-0-0"]')).toBeFalsy();
702+
703+
// Test initial data - second route (type: abtest)
704+
const abtestRadio1 = container.querySelector('[data-route-index="1"] input[value="abtest"]') as HTMLInputElement;
705+
expect(abtestRadio1.checked).toBe(true);
706+
expect((getByPlaceholderText('route-abtest-0-1') as HTMLInputElement).value).toBe('uid');
707+
expect(container.querySelector('[placeholder="route-weight-0-1"]')).toBeFalsy();
708+
709+
// Test switching first route from weight to abtest
710+
const abtestRadio0 = container.querySelector('[data-route-index="0"] input[value="abtest"]') as HTMLInputElement;
711+
fireEvent.click(abtestRadio0);
712+
await mockTimeout();
713+
expect((getByPlaceholderText('route-abtest-0-0') as HTMLInputElement).value).toBe('cid');
714+
expect(container.querySelector('[placeholder="route-weight-0-0"]')).toBeFalsy();
715+
716+
// Test switching first route back to weight
717+
fireEvent.click(weightRadio0);
718+
await mockTimeout();
719+
expect((getByPlaceholderText('route-weight-0-0') as HTMLInputElement).value).toBe('50');
720+
expect(container.querySelector('[placeholder="route-abtest-0-0"]')).toBeFalsy();
721+
722+
// Test switching second route from abtest to weight
723+
const weightRadio1 = container.querySelector('[data-route-index="1"] input[value="weight"]') as HTMLInputElement;
724+
fireEvent.click(weightRadio1);
725+
await mockTimeout();
726+
expect((getByPlaceholderText('route-weight-0-1') as HTMLInputElement).value).toBe('30');
727+
expect(container.querySelector('[placeholder="route-abtest-0-1"]')).toBeFalsy();
728+
729+
// Test switching second route back to abtest
730+
fireEvent.click(abtestRadio1);
731+
await mockTimeout();
732+
expect((getByPlaceholderText('route-abtest-0-1') as HTMLInputElement).value).toBe('uid');
733+
expect(container.querySelector('[placeholder="route-weight-0-1"]')).toBeFalsy();
734+
735+
// Test adding default route (empty data)
736+
const addDefaultBtn = container.querySelector('#test-add-route-0-default');
737+
fireEvent.click(addDefaultBtn);
738+
await mockTimeout();
739+
const newRouteRadios = container.querySelectorAll('[data-route-index="2"] input[type="radio"]');
740+
expect(newRouteRadios.length).toBe(2);
741+
// No radio should be checked initially
742+
const checkedRadio = container.querySelector('[data-route-index="2"] input[type="radio"]:checked');
743+
expect(checkedRadio).toBeFalsy();
744+
// No conditional field should be rendered when type is empty
745+
expect(container.querySelector('[placeholder="route-weight-0-2"]')).toBeFalsy();
746+
expect(container.querySelector('[placeholder="route-abtest-0-2"]')).toBeFalsy();
747+
748+
// Test setting type to weight for new route
749+
const newWeightRadio = container.querySelector('[data-route-index="2"] input[value="weight"]') as HTMLInputElement;
750+
fireEvent.click(newWeightRadio);
751+
await mockTimeout();
752+
const newWeightInput = getByPlaceholderText('route-weight-0-2') as HTMLInputElement;
753+
expect(newWeightInput).toBeTruthy();
754+
expect(newWeightInput.value).toBe('');
755+
756+
// Test setting weight value
757+
fireEvent.change(newWeightInput, { target: { value: '100' } });
758+
await mockTimeout();
759+
expect(newWeightInput.value).toBe('100');
760+
761+
// Test switching new route to abtest
762+
const newAbtestRadio = container.querySelector('[data-route-index="2"] input[value="abtest"]') as HTMLInputElement;
763+
fireEvent.click(newAbtestRadio);
764+
await mockTimeout();
765+
expect(container.querySelector('[placeholder="route-weight-0-2"]')).toBeFalsy();
766+
const newAbtestInput = getByPlaceholderText('route-abtest-0-2') as HTMLInputElement;
767+
expect(newAbtestInput).toBeTruthy();
768+
expect(newAbtestInput.value).toBe('');
769+
770+
// Test setting abtest value
771+
fireEvent.change(newAbtestInput, { target: { value: 'new-key' } });
772+
await mockTimeout();
773+
expect(newAbtestInput.value).toBe('new-key');
774+
775+
// Test switching back to weight - previous weight value should be preserved
776+
fireEvent.click(newWeightRadio);
777+
await mockTimeout();
778+
expect(container.querySelector('[placeholder="route-abtest-0-2"]')).toBeFalsy();
779+
const weightInputAgain = getByPlaceholderText('route-weight-0-2') as HTMLInputElement;
780+
expect(weightInputAgain.value).toBe('100');
781+
782+
// Test adding specified route (with initial data)
783+
const addSpecifiedBtn = container.querySelector('#test-add-route-0-specified');
784+
fireEvent.click(addSpecifiedBtn);
785+
await mockTimeout();
786+
const specifiedWeightRadio = container.querySelector(
787+
'[data-route-index="3"] input[value="weight"]',
788+
) as HTMLInputElement;
789+
expect(specifiedWeightRadio.checked).toBe(true);
790+
const specifiedWeightInput = getByPlaceholderText('route-weight-0-3') as HTMLInputElement;
791+
expect(specifiedWeightInput.value).toBe('50');
792+
expect(container.querySelector('[placeholder="route-abtest-0-3"]')).toBeFalsy();
793+
794+
// Test switching specified route to abtest
795+
const specifiedAbtestRadio = container.querySelector(
796+
'[data-route-index="3"] input[value="abtest"]',
797+
) as HTMLInputElement;
798+
fireEvent.click(specifiedAbtestRadio);
799+
await mockTimeout();
800+
const specifiedAbtestInput = getByPlaceholderText('route-abtest-0-3') as HTMLInputElement;
801+
expect(specifiedAbtestInput.value).toBe('cid');
802+
expect(container.querySelector('[placeholder="route-weight-0-3"]')).toBeFalsy();
803+
});
608804
});

packages/components/form/_example/form-field-linkage.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React from 'react';
2-
import { Form, Radio, Button } from 'tdesign-react';
2+
import { Button, Form, Radio } from 'tdesign-react';
33

44
const { FormItem } = Form;
55

66
export default function FormExample() {
77
const [form] = Form.useForm();
8-
const setMessage = () => {
8+
9+
const applyColdPreset = () => {
910
form.setFieldsValue({
1011
type: 'cold',
1112
ice: '1',
@@ -15,20 +16,31 @@ export default function FormExample() {
1516
return (
1617
<Form form={form} colon labelWidth={100}>
1718
<FormItem label="类型" name="type" initialData="hot">
18-
<Radio.Group>
19-
<Radio value="hot">热饮</Radio>
20-
<Radio value="cold">冷饮</Radio>
19+
<Radio.Group variant="default-filled">
20+
<Radio.Button value="hot">热饮</Radio.Button>
21+
<Radio.Button value="cold">冷饮</Radio.Button>
2122
</Radio.Group>
2223
</FormItem>
2324
<FormItem shouldUpdate={(prev, next) => prev.type !== next.type}>
2425
{({ getFieldValue }) => {
25-
if (getFieldValue('type') === 'cold') {
26+
const type = getFieldValue('type');
27+
if (type === 'cold') {
28+
return (
29+
<FormItem label="冰量" key={type} name="ice">
30+
<Radio.Group>
31+
<Radio value="normal">正常冰</Radio>
32+
<Radio value="less">少冰</Radio>
33+
<Radio value="none">去冰</Radio>
34+
</Radio.Group>
35+
</FormItem>
36+
);
37+
}
38+
if (type === 'hot') {
2639
return (
27-
<FormItem label="冰量" key="ice" name="ice">
40+
<FormItem label="温度" key={type} name="heat">
2841
<Radio.Group>
29-
<Radio value="0">正常冰</Radio>
30-
<Radio value="1">少冰</Radio>
31-
<Radio value="2">去冰</Radio>
42+
<Radio value="high"></Radio>
43+
<Radio value="warm">常温</Radio>
3244
</Radio.Group>
3345
</FormItem>
3446
);
@@ -38,7 +50,7 @@ export default function FormExample() {
3850
</FormItem>
3951

4052
<FormItem style={{ marginLeft: 100 }}>
41-
<Button onClick={setMessage}>选择冷饮-少冰</Button>
53+
<Button onClick={applyColdPreset}>选择冷饮-少冰</Button>
4254
</FormItem>
4355
</Form>
4456
);

packages/components/form/form.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
### 字段联动的表单
1616

17-
在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 `FormItem` 的更新逻辑。
17+
在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 `FormItem` 的更新逻辑,同时要给具体的 `FormItem` 添加不同的 `key`
1818

1919
{{ form-field-linkage }}
2020

packages/components/form/hooks/useFormItemInitialData.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,12 @@ export default function useFormItemInitialData(name: NamePath, fullPath: NamePat
7676
return initialData;
7777
}
7878

79-
if (name && formListInitialData?.length) {
80-
const defaultInitialData = get(formListInitialData, name);
79+
if (Array.isArray(name) && formListInitialData?.length) {
80+
let defaultInitialData;
81+
const [index, ...relativePath] = name;
82+
if (formListInitialData[index]) {
83+
defaultInitialData = get(formListInitialData[index], relativePath);
84+
}
8185
if (typeof defaultInitialData !== 'undefined') return defaultInitialData;
8286
}
8387

0 commit comments

Comments
 (0)