react-form-manage 1.0.8-beta.9 → 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 +197 -22
- 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 +38 -163
- 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 +451 -23
- 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,573 @@
|
|
|
1
|
+
# Deep Trigger Set Value - Logic Chi Tiết
|
|
2
|
+
|
|
3
|
+
## Tổng Quan
|
|
4
|
+
|
|
5
|
+
`handleDeepTriggerSet` là hàm thực thi việc set value và trigger listener một cách đệ quy, đảm bảo:
|
|
6
|
+
|
|
7
|
+
1. **Thứ tự trigger đúng**: Field level → Item/Index level → Property level
|
|
8
|
+
2. **Xử lý nested structures**: Plain objects, arrays, class instances
|
|
9
|
+
3. **Phân biệt listener types**: Array listeners (FormList) vs Non-array listeners
|
|
10
|
+
4. **Handle data cleanup**: Khi phần tử bị xóa khỏi array/object
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quy Tắc Core
|
|
15
|
+
|
|
16
|
+
### Quy tắc 1: Primitive Values
|
|
17
|
+
|
|
18
|
+
**Áp dụng:** `string`, `number`, `boolean`, `null`, `undefined`
|
|
19
|
+
|
|
20
|
+
**Hành động:**
|
|
21
|
+
|
|
22
|
+
- Tìm listener trên chính field đó
|
|
23
|
+
- Nếu có: gọi `listener.onChange(value)`
|
|
24
|
+
- Nếu không: `setData(formName, name, value)`
|
|
25
|
+
- **DỪNG** - không đệ quy
|
|
26
|
+
|
|
27
|
+
### Quy tắc 2: Plain Objects
|
|
28
|
+
|
|
29
|
+
**Áp dụng:** `isPlainObject(value) === true`
|
|
30
|
+
|
|
31
|
+
**Hành động:**
|
|
32
|
+
|
|
33
|
+
1. Trigger listener trên chính field đó (nếu có)
|
|
34
|
+
2. Lấy tất cả paths (bao gồm container level): `getAllPathsIncludingContainers(value)`
|
|
35
|
+
3. Đệ quy vào từng path: `coreRecursive(name.path, get(value, path))`
|
|
36
|
+
|
|
37
|
+
### Quy tắc 3: Arrays với Listener (FormList)
|
|
38
|
+
|
|
39
|
+
**Áp dụng:** `Array.isArray(value) && listener.type === "array"`
|
|
40
|
+
|
|
41
|
+
**Hành động:**
|
|
42
|
+
|
|
43
|
+
1. Gọi `listener.onArrayChange(value)` → FormList mount/unmount items
|
|
44
|
+
2. Với từng phần tử:
|
|
45
|
+
- Trigger listener trên `name.index` (nếu có)
|
|
46
|
+
- Đệ quy vào nested paths
|
|
47
|
+
3. Xử lý phần tử bị xóa: trigger undefined đệ quy
|
|
48
|
+
|
|
49
|
+
### Quy tắc 4: Arrays không có Listener
|
|
50
|
+
|
|
51
|
+
**Áp dụng:** `Array.isArray(value) && !listener`
|
|
52
|
+
|
|
53
|
+
**Hành động:**
|
|
54
|
+
|
|
55
|
+
1. **Set mảng về kích thước mới TRƯỚC**: `setData(formName, name, value)`
|
|
56
|
+
2. Với từng phần tử thay đổi:
|
|
57
|
+
- Trigger listener trên `name.index` (nếu có)
|
|
58
|
+
- Đệ quy vào nested paths
|
|
59
|
+
3. Xử lý phần tử bị xóa: trigger undefined đệ quy
|
|
60
|
+
|
|
61
|
+
### Quy tắc 5: Class Instances
|
|
62
|
+
|
|
63
|
+
**Áp dụng:** `!isPlainObject(value) && typeof value === "object"`
|
|
64
|
+
|
|
65
|
+
**Hành động:** Xử lý như primitive (không đệ quy)
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Case Studies với Examples
|
|
70
|
+
|
|
71
|
+
### Case 1: Plain Object → Primitive Listeners
|
|
72
|
+
|
|
73
|
+
#### Setup:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Listeners đã đăng ký:
|
|
77
|
+
// - "user" (non-array listener)
|
|
78
|
+
// - "user.name" (primitive)
|
|
79
|
+
// - "user.age" (primitive)
|
|
80
|
+
|
|
81
|
+
setFieldValue("user", { name: "John", age: 30 });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Execution Flow:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
deepTriggerSetCore("user", {name: "John", age: 30})
|
|
88
|
+
│
|
|
89
|
+
├─ isPlainObject = true
|
|
90
|
+
│
|
|
91
|
+
└─ handlePlainObject("user", {name: "John", age: 30})
|
|
92
|
+
│
|
|
93
|
+
├─ Step 1: Trigger field-level listener
|
|
94
|
+
│ └─ user.onChange({name: "John", age: 30}) ✅
|
|
95
|
+
│
|
|
96
|
+
├─ Step 2: Get all paths
|
|
97
|
+
│ └─ getAllPathsIncludingContainers({name: "John", age: 30})
|
|
98
|
+
│ └─ ["name", "age"]
|
|
99
|
+
│
|
|
100
|
+
└─ Step 3: Recursive into each path
|
|
101
|
+
│
|
|
102
|
+
├─ coreRecursive("user.name", "John")
|
|
103
|
+
│ └─ handlePrimitiveValue("user.name", "John")
|
|
104
|
+
│ ├─ user.name.onChange("John") ✅
|
|
105
|
+
│ └─ setData(formName, "user.name", "John")
|
|
106
|
+
│
|
|
107
|
+
└─ coreRecursive("user.age", 30)
|
|
108
|
+
└─ handlePrimitiveValue("user.age", 30)
|
|
109
|
+
├─ user.age.onChange(30) ✅
|
|
110
|
+
└─ setData(formName, "user.age", 30)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Result:
|
|
114
|
+
|
|
115
|
+
- ✅ "user" listener: `{name: "John", age: 30}`
|
|
116
|
+
- ✅ "user.name" listener: `"John"`
|
|
117
|
+
- ✅ "user.age" listener: `30`
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### Case 2: Plain Object → Array Listener (FormList) → Primitives
|
|
122
|
+
|
|
123
|
+
#### Setup:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// Listeners:
|
|
127
|
+
// - "user"
|
|
128
|
+
// - "user.name"
|
|
129
|
+
// - "user.orders" (array listener - FormList)
|
|
130
|
+
// - "user.orders[0]", "user.orders[1]"
|
|
131
|
+
// - "user.orders[0].id", "user.orders[0].product"
|
|
132
|
+
// - "user.orders[1].id", "user.orders[1].product"
|
|
133
|
+
|
|
134
|
+
setFieldValue("user", {
|
|
135
|
+
name: "John",
|
|
136
|
+
orders: [
|
|
137
|
+
{ id: 1, product: "A" },
|
|
138
|
+
{ id: 2, product: "B" },
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Execution Flow:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
deepTriggerSetCore("user", {name: "John", orders: [...]})
|
|
147
|
+
│
|
|
148
|
+
└─ handlePlainObject("user", {name: "John", orders: [...]})
|
|
149
|
+
│
|
|
150
|
+
├─ Step 1: Trigger field-level
|
|
151
|
+
│ └─ user.onChange({name: "John", orders: [...]}) ✅
|
|
152
|
+
│
|
|
153
|
+
├─ Step 2: Get paths
|
|
154
|
+
│ └─ ["name", "orders", "orders[0]", "orders[0].id", "orders[0].product",
|
|
155
|
+
│ "orders[1]", "orders[1].id", "orders[1].product"]
|
|
156
|
+
│
|
|
157
|
+
└─ Step 3: Recursive
|
|
158
|
+
│
|
|
159
|
+
├─ coreRecursive("user.name", "John")
|
|
160
|
+
│ └─ user.name.onChange("John") ✅
|
|
161
|
+
│
|
|
162
|
+
└─ coreRecursive("user.orders", [{id: 1, ...}, {id: 2, ...}])
|
|
163
|
+
│
|
|
164
|
+
└─ handleArrayListener("user.orders", [...])
|
|
165
|
+
│
|
|
166
|
+
├─ Step 3.1: onArrayChange
|
|
167
|
+
│ └─ user.orders.onArrayChange([...]) ✅
|
|
168
|
+
│ └─ FormList mounts 2 items
|
|
169
|
+
│
|
|
170
|
+
└─ Step 3.2: Process each item
|
|
171
|
+
│
|
|
172
|
+
├─ Item 0:
|
|
173
|
+
│ ├─ user.orders[0].onChange({id: 1, product: "A"}) ✅
|
|
174
|
+
│ └─ Recursive nested:
|
|
175
|
+
│ ├─ coreRecursive("user.orders[0].id", 1)
|
|
176
|
+
│ │ └─ user.orders[0].id.onChange(1) ✅
|
|
177
|
+
│ └─ coreRecursive("user.orders[0].product", "A")
|
|
178
|
+
│ └─ user.orders[0].product.onChange("A") ✅
|
|
179
|
+
│
|
|
180
|
+
└─ Item 1: (similar)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Result:
|
|
184
|
+
|
|
185
|
+
- ✅ "user" listener: Full object
|
|
186
|
+
- ✅ "user.name" listener: `"John"`
|
|
187
|
+
- ✅ "user.orders" FormList: Array updated, 2 items mounted
|
|
188
|
+
- ✅ All nested primitive listeners triggered
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### Case 3: Array Non-Listener → Primitives
|
|
193
|
+
|
|
194
|
+
#### Setup:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Listeners:
|
|
198
|
+
// - "tags[0]", "tags[1]" (individual FormItems, NO FormList)
|
|
199
|
+
// NO listener on "tags" itself
|
|
200
|
+
|
|
201
|
+
setFieldValue("tags", ["javascript", "react"]);
|
|
202
|
+
// Previous: ["vue", "angular", "svelte"]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### Execution Flow:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
deepTriggerSetCore("tags", ["javascript", "react"])
|
|
209
|
+
│
|
|
210
|
+
└─ handleNonListenerArray("tags", ["javascript", "react"])
|
|
211
|
+
│
|
|
212
|
+
├─ previousValues = ["vue", "angular", "svelte"]
|
|
213
|
+
│
|
|
214
|
+
├─ Step 1: Set array to new size FIRST
|
|
215
|
+
│ └─ setData(formName, "tags", ["javascript", "react"]) ✅
|
|
216
|
+
│ └─ Watchers of "tags" see array cut: 3 → 2
|
|
217
|
+
│
|
|
218
|
+
├─ Step 2: Process changed items
|
|
219
|
+
│ │
|
|
220
|
+
│ ├─ Index 0: "javascript" !== "vue"
|
|
221
|
+
│ │ ├─ tags[0].onChange("javascript") ✅
|
|
222
|
+
│ │ └─ No nested paths (primitive)
|
|
223
|
+
│ │
|
|
224
|
+
│ └─ Index 1: "react" !== "angular"
|
|
225
|
+
│ ├─ tags[1].onChange("react") ✅
|
|
226
|
+
│ └─ No nested paths
|
|
227
|
+
│
|
|
228
|
+
└─ Step 3: Process removed items
|
|
229
|
+
└─ for index = 2 to 2:
|
|
230
|
+
└─ coreRecursive("tags[2]", undefined)
|
|
231
|
+
└─ handlePrimitiveValue("tags[2]", undefined)
|
|
232
|
+
└─ if tags[2] listener exists:
|
|
233
|
+
└─ tags[2].onChange(undefined) ✅
|
|
234
|
+
└─ else:
|
|
235
|
+
└─ setData(formName, "tags[2]", undefined) ✅
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Result:
|
|
239
|
+
|
|
240
|
+
- ✅ Data "tags": `["javascript", "react"]` (array cut)
|
|
241
|
+
- ✅ Watcher sees new array length
|
|
242
|
+
- ✅ "tags[0]" listener: `"javascript"`
|
|
243
|
+
- ✅ "tags[1]" listener: `"react"`
|
|
244
|
+
- ✅ "tags[2]": `undefined` (data cleaned or listener notified)
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### Case 4: Plain Object with Removed Fields
|
|
249
|
+
|
|
250
|
+
#### Setup:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// Previous data: {name: "John", age: 30, city: "NY"}
|
|
254
|
+
// Listeners: "user", "user.name", "user.age", "user.city"
|
|
255
|
+
|
|
256
|
+
setFieldValue("user", { name: "John", age: 31 });
|
|
257
|
+
// "city" field REMOVED
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Execution Flow:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
deepTriggerSetCore("user", {name: "John", age: 31})
|
|
264
|
+
│
|
|
265
|
+
└─ handlePlainObject("user", {name: "John", age: 31})
|
|
266
|
+
│
|
|
267
|
+
├─ Step 1: Trigger field-level
|
|
268
|
+
│ └─ user.onChange({name: "John", age: 31}) ✅
|
|
269
|
+
│ └─ Listener receives NEW object (without city)
|
|
270
|
+
│
|
|
271
|
+
├─ Step 2: Get paths from NEW value
|
|
272
|
+
│ └─ ["name", "age"]
|
|
273
|
+
│ └─ ⚠️ "city" NOT included
|
|
274
|
+
│
|
|
275
|
+
└─ Step 3: Recursive
|
|
276
|
+
├─ coreRecursive("user.name", "John")
|
|
277
|
+
└─ coreRecursive("user.age", 31) ✅
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Problem:
|
|
281
|
+
|
|
282
|
+
- ❌ "user.city" listener NOT triggered
|
|
283
|
+
- ❌ Data "user.city" still has old value "NY"
|
|
284
|
+
|
|
285
|
+
#### Solution Needed:
|
|
286
|
+
|
|
287
|
+
Compare previousValue with newValue → detect removed fields → trigger undefined
|
|
288
|
+
|
|
289
|
+
**Current Status:** ⚠️ NOT HANDLED - Needs fix
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### Case 5: Array Listener (FormList) with Removed Items
|
|
294
|
+
|
|
295
|
+
#### Setup:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// Previous: [{id: 1}, {id: 2}, {id: 3}]
|
|
299
|
+
// Listeners:
|
|
300
|
+
// - "items" (array listener - FormList)
|
|
301
|
+
// - "items[0].id", "items[1].id", "items[2].id"
|
|
302
|
+
|
|
303
|
+
setFieldValue("items", [{ id: 1 }]);
|
|
304
|
+
// items[1], items[2] REMOVED
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
#### Execution Flow:
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
deepTriggerSetCore("items", [{id: 1}])
|
|
311
|
+
│
|
|
312
|
+
└─ handleArrayListener("items", [{id: 1}])
|
|
313
|
+
│
|
|
314
|
+
├─ currentValue = [{id: 1}, {id: 2}, {id: 3}]
|
|
315
|
+
│
|
|
316
|
+
├─ Step 1: onArrayChange
|
|
317
|
+
│ └─ items.onArrayChange([{id: 1}]) ✅
|
|
318
|
+
│ └─ FormList unmounts items[1], items[2]
|
|
319
|
+
│ └─ Listeners "items[1].id", "items[2].id" REVOKED
|
|
320
|
+
│
|
|
321
|
+
├─ Step 2: Process remaining items
|
|
322
|
+
│ └─ items[0]:
|
|
323
|
+
│ ├─ items[0].onChange({id: 1}) ✅
|
|
324
|
+
│ └─ items[0].id.onChange(1) ✅
|
|
325
|
+
│
|
|
326
|
+
└─ Step 3: Process removed items
|
|
327
|
+
└─ for index = 1 to 2:
|
|
328
|
+
└─ coreRecursive("items[1]", undefined)
|
|
329
|
+
└─ handlePrimitiveValue("items[1]", undefined)
|
|
330
|
+
└─ listener = getListeners("items[1]")
|
|
331
|
+
└─ NOT FOUND (already revoked)
|
|
332
|
+
└─ setData(formName, "items[1]", undefined) ✅
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### Result:
|
|
336
|
+
|
|
337
|
+
- ✅ FormList unmounts items[1], items[2]
|
|
338
|
+
- ✅ Listeners revoked
|
|
339
|
+
- ✅ Data cleaned with undefined
|
|
340
|
+
- ✅ External watchers (if any) receive undefined
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
### Case 6: Array Non-Listener → Nested Array Listener Removed
|
|
345
|
+
|
|
346
|
+
#### Setup:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// Previous: [
|
|
350
|
+
// {id: 1, tags: ["a", "b"]}, // tags is FormList
|
|
351
|
+
// {id: 2, tags: ["c", "d"]} // ← REMOVED
|
|
352
|
+
// ]
|
|
353
|
+
// Listeners:
|
|
354
|
+
// - "data[0].tags", "data[1].tags" (array listeners)
|
|
355
|
+
// - "data[0].tags[0]", "data[0].tags[1]"
|
|
356
|
+
// - "data[1].tags[0]", "data[1].tags[1]"
|
|
357
|
+
|
|
358
|
+
setFieldValue("data", [{ id: 1, tags: ["a", "b"] }]);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
#### Execution Flow:
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
deepTriggerSetCore("data", [{id: 1, tags: ["a", "b"]}])
|
|
365
|
+
│
|
|
366
|
+
└─ handleNonListenerArray("data", [{id: 1, tags: ["a", "b"]}])
|
|
367
|
+
│
|
|
368
|
+
├─ Step 1: Set array to new size
|
|
369
|
+
│ └─ setData(formName, "data", [{id: 1, tags: [...]}]) ✅
|
|
370
|
+
│
|
|
371
|
+
├─ Step 2: Process data[0]
|
|
372
|
+
│ ├─ data[0].onChange({id: 1, tags: [...]}) ✅
|
|
373
|
+
│ └─ Recursive nested:
|
|
374
|
+
│ ├─ coreRecursive("data[0].id", 1) ✅
|
|
375
|
+
│ └─ coreRecursive("data[0].tags", ["a", "b"])
|
|
376
|
+
│ └─ handleArrayListener("data[0].tags", [...])
|
|
377
|
+
│ ├─ data[0].tags.onArrayChange([...]) ✅
|
|
378
|
+
│ └─ Recursive items... ✅
|
|
379
|
+
│
|
|
380
|
+
└─ Step 3: Process removed items
|
|
381
|
+
└─ coreRecursive("data[1]", undefined)
|
|
382
|
+
└─ handlePrimitiveValue("data[1]", undefined)
|
|
383
|
+
├─ data[1].onChange(undefined) ✅ (if exists)
|
|
384
|
+
└─ setData(formName, "data[1]", undefined) ✅
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### Problem:
|
|
388
|
+
|
|
389
|
+
- ❌ `data[1].tags` (nested array listener) NOT triggered
|
|
390
|
+
- ❌ `data[1].tags[0]`, `data[1].tags[1]` NOT triggered
|
|
391
|
+
- **Root cause:** `coreRecursive("data[1]", undefined)` only handles primitive
|
|
392
|
+
- Does NOT recurse into nested paths of OLD value
|
|
393
|
+
|
|
394
|
+
#### Solution Needed:
|
|
395
|
+
|
|
396
|
+
When triggering undefined for removed items, must recurse into nested paths of **previousValue**
|
|
397
|
+
|
|
398
|
+
**Current Status:** ⚠️ NOT HANDLED - Needs fix
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Edge Cases Summary
|
|
403
|
+
|
|
404
|
+
### ✅ Handled Correctly:
|
|
405
|
+
|
|
406
|
+
1. Plain object → primitives
|
|
407
|
+
2. Plain object → array listener (FormList) → primitives
|
|
408
|
+
3. Array non-listener → primitives
|
|
409
|
+
4. Array listener with removed items (FormList auto-cleanup)
|
|
410
|
+
5. Deep nesting with multiple array listeners
|
|
411
|
+
|
|
412
|
+
### ⚠️ Needs Fix:
|
|
413
|
+
|
|
414
|
+
1. **Plain object with removed fields**
|
|
415
|
+
- Fields removed from object not triggered with undefined
|
|
416
|
+
- Solution: Compare previousValue, detect removed keys, trigger undefined
|
|
417
|
+
|
|
418
|
+
2. **Array non-listener with nested array listeners removed**
|
|
419
|
+
- Nested FormLists in removed items not triggered
|
|
420
|
+
- Solution: When triggering undefined for removed item, recurse into its nested structure using previousValue
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Implementation Notes
|
|
425
|
+
|
|
426
|
+
### Key Functions:
|
|
427
|
+
|
|
428
|
+
- `handlePrimitiveValue`: Terminal handler for primitives
|
|
429
|
+
- `handlePlainObject`: Handles plain objects, triggers field-level then recurses
|
|
430
|
+
- `handleArrayListener`: Handles FormList arrays, calls onArrayChange then recurses
|
|
431
|
+
- `handleNonListenerArray`: Handles non-FormList arrays, sets data then recurses
|
|
432
|
+
- `deepTriggerSetCore`: Core router, dispatches to appropriate handler
|
|
433
|
+
|
|
434
|
+
### Important Utilities:
|
|
435
|
+
|
|
436
|
+
- `getAllPathsIncludingContainers(value)`: Returns ALL paths including intermediate containers
|
|
437
|
+
- Example: `{user: {profile: {age: 30}}}` → `["user", "user.profile", "user.profile.age"]`
|
|
438
|
+
- `getAllNoneObjStringPath(value)`: Returns only leaf paths (primitives)
|
|
439
|
+
- Example: Same object → `["user.profile.age"]`
|
|
440
|
+
|
|
441
|
+
### Data vs Listener Separation:
|
|
442
|
+
|
|
443
|
+
- **setData**: Updates store data only
|
|
444
|
+
- **listener.onChange**: Triggers listener callback (may cause re-render)
|
|
445
|
+
- **revokeListener**: Removes listener from store (happens on FormItem unmount)
|
|
446
|
+
- **setFieldValue**: Should update data AND trigger listeners appropriately
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## TODO: Fixes Required
|
|
451
|
+
|
|
452
|
+
### Fix 1: Handle Removed Object Fields
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// In handlePlainObject, after recursive:
|
|
456
|
+
const previousValue = getFieldValue(name);
|
|
457
|
+
if (isPlainObject(previousValue)) {
|
|
458
|
+
const previousKeys = Object.keys(previousValue);
|
|
459
|
+
const newKeys = Object.keys(value);
|
|
460
|
+
const removedKeys = previousKeys.filter((k) => !newKeys.includes(k));
|
|
461
|
+
|
|
462
|
+
removedKeys.forEach((key) => {
|
|
463
|
+
if (coreRecursive) {
|
|
464
|
+
coreRecursive(`${name}.${key}`, undefined, options);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Fix 2: Handle Removed Array Items with Nested Structures
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
// In handleNonListenerArray, when processing removed items:
|
|
474
|
+
// Instead of just: coreRecursive(itemName, undefined)
|
|
475
|
+
// Should get nested paths from previousValue[index] and recurse each
|
|
476
|
+
|
|
477
|
+
if (previousValues.length > value.length) {
|
|
478
|
+
for (let index = value.length; index < previousValues.length; index++) {
|
|
479
|
+
const itemName = `${name}.${index}`;
|
|
480
|
+
const removedItem = previousValues[index];
|
|
481
|
+
|
|
482
|
+
// Get all nested paths from REMOVED item
|
|
483
|
+
const nestedPaths = getAllPathsIncludingContainers(removedItem);
|
|
484
|
+
|
|
485
|
+
// Trigger undefined for each nested path
|
|
486
|
+
nestedPaths.forEach((p) => {
|
|
487
|
+
if (coreRecursive) {
|
|
488
|
+
coreRecursive(`${itemName}.${p}`, undefined, options);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Also trigger for the item itself
|
|
493
|
+
coreRecursive(itemName, undefined, options);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Testing Scenarios
|
|
501
|
+
|
|
502
|
+
### Scenario 1: Deep Nesting
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
setFieldValue("company", {
|
|
506
|
+
name: "Acme",
|
|
507
|
+
departments: [
|
|
508
|
+
{
|
|
509
|
+
name: "Engineering",
|
|
510
|
+
teams: [
|
|
511
|
+
{ name: "Frontend", members: ["Alice", "Bob"] },
|
|
512
|
+
{ name: "Backend", members: ["Charlie"] },
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Expected: All listeners at every level triggered in correct order
|
|
520
|
+
|
|
521
|
+
### Scenario 2: Complex Removal
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
// From 3 departments to 1
|
|
525
|
+
setFieldValue("company.departments", [
|
|
526
|
+
{name: "Engineering", teams: [...]}
|
|
527
|
+
]);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Expected:
|
|
531
|
+
|
|
532
|
+
- departments[1], departments[2] cleaned
|
|
533
|
+
- All nested teams/members listeners notified
|
|
534
|
+
|
|
535
|
+
### Scenario 3: Mixed Structures
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
setFieldValue("config", {
|
|
539
|
+
settings: { key: "value" },
|
|
540
|
+
items: [{ id: 1 }],
|
|
541
|
+
flags: ["a", "b"],
|
|
542
|
+
metadata: new Date(), // class instance
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Expected: Each type handled correctly per rules
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Performance Considerations
|
|
551
|
+
|
|
552
|
+
While performance is not the primary concern (setFieldValue is not called continuously), the implementation should still be reasonable:
|
|
553
|
+
|
|
554
|
+
1. **Listener Lookup**: O(n) where n = number of listeners
|
|
555
|
+
- Could optimize with Map if needed
|
|
556
|
+
2. **Path Collection**: O(m) where m = object/array depth
|
|
557
|
+
- Unavoidable for complete traversal
|
|
558
|
+
3. **Recursive Calls**: Depth-first traversal
|
|
559
|
+
- Stack depth = nesting depth
|
|
560
|
+
- Should handle typical form structures (< 10 levels)
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Conclusion
|
|
565
|
+
|
|
566
|
+
The deep trigger logic ensures complete and correct propagation of value changes through nested form structures. The key principles are:
|
|
567
|
+
|
|
568
|
+
1. **Top-down triggering**: Field level first, then nested
|
|
569
|
+
2. **Recursive by design**: Each level handled independently
|
|
570
|
+
3. **Listener-aware**: Different handling for FormList vs regular fields
|
|
571
|
+
4. **Cleanup on removal**: Removed data properly cleaned and notified
|
|
572
|
+
|
|
573
|
+
The remaining edge cases (removed object fields, nested array listeners in removed items) need to be addressed for complete correctness.
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { useShallow } from "zustand/react/shallow";
|
|
3
|
-
import {
|
|
4
|
-
useFormCleanUp,
|
|
5
|
-
useFormListeners,
|
|
6
|
-
useFormStore,
|
|
7
|
-
} from "../../stores/formStore";
|
|
3
|
+
import { useFormStore } from "../../stores/formStore";
|
|
8
4
|
|
|
9
5
|
const FormCleanUp = () => {
|
|
10
|
-
const { cleanUpStack, clearCleanUpStack } =
|
|
6
|
+
const { cleanUpStack, clearCleanUpStack } = useFormStore(
|
|
11
7
|
useShallow((state) => ({
|
|
12
8
|
cleanUpStack: state.cleanUpStack,
|
|
13
9
|
clearCleanUpStack: state.clearCleanUpStack,
|
|
14
10
|
})),
|
|
15
11
|
);
|
|
16
12
|
|
|
17
|
-
const { revokeListener } =
|
|
13
|
+
const { revokeListener } = useFormStore(
|
|
18
14
|
useShallow((state) => ({
|
|
19
15
|
revokeListener: state.revokeListener,
|
|
20
16
|
})),
|
|
@@ -42,7 +38,7 @@ const FormCleanUp = () => {
|
|
|
42
38
|
});
|
|
43
39
|
} else {
|
|
44
40
|
if (c.type === "array") {
|
|
45
|
-
console.log(c);
|
|
41
|
+
// console.log(c);
|
|
46
42
|
clearArrItem(c.formName, c.name);
|
|
47
43
|
} else {
|
|
48
44
|
}
|