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.
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 +197 -22
  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 +38 -163
  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 +451 -23
  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,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 } = useFormCleanUp(
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 } = useFormListeners(
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
  }