react-form-manage 1.0.8-beta.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +173 -4
- package/README.md +8 -4
- package/dist/components/Form/FormCleanUp.js +3 -3
- package/dist/components/Form/FormItem.d.ts +10 -4
- package/dist/components/Form/FormItem.js +52 -14
- package/dist/components/Form/FormList.d.ts +2 -2
- package/dist/components/Form/FormList.js +2 -2
- package/dist/constants/form.d.ts +1 -1
- package/dist/hooks/useFormItemControl.d.ts +8 -3
- package/dist/hooks/useFormItemControl.js +64 -28
- package/dist/hooks/useFormListControl.d.ts +2 -1
- package/dist/hooks/useFormListControl.js +85 -19
- package/dist/index.cjs.d.ts +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.esm.d.ts +1 -0
- package/dist/index.js +4 -2
- package/dist/providers/Form.d.ts +15 -2
- package/dist/providers/Form.js +226 -41
- package/dist/stores/formStore.d.ts +44 -4
- package/dist/stores/formStore.js +42 -7
- package/dist/test/CommonTest.d.ts +3 -0
- package/dist/test/CommonTest.js +49 -0
- package/dist/test/TestDialog.d.ts +3 -0
- package/dist/test/TestDialog.js +21 -0
- package/dist/test/TestListener.d.ts +3 -0
- package/dist/test/TestListener.js +17 -0
- package/dist/test/TestNotFormWrapper.d.ts +3 -0
- package/dist/test/TestNotFormWrapper.js +15 -0
- package/dist/test/TestSelect.d.ts +6 -0
- package/dist/test/TestSelect.js +24 -0
- package/dist/test/TestWatchNormalize.d.ts +3 -0
- package/dist/test/TestWatchNormalize.js +23 -0
- package/dist/test/TestWrapperFormItem.d.ts +3 -0
- package/dist/test/TestWrapperFormItem.js +13 -0
- package/dist/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.d.ts +21 -0
- package/dist/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.js +61 -0
- package/dist/test/testSetValue/TestCase1_PlainObjectToPrimitives.d.ts +16 -0
- package/dist/test/testSetValue/TestCase1_PlainObjectToPrimitives.js +18 -0
- package/dist/test/testSetValue/TestCase2_PlainObjectToFormList.d.ts +21 -0
- package/dist/test/testSetValue/TestCase2_PlainObjectToFormList.js +33 -0
- package/dist/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.d.ts +21 -0
- package/dist/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.js +26 -0
- package/dist/test/testSetValue/TestCase4_PlainObjectRemovedFields.d.ts +20 -0
- package/dist/test/testSetValue/TestCase4_PlainObjectRemovedFields.js +32 -0
- package/dist/test/testSetValue/TestCase5_FormListRemovedItems.d.ts +22 -0
- package/dist/test/testSetValue/TestCase5_FormListRemovedItems.js +29 -0
- package/dist/test/testSetValue/TestCase6_NestedFormListRemoved.d.ts +28 -0
- package/dist/test/testSetValue/TestCase6_NestedFormListRemoved.js +36 -0
- package/dist/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.d.ts +17 -0
- package/dist/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.js +33 -0
- package/dist/test/testSetValue/TestCase8_SetFieldValues_NestedObject.d.ts +27 -0
- package/dist/test/testSetValue/TestCase8_SetFieldValues_NestedObject.js +57 -0
- package/dist/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.d.ts +25 -0
- package/dist/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.js +46 -0
- package/dist/test/testSetValue/index.d.ts +2 -0
- package/dist/test/testSetValue/index.js +28 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/public.d.ts +1 -1
- package/dist/utils/obj.util.d.ts +29 -1
- package/dist/utils/obj.util.js +59 -5
- package/package.json +2 -1
- package/src/App.tsx +39 -156
- package/src/DEEP_TRIGGER_LOGIC.md +573 -0
- package/src/components/Form/FormCleanUp.tsx +4 -8
- package/src/components/Form/FormItem.tsx +174 -57
- package/src/components/Form/FormList.tsx +17 -4
- package/src/constants/form.ts +1 -1
- package/src/hooks/useFormItemControl.ts +78 -32
- package/src/hooks/useFormListControl.ts +133 -43
- package/src/index.ts +25 -13
- package/src/main.tsx +6 -1
- package/src/providers/Form.tsx +454 -26
- package/src/stores/formStore.ts +363 -283
- package/src/test/CommonTest.tsx +177 -0
- package/src/test/TestDialog.tsx +52 -0
- package/src/test/TestListener.tsx +21 -0
- package/src/test/TestNotFormWrapper.tsx +43 -0
- package/src/test/TestSelect.tsx +38 -0
- package/src/test/TestWatchNormalize.tsx +32 -0
- package/src/test/TestWrapperFormItem.tsx +34 -0
- package/src/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.tsx +203 -0
- package/src/test/testSetValue/TestCase1_PlainObjectToPrimitives.tsx +72 -0
- package/src/test/testSetValue/TestCase2_PlainObjectToFormList.tsx +114 -0
- package/src/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.tsx +99 -0
- package/src/test/testSetValue/TestCase4_PlainObjectRemovedFields.tsx +112 -0
- package/src/test/testSetValue/TestCase5_FormListRemovedItems.tsx +119 -0
- package/src/test/testSetValue/TestCase6_NestedFormListRemoved.tsx +185 -0
- package/src/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.tsx +110 -0
- package/src/test/testSetValue/TestCase8_SetFieldValues_NestedObject.tsx +162 -0
- package/src/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.tsx +169 -0
- package/src/test/testSetValue/index.tsx +100 -0
- package/src/types/index.ts +1 -1
- package/src/types/public.ts +1 -1
- package/src/utils/obj.util.ts +153 -13
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import { Button, Input, Typography } from "antd";
|
|
3
|
+
import FormList from "../../components/Form/FormList";
|
|
4
|
+
import Form from "../../providers/Form";
|
|
5
|
+
|
|
6
|
+
type Props = {};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test Case 2: Plain Object → Array Listener (FormList) → Primitives
|
|
10
|
+
*
|
|
11
|
+
* Cấu trúc:
|
|
12
|
+
* - user.name (string)
|
|
13
|
+
* - user.items (array - FormList)
|
|
14
|
+
* - user.items.0.id (number)
|
|
15
|
+
* - user.items.0.title (string)
|
|
16
|
+
*
|
|
17
|
+
* Test: setFieldValue("user", {name: "John", items: [{id: 1, title: "Item 1"}]}, {deepTrigger: true})
|
|
18
|
+
* Kỳ vọng:
|
|
19
|
+
* - Trigger "user" listener
|
|
20
|
+
* - Trigger "user.name" listener
|
|
21
|
+
* - Trigger "user.items" onArrayChange
|
|
22
|
+
* - Trigger "user.items.0" listener
|
|
23
|
+
* - Trigger "user.items.0.id" listener
|
|
24
|
+
* - Trigger "user.items.0.title" listener
|
|
25
|
+
*/
|
|
26
|
+
function TestCase2_PlainObjectToFormList({}: Props) {
|
|
27
|
+
const [form] = Form.useForm("testCase2");
|
|
28
|
+
|
|
29
|
+
const handleTestDeepTrigger = () => {
|
|
30
|
+
form?.setFieldValue(
|
|
31
|
+
"user",
|
|
32
|
+
{
|
|
33
|
+
name: "John Doe",
|
|
34
|
+
items: [
|
|
35
|
+
{ id: 1, title: "Item 1" },
|
|
36
|
+
{ id: 2, title: "Item 2" },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{ deepTrigger: true },
|
|
40
|
+
);
|
|
41
|
+
console.log("✅ Called setFieldValue with deepTrigger=true");
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleTestNormalSet = () => {
|
|
45
|
+
form?.setFieldValue("user", {
|
|
46
|
+
name: "Jane Doe",
|
|
47
|
+
items: [{ id: 3, title: "Item 3" }],
|
|
48
|
+
});
|
|
49
|
+
console.log("✅ Called setFieldValue without deepTrigger");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Box sx={{ p: 2 }}>
|
|
54
|
+
<Typography.Title level={4}>
|
|
55
|
+
Test Case 2: Plain Object → FormList → Primitives
|
|
56
|
+
</Typography.Title>
|
|
57
|
+
<Typography.Paragraph>
|
|
58
|
+
Test setFieldValue("user", {"{name, items: [...]}"}) với nested FormList
|
|
59
|
+
</Typography.Paragraph>
|
|
60
|
+
|
|
61
|
+
<Form formName="testCase2">
|
|
62
|
+
{/* User name */}
|
|
63
|
+
<Form.Item name="user.name" label="User Name">
|
|
64
|
+
<Input placeholder="Enter user name" />
|
|
65
|
+
</Form.Item>
|
|
66
|
+
|
|
67
|
+
{/* Items FormList */}
|
|
68
|
+
<FormList name="user.items">
|
|
69
|
+
{(fields, { add, remove }) => (
|
|
70
|
+
<Box>
|
|
71
|
+
{fields.map((field, index) => (
|
|
72
|
+
<Box
|
|
73
|
+
key={field.key}
|
|
74
|
+
sx={{
|
|
75
|
+
border: "1px solid #d9d9d9",
|
|
76
|
+
p: 2,
|
|
77
|
+
mb: 2,
|
|
78
|
+
borderRadius: 1,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<Typography.Text strong>Item {index + 1}</Typography.Text>
|
|
82
|
+
|
|
83
|
+
<Form.Item name={`user.items.${index}.id`} label="ID">
|
|
84
|
+
<Input placeholder="Item ID" />
|
|
85
|
+
</Form.Item>
|
|
86
|
+
|
|
87
|
+
<Form.Item name={`user.items.${index}.title`} label="Title">
|
|
88
|
+
<Input placeholder="Item title" />
|
|
89
|
+
</Form.Item>
|
|
90
|
+
|
|
91
|
+
<Button danger onClick={() => remove({ key: field.key })}>
|
|
92
|
+
Remove
|
|
93
|
+
</Button>
|
|
94
|
+
</Box>
|
|
95
|
+
))}
|
|
96
|
+
<Button type="dashed" onClick={() => add()}>
|
|
97
|
+
Add Item
|
|
98
|
+
</Button>
|
|
99
|
+
</Box>
|
|
100
|
+
)}
|
|
101
|
+
</FormList>
|
|
102
|
+
|
|
103
|
+
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
|
|
104
|
+
<Button type="primary" onClick={handleTestDeepTrigger}>
|
|
105
|
+
Test Deep Trigger
|
|
106
|
+
</Button>
|
|
107
|
+
<Button onClick={handleTestNormalSet}>Test Normal Set</Button>
|
|
108
|
+
</Box>
|
|
109
|
+
</Form>
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default TestCase2_PlainObjectToFormList;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import { Button, Input, Typography } from "antd";
|
|
3
|
+
import Form from "../../providers/Form";
|
|
4
|
+
|
|
5
|
+
type Props = {};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Test Case 3: Array Non-Listener → Primitives
|
|
9
|
+
*
|
|
10
|
+
* Cấu trúc:
|
|
11
|
+
* - data (array - KHÔNG phải FormList, chỉ là field bình thường)
|
|
12
|
+
* - data.0.name (string)
|
|
13
|
+
* - data.0.value (number)
|
|
14
|
+
*
|
|
15
|
+
* Test: setFieldValue("data", [{name: "A", value: 1}, {name: "B", value: 2}], {deepTrigger: true})
|
|
16
|
+
* Kỳ vọng:
|
|
17
|
+
* - Set data array vào store
|
|
18
|
+
* - Trigger "data.0" listener (nếu có)
|
|
19
|
+
* - Trigger "data.0.name" listener
|
|
20
|
+
* - Trigger "data.0.value" listener
|
|
21
|
+
* - Trigger "data.1" listener (nếu có)
|
|
22
|
+
* - Trigger "data.1.name" listener
|
|
23
|
+
* - Trigger "data.1.value" listener
|
|
24
|
+
*/
|
|
25
|
+
function TestCase3_ArrayNonListenerToPrimitives({}: Props) {
|
|
26
|
+
const [form] = Form.useForm("testCase3");
|
|
27
|
+
|
|
28
|
+
const handleTestDeepTrigger = () => {
|
|
29
|
+
form?.setFieldValue(
|
|
30
|
+
"data",
|
|
31
|
+
[
|
|
32
|
+
{ name: "Item A", value: 100 },
|
|
33
|
+
{ name: "Item B", value: 200 },
|
|
34
|
+
],
|
|
35
|
+
{ deepTrigger: true },
|
|
36
|
+
);
|
|
37
|
+
console.log("✅ Called setFieldValue with deepTrigger=true");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleTestRemoveItems = () => {
|
|
41
|
+
// Test cleanup: remove last item
|
|
42
|
+
form?.setFieldValue("data", [{ name: "Item A", value: 100 }], {
|
|
43
|
+
deepTrigger: true,
|
|
44
|
+
});
|
|
45
|
+
console.log("✅ Called setFieldValue with fewer items (cleanup test)");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleTestNormalSet = () => {
|
|
49
|
+
form?.setFieldValue("data", [{ name: "Item C", value: 300 }]);
|
|
50
|
+
console.log("✅ Called setFieldValue without deepTrigger");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Box sx={{ p: 2 }}>
|
|
55
|
+
<Typography.Title level={4}>
|
|
56
|
+
Test Case 3: Array Non-Listener → Primitives
|
|
57
|
+
</Typography.Title>
|
|
58
|
+
<Typography.Paragraph>
|
|
59
|
+
Test setFieldValue("data", {"[{name, value}, ...]"}) với array không
|
|
60
|
+
phải FormList
|
|
61
|
+
</Typography.Paragraph>
|
|
62
|
+
|
|
63
|
+
<Form formName="testCase3">
|
|
64
|
+
{/* Array items - manually defined (not FormList) */}
|
|
65
|
+
<Box sx={{ border: "1px solid #d9d9d9", p: 2, mb: 2, borderRadius: 1 }}>
|
|
66
|
+
<Typography.Text strong>Item 0</Typography.Text>
|
|
67
|
+
<Form.Item name="data.0.name" label="Name">
|
|
68
|
+
<Input placeholder="Item 0 name" />
|
|
69
|
+
</Form.Item>
|
|
70
|
+
<Form.Item name="data.0.value" label="Value">
|
|
71
|
+
<Input type="number" placeholder="Item 0 value" />
|
|
72
|
+
</Form.Item>
|
|
73
|
+
</Box>
|
|
74
|
+
|
|
75
|
+
<Box sx={{ border: "1px solid #d9d9d9", p: 2, mb: 2, borderRadius: 1 }}>
|
|
76
|
+
<Typography.Text strong>Item 1</Typography.Text>
|
|
77
|
+
<Form.Item name="data.1.name" label="Name">
|
|
78
|
+
<Input placeholder="Item 1 name" />
|
|
79
|
+
</Form.Item>
|
|
80
|
+
<Form.Item name="data.1.value" label="Value">
|
|
81
|
+
<Input type="number" placeholder="Item 1 value" />
|
|
82
|
+
</Form.Item>
|
|
83
|
+
</Box>
|
|
84
|
+
|
|
85
|
+
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
|
|
86
|
+
<Button type="primary" onClick={handleTestDeepTrigger}>
|
|
87
|
+
Test Deep Trigger (2 items)
|
|
88
|
+
</Button>
|
|
89
|
+
<Button onClick={handleTestRemoveItems}>
|
|
90
|
+
Test Remove Items (1 item)
|
|
91
|
+
</Button>
|
|
92
|
+
<Button onClick={handleTestNormalSet}>Test Normal Set</Button>
|
|
93
|
+
</Box>
|
|
94
|
+
</Form>
|
|
95
|
+
</Box>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default TestCase3_ArrayNonListenerToPrimitives;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import { Button, Input, Typography } from "antd";
|
|
3
|
+
import Form from "../../providers/Form";
|
|
4
|
+
|
|
5
|
+
type Props = {};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Test Case 4: Plain Object with Removed Fields (Edge Case)
|
|
9
|
+
*
|
|
10
|
+
* Cấu trúc ban đầu:
|
|
11
|
+
* - profile.name (string)
|
|
12
|
+
* - profile.city (string)
|
|
13
|
+
* - profile.country (string)
|
|
14
|
+
*
|
|
15
|
+
* Test:
|
|
16
|
+
* 1. Set initial: {name: "John", city: "NY", country: "USA"}
|
|
17
|
+
* 2. Set removed: {name: "John"} (city và country bị xóa)
|
|
18
|
+
*
|
|
19
|
+
* Kỳ vọng:
|
|
20
|
+
* - Trigger "profile.name" với "John"
|
|
21
|
+
* - Trigger "profile.city" với undefined (cleanup)
|
|
22
|
+
* - Trigger "profile.country" với undefined (cleanup)
|
|
23
|
+
*/
|
|
24
|
+
function TestCase4_PlainObjectRemovedFields({}: Props) {
|
|
25
|
+
const [form] = Form.useForm("testCase4");
|
|
26
|
+
|
|
27
|
+
const handleSetInitial = () => {
|
|
28
|
+
form?.setFieldValue(
|
|
29
|
+
"profile",
|
|
30
|
+
{
|
|
31
|
+
name: "John Doe",
|
|
32
|
+
city: "New York",
|
|
33
|
+
country: "USA",
|
|
34
|
+
},
|
|
35
|
+
{ deepTrigger: true },
|
|
36
|
+
);
|
|
37
|
+
console.log("✅ Set initial profile with all fields");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleRemoveFields = () => {
|
|
41
|
+
form?.setFieldValue(
|
|
42
|
+
"profile",
|
|
43
|
+
{
|
|
44
|
+
name: "John Doe",
|
|
45
|
+
// city and country removed!
|
|
46
|
+
},
|
|
47
|
+
{ deepTrigger: true },
|
|
48
|
+
);
|
|
49
|
+
console.log(
|
|
50
|
+
"✅ Set profile with removed fields (city, country should trigger undefined)",
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleRestoreFields = () => {
|
|
55
|
+
form?.setFieldValue(
|
|
56
|
+
"profile",
|
|
57
|
+
{
|
|
58
|
+
name: "Jane Doe",
|
|
59
|
+
city: "Los Angeles",
|
|
60
|
+
country: "USA",
|
|
61
|
+
},
|
|
62
|
+
{ deepTrigger: true },
|
|
63
|
+
);
|
|
64
|
+
console.log("✅ Restored all fields");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Box sx={{ p: 2 }}>
|
|
69
|
+
<Typography.Title level={4}>
|
|
70
|
+
Test Case 4: Plain Object - Removed Fields
|
|
71
|
+
</Typography.Title>
|
|
72
|
+
<Typography.Paragraph>
|
|
73
|
+
Test edge case: khi object fields bị xóa, listener phải được trigger với
|
|
74
|
+
undefined
|
|
75
|
+
</Typography.Paragraph>
|
|
76
|
+
|
|
77
|
+
<Form formName="testCase4">
|
|
78
|
+
<Form.Item name="profile.name" label="Name">
|
|
79
|
+
<Input placeholder="Enter name" />
|
|
80
|
+
</Form.Item>
|
|
81
|
+
|
|
82
|
+
<Form.Item name="profile.city" label="City">
|
|
83
|
+
<Input placeholder="Enter city" />
|
|
84
|
+
</Form.Item>
|
|
85
|
+
|
|
86
|
+
<Form.Item name="profile.country" label="Country">
|
|
87
|
+
<Input placeholder="Enter country" />
|
|
88
|
+
</Form.Item>
|
|
89
|
+
|
|
90
|
+
<Box sx={{ mt: 2, display: "flex", gap: 2, flexDirection: "column" }}>
|
|
91
|
+
<Button type="primary" onClick={handleSetInitial}>
|
|
92
|
+
1. Set Initial (All Fields)
|
|
93
|
+
</Button>
|
|
94
|
+
<Button danger onClick={handleRemoveFields}>
|
|
95
|
+
2. Remove Fields (city, country)
|
|
96
|
+
</Button>
|
|
97
|
+
<Button onClick={handleRestoreFields}>3. Restore Fields</Button>
|
|
98
|
+
</Box>
|
|
99
|
+
|
|
100
|
+
<Box sx={{ mt: 2, p: 2, bgcolor: "#f0f0f0", borderRadius: 1 }}>
|
|
101
|
+
<Typography.Text strong>Expected Behavior:</Typography.Text>
|
|
102
|
+
<Typography.Paragraph>
|
|
103
|
+
Khi click "Remove Fields", city và country listeners phải nhận
|
|
104
|
+
undefined
|
|
105
|
+
</Typography.Paragraph>
|
|
106
|
+
</Box>
|
|
107
|
+
</Form>
|
|
108
|
+
</Box>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default TestCase4_PlainObjectRemovedFields;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import { Button, Input, Typography } from "antd";
|
|
3
|
+
import FormList from "../../components/Form/FormList";
|
|
4
|
+
import Form from "../../providers/Form";
|
|
5
|
+
|
|
6
|
+
type Props = {};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test Case 5: Array Listener (FormList) with Removed Items
|
|
10
|
+
*
|
|
11
|
+
* Cấu trúc:
|
|
12
|
+
* - items (FormList)
|
|
13
|
+
* - items.0.name (string)
|
|
14
|
+
* - items.1.name (string)
|
|
15
|
+
* - items.2.name (string)
|
|
16
|
+
*
|
|
17
|
+
* Test:
|
|
18
|
+
* 1. Set initial: [{name: "A"}, {name: "B"}, {name: "C"}]
|
|
19
|
+
* 2. Set removed: [{name: "A"}] (items 1 và 2 bị xóa)
|
|
20
|
+
*
|
|
21
|
+
* Kỳ vọng:
|
|
22
|
+
* - onArrayChange được gọi với array mới
|
|
23
|
+
* - Trigger "items.0.name" với "A"
|
|
24
|
+
* - Trigger "items.1" với undefined (cleanup)
|
|
25
|
+
* - Trigger "items.2" với undefined (cleanup)
|
|
26
|
+
*/
|
|
27
|
+
function TestCase5_FormListRemovedItems({}: Props) {
|
|
28
|
+
const [form] = Form.useForm("testCase5");
|
|
29
|
+
|
|
30
|
+
const handleSetInitial = () => {
|
|
31
|
+
form?.setFieldValue(
|
|
32
|
+
"items",
|
|
33
|
+
[{ name: "Item A" }, { name: "Item B" }, { name: "Item C" }],
|
|
34
|
+
{ deepTrigger: true },
|
|
35
|
+
);
|
|
36
|
+
console.log("✅ Set initial FormList with 3 items");
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleRemoveItems = () => {
|
|
40
|
+
form?.setFieldValue("items", [{ name: "Item A" }], { deepTrigger: true });
|
|
41
|
+
console.log(
|
|
42
|
+
"✅ Set FormList with 1 item (2 items removed, should trigger undefined)",
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleRestoreItems = () => {
|
|
47
|
+
form?.setFieldValue("items", [{ name: "Item A" }, { name: "Item B" }], {
|
|
48
|
+
deepTrigger: true,
|
|
49
|
+
});
|
|
50
|
+
console.log("✅ Restored FormList with 2 items");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Box sx={{ p: 2 }}>
|
|
55
|
+
<Typography.Title level={4}>
|
|
56
|
+
Test Case 5: FormList - Removed Items
|
|
57
|
+
</Typography.Title>
|
|
58
|
+
<Typography.Paragraph>
|
|
59
|
+
Test edge case: khi FormList items bị xóa, listeners phải được trigger
|
|
60
|
+
với undefined
|
|
61
|
+
</Typography.Paragraph>
|
|
62
|
+
|
|
63
|
+
<Form formName="testCase5">
|
|
64
|
+
<FormList name="items">
|
|
65
|
+
{(fields, { add, remove }) => (
|
|
66
|
+
<Box>
|
|
67
|
+
{fields.map((field, index) => (
|
|
68
|
+
<Box
|
|
69
|
+
key={field.key}
|
|
70
|
+
sx={{
|
|
71
|
+
border: "1px solid #d9d9d9",
|
|
72
|
+
p: 2,
|
|
73
|
+
mb: 2,
|
|
74
|
+
borderRadius: 1,
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<Typography.Text strong>Item {index + 1}</Typography.Text>
|
|
78
|
+
|
|
79
|
+
<Form.Item name={`items.${index}.name`} label="Name">
|
|
80
|
+
<Input placeholder={`Item ${index + 1} name`} />
|
|
81
|
+
</Form.Item>
|
|
82
|
+
|
|
83
|
+
<Button danger onClick={() => remove({ key: field.key })}>
|
|
84
|
+
Remove
|
|
85
|
+
</Button>
|
|
86
|
+
</Box>
|
|
87
|
+
))}
|
|
88
|
+
<Button type="dashed" onClick={() => add()}>
|
|
89
|
+
Add Item
|
|
90
|
+
</Button>
|
|
91
|
+
</Box>
|
|
92
|
+
)}
|
|
93
|
+
</FormList>
|
|
94
|
+
|
|
95
|
+
<Box sx={{ mt: 2, display: "flex", gap: 2, flexDirection: "column" }}>
|
|
96
|
+
<Button type="primary" onClick={handleSetInitial}>
|
|
97
|
+
1. Set Initial (3 Items)
|
|
98
|
+
</Button>
|
|
99
|
+
<Button danger onClick={handleRemoveItems}>
|
|
100
|
+
2. Remove Items (Keep 1)
|
|
101
|
+
</Button>
|
|
102
|
+
<Button onClick={handleRestoreItems}>
|
|
103
|
+
3. Restore Items (2 Items)
|
|
104
|
+
</Button>
|
|
105
|
+
</Box>
|
|
106
|
+
|
|
107
|
+
<Box sx={{ mt: 2, p: 2, bgcolor: "#f0f0f0", borderRadius: 1 }}>
|
|
108
|
+
<Typography.Text strong>Expected Behavior:</Typography.Text>
|
|
109
|
+
<Typography.Paragraph>
|
|
110
|
+
Khi click "Remove Items", items.1 và items.2 listeners phải nhận
|
|
111
|
+
undefined
|
|
112
|
+
</Typography.Paragraph>
|
|
113
|
+
</Box>
|
|
114
|
+
</Form>
|
|
115
|
+
</Box>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default TestCase5_FormListRemovedItems;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import { Button, Input, Typography } from "antd";
|
|
3
|
+
import FormList from "../../components/Form/FormList";
|
|
4
|
+
import Form from "../../providers/Form";
|
|
5
|
+
|
|
6
|
+
type Props = {};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test Case 6: Array Non-Listener → Nested FormList Removed (Complex Edge Case)
|
|
10
|
+
*
|
|
11
|
+
* Cấu trúc:
|
|
12
|
+
* - data (array - NON-LISTENER)
|
|
13
|
+
* - data.0.name (string)
|
|
14
|
+
* - data.0.tags (FormList)
|
|
15
|
+
* - data.0.tags.0 (string)
|
|
16
|
+
* - data.0.tags.1 (string)
|
|
17
|
+
* - data.1.name (string)
|
|
18
|
+
* - data.1.tags (FormList)
|
|
19
|
+
*
|
|
20
|
+
* Test:
|
|
21
|
+
* 1. Set initial: [
|
|
22
|
+
* {name: "A", tags: ["tag1", "tag2"]},
|
|
23
|
+
* {name: "B", tags: ["tag3"]}
|
|
24
|
+
* ]
|
|
25
|
+
* 2. Set removed: [{name: "A", tags: ["tag1"]}] (data.1 bị xóa hoàn toàn)
|
|
26
|
+
*
|
|
27
|
+
* Kỳ vọng:
|
|
28
|
+
* - Trigger "data.0.tags" onArrayChange
|
|
29
|
+
* - Trigger "data.1" với undefined
|
|
30
|
+
* - Trigger "data.1.tags" với undefined (nested FormList cleanup!)
|
|
31
|
+
* - Trigger "data.1.tags.0" với undefined
|
|
32
|
+
*/
|
|
33
|
+
function TestCase6_NestedFormListRemoved({}: Props) {
|
|
34
|
+
const [form] = Form.useForm("testCase6");
|
|
35
|
+
|
|
36
|
+
const handleSetInitial = () => {
|
|
37
|
+
form?.setFieldValue(
|
|
38
|
+
"data",
|
|
39
|
+
[
|
|
40
|
+
{ name: "Group A", tags: ["tag1", "tag2"] },
|
|
41
|
+
{ name: "Group B", tags: ["tag3", "tag4"] },
|
|
42
|
+
],
|
|
43
|
+
{ deepTrigger: true },
|
|
44
|
+
);
|
|
45
|
+
console.log("✅ Set initial nested structure with 2 groups");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleRemoveNestedArray = () => {
|
|
49
|
+
form?.setFieldValue("data", [{ name: "Group A", tags: ["tag1"] }], {
|
|
50
|
+
deepTrigger: true,
|
|
51
|
+
});
|
|
52
|
+
console.log(
|
|
53
|
+
"✅ Removed data.1 (including nested FormList - should trigger undefined recursively)",
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleRestoreNested = () => {
|
|
58
|
+
form?.setFieldValue(
|
|
59
|
+
"data",
|
|
60
|
+
[
|
|
61
|
+
{ name: "Group A", tags: ["tag1", "tag2"] },
|
|
62
|
+
{ name: "Group B", tags: ["tag3"] },
|
|
63
|
+
],
|
|
64
|
+
{ deepTrigger: true },
|
|
65
|
+
);
|
|
66
|
+
console.log("✅ Restored nested structure");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Box sx={{ p: 2 }}>
|
|
71
|
+
<Typography.Title level={4}>
|
|
72
|
+
Test Case 6: Nested FormList - Removed Items
|
|
73
|
+
</Typography.Title>
|
|
74
|
+
<Typography.Paragraph>
|
|
75
|
+
Test complex edge case: array non-listener với nested FormList bị xóa
|
|
76
|
+
</Typography.Paragraph>
|
|
77
|
+
|
|
78
|
+
<Form formName="testCase6">
|
|
79
|
+
{/* Group 0 */}
|
|
80
|
+
<Box sx={{ border: "2px solid #1890ff", p: 2, mb: 2, borderRadius: 1 }}>
|
|
81
|
+
<Typography.Text strong style={{ fontSize: 16 }}>
|
|
82
|
+
Group 0
|
|
83
|
+
</Typography.Text>
|
|
84
|
+
|
|
85
|
+
<Form.Item name="data.0.name" label="Group Name">
|
|
86
|
+
<Input placeholder="Group 0 name" />
|
|
87
|
+
</Form.Item>
|
|
88
|
+
|
|
89
|
+
<Typography.Text>Tags (FormList):</Typography.Text>
|
|
90
|
+
<FormList name="data.0.tags">
|
|
91
|
+
{(fields, { add, remove }) => (
|
|
92
|
+
<Box>
|
|
93
|
+
{fields.map((field, index) => (
|
|
94
|
+
<Box key={field.key} sx={{ display: "flex", gap: 1, mb: 1 }}>
|
|
95
|
+
<Form.Item name={`data.0.tags.${index}`}>
|
|
96
|
+
<Input placeholder={`Tag ${index + 1}`} />
|
|
97
|
+
</Form.Item>
|
|
98
|
+
<Button
|
|
99
|
+
danger
|
|
100
|
+
size="small"
|
|
101
|
+
onClick={() => remove({ key: field.key })}
|
|
102
|
+
>
|
|
103
|
+
Remove
|
|
104
|
+
</Button>
|
|
105
|
+
</Box>
|
|
106
|
+
))}
|
|
107
|
+
<Button type="dashed" size="small" onClick={() => add()}>
|
|
108
|
+
Add Tag
|
|
109
|
+
</Button>
|
|
110
|
+
</Box>
|
|
111
|
+
)}
|
|
112
|
+
</FormList>
|
|
113
|
+
</Box>
|
|
114
|
+
|
|
115
|
+
{/* Group 1 */}
|
|
116
|
+
<Box sx={{ border: "2px solid #52c41a", p: 2, mb: 2, borderRadius: 1 }}>
|
|
117
|
+
<Typography.Text strong style={{ fontSize: 16 }}>
|
|
118
|
+
Group 1
|
|
119
|
+
</Typography.Text>
|
|
120
|
+
|
|
121
|
+
<Form.Item name="data.1.name" label="Group Name">
|
|
122
|
+
<Input placeholder="Group 1 name" />
|
|
123
|
+
</Form.Item>
|
|
124
|
+
|
|
125
|
+
<Typography.Text>Tags (FormList):</Typography.Text>
|
|
126
|
+
<FormList name="data.1.tags">
|
|
127
|
+
{(fields, { add, remove }) => (
|
|
128
|
+
<Box>
|
|
129
|
+
{fields.map((field, index) => (
|
|
130
|
+
<Box key={field.key} sx={{ display: "flex", gap: 1, mb: 1 }}>
|
|
131
|
+
<Form.Item name={`data.1.tags.${index}`}>
|
|
132
|
+
<Input placeholder={`Tag ${index + 1}`} />
|
|
133
|
+
</Form.Item>
|
|
134
|
+
<Button
|
|
135
|
+
danger
|
|
136
|
+
size="small"
|
|
137
|
+
onClick={() => remove({ key: field.key })}
|
|
138
|
+
>
|
|
139
|
+
Remove
|
|
140
|
+
</Button>
|
|
141
|
+
</Box>
|
|
142
|
+
))}
|
|
143
|
+
<Button type="dashed" size="small" onClick={() => add()}>
|
|
144
|
+
Add Tag
|
|
145
|
+
</Button>
|
|
146
|
+
</Box>
|
|
147
|
+
)}
|
|
148
|
+
</FormList>
|
|
149
|
+
</Box>
|
|
150
|
+
|
|
151
|
+
<Box sx={{ mt: 2, display: "flex", gap: 2, flexDirection: "column" }}>
|
|
152
|
+
<Button type="primary" onClick={handleSetInitial}>
|
|
153
|
+
1. Set Initial (2 Groups with Tags)
|
|
154
|
+
</Button>
|
|
155
|
+
<Button danger onClick={handleRemoveNestedArray}>
|
|
156
|
+
2. Remove Group 1 (Nested FormList)
|
|
157
|
+
</Button>
|
|
158
|
+
<Button onClick={handleRestoreNested}>
|
|
159
|
+
3. Restore Nested Structure
|
|
160
|
+
</Button>
|
|
161
|
+
</Box>
|
|
162
|
+
|
|
163
|
+
<Box
|
|
164
|
+
sx={{
|
|
165
|
+
mt: 2,
|
|
166
|
+
p: 2,
|
|
167
|
+
bgcolor: "#fff7e6",
|
|
168
|
+
border: "1px solid #ffa940",
|
|
169
|
+
borderRadius: 1,
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<Typography.Text strong>Expected Behavior:</Typography.Text>
|
|
173
|
+
<Typography.Paragraph>
|
|
174
|
+
Khi click "Remove Group 1":
|
|
175
|
+
<br />- data.1 → undefined
|
|
176
|
+
<br />- data.1.tags (FormList) → undefined
|
|
177
|
+
<br />- data.1.tags.0, data.1.tags.1, ... → undefined
|
|
178
|
+
</Typography.Paragraph>
|
|
179
|
+
</Box>
|
|
180
|
+
</Form>
|
|
181
|
+
</Box>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default TestCase6_NestedFormListRemoved;
|