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.
Files changed (94) hide show
  1. package/CHANGELOG.md +173 -4
  2. package/README.md +8 -4
  3. package/dist/components/Form/FormCleanUp.js +3 -3
  4. package/dist/components/Form/FormItem.d.ts +10 -4
  5. package/dist/components/Form/FormItem.js +52 -14
  6. package/dist/components/Form/FormList.d.ts +2 -2
  7. package/dist/components/Form/FormList.js +2 -2
  8. package/dist/constants/form.d.ts +1 -1
  9. package/dist/hooks/useFormItemControl.d.ts +8 -3
  10. package/dist/hooks/useFormItemControl.js +64 -28
  11. package/dist/hooks/useFormListControl.d.ts +2 -1
  12. package/dist/hooks/useFormListControl.js +85 -19
  13. package/dist/index.cjs.d.ts +1 -0
  14. package/dist/index.d.ts +4 -3
  15. package/dist/index.esm.d.ts +1 -0
  16. package/dist/index.js +4 -2
  17. package/dist/providers/Form.d.ts +15 -2
  18. package/dist/providers/Form.js +226 -41
  19. package/dist/stores/formStore.d.ts +44 -4
  20. package/dist/stores/formStore.js +42 -7
  21. package/dist/test/CommonTest.d.ts +3 -0
  22. package/dist/test/CommonTest.js +49 -0
  23. package/dist/test/TestDialog.d.ts +3 -0
  24. package/dist/test/TestDialog.js +21 -0
  25. package/dist/test/TestListener.d.ts +3 -0
  26. package/dist/test/TestListener.js +17 -0
  27. package/dist/test/TestNotFormWrapper.d.ts +3 -0
  28. package/dist/test/TestNotFormWrapper.js +15 -0
  29. package/dist/test/TestSelect.d.ts +6 -0
  30. package/dist/test/TestSelect.js +24 -0
  31. package/dist/test/TestWatchNormalize.d.ts +3 -0
  32. package/dist/test/TestWatchNormalize.js +23 -0
  33. package/dist/test/TestWrapperFormItem.d.ts +3 -0
  34. package/dist/test/TestWrapperFormItem.js +13 -0
  35. package/dist/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.d.ts +21 -0
  36. package/dist/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.js +61 -0
  37. package/dist/test/testSetValue/TestCase1_PlainObjectToPrimitives.d.ts +16 -0
  38. package/dist/test/testSetValue/TestCase1_PlainObjectToPrimitives.js +18 -0
  39. package/dist/test/testSetValue/TestCase2_PlainObjectToFormList.d.ts +21 -0
  40. package/dist/test/testSetValue/TestCase2_PlainObjectToFormList.js +33 -0
  41. package/dist/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.d.ts +21 -0
  42. package/dist/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.js +26 -0
  43. package/dist/test/testSetValue/TestCase4_PlainObjectRemovedFields.d.ts +20 -0
  44. package/dist/test/testSetValue/TestCase4_PlainObjectRemovedFields.js +32 -0
  45. package/dist/test/testSetValue/TestCase5_FormListRemovedItems.d.ts +22 -0
  46. package/dist/test/testSetValue/TestCase5_FormListRemovedItems.js +29 -0
  47. package/dist/test/testSetValue/TestCase6_NestedFormListRemoved.d.ts +28 -0
  48. package/dist/test/testSetValue/TestCase6_NestedFormListRemoved.js +36 -0
  49. package/dist/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.d.ts +17 -0
  50. package/dist/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.js +33 -0
  51. package/dist/test/testSetValue/TestCase8_SetFieldValues_NestedObject.d.ts +27 -0
  52. package/dist/test/testSetValue/TestCase8_SetFieldValues_NestedObject.js +57 -0
  53. package/dist/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.d.ts +25 -0
  54. package/dist/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.js +46 -0
  55. package/dist/test/testSetValue/index.d.ts +2 -0
  56. package/dist/test/testSetValue/index.js +28 -0
  57. package/dist/types/index.d.ts +1 -1
  58. package/dist/types/public.d.ts +1 -1
  59. package/dist/utils/obj.util.d.ts +29 -1
  60. package/dist/utils/obj.util.js +59 -5
  61. package/package.json +2 -1
  62. package/src/App.tsx +39 -156
  63. package/src/DEEP_TRIGGER_LOGIC.md +573 -0
  64. package/src/components/Form/FormCleanUp.tsx +4 -8
  65. package/src/components/Form/FormItem.tsx +174 -57
  66. package/src/components/Form/FormList.tsx +17 -4
  67. package/src/constants/form.ts +1 -1
  68. package/src/hooks/useFormItemControl.ts +78 -32
  69. package/src/hooks/useFormListControl.ts +133 -43
  70. package/src/index.ts +25 -13
  71. package/src/main.tsx +6 -1
  72. package/src/providers/Form.tsx +454 -26
  73. package/src/stores/formStore.ts +363 -283
  74. package/src/test/CommonTest.tsx +177 -0
  75. package/src/test/TestDialog.tsx +52 -0
  76. package/src/test/TestListener.tsx +21 -0
  77. package/src/test/TestNotFormWrapper.tsx +43 -0
  78. package/src/test/TestSelect.tsx +38 -0
  79. package/src/test/TestWatchNormalize.tsx +32 -0
  80. package/src/test/TestWrapperFormItem.tsx +34 -0
  81. package/src/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.tsx +203 -0
  82. package/src/test/testSetValue/TestCase1_PlainObjectToPrimitives.tsx +72 -0
  83. package/src/test/testSetValue/TestCase2_PlainObjectToFormList.tsx +114 -0
  84. package/src/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.tsx +99 -0
  85. package/src/test/testSetValue/TestCase4_PlainObjectRemovedFields.tsx +112 -0
  86. package/src/test/testSetValue/TestCase5_FormListRemovedItems.tsx +119 -0
  87. package/src/test/testSetValue/TestCase6_NestedFormListRemoved.tsx +185 -0
  88. package/src/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.tsx +110 -0
  89. package/src/test/testSetValue/TestCase8_SetFieldValues_NestedObject.tsx +162 -0
  90. package/src/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.tsx +169 -0
  91. package/src/test/testSetValue/index.tsx +100 -0
  92. package/src/types/index.ts +1 -1
  93. package/src/types/public.ts +1 -1
  94. 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;