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
@@ -1,30 +1,51 @@
1
- import { cloneDeep, get, isEqual, last, set, uniq } from "lodash";
1
+ import {
2
+ cloneDeep,
3
+ filter,
4
+ get,
5
+ isArray,
6
+ isEqual,
7
+ isNil,
8
+ isPlainObject,
9
+ last,
10
+ set,
11
+ uniqBy,
12
+ } from "lodash";
2
13
  import { useTask } from "minh-custom-hooks-release";
3
14
  import type { ComponentType, FormHTMLAttributes, ReactNode } from "react";
4
- import { createContext, useContext, useEffect, useState } from "react";
15
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
5
16
  import { flushSync } from "react-dom";
6
17
  import { useShallow } from "zustand/react/shallow"; // Import useShallow
7
- import FormCleanUp from "../components/Form/FormCleanUp";
18
+ import FormItem from "../components/Form/FormItem";
8
19
  import { SUBMIT_STATE } from "../constants/form";
9
- import { useFormListeners, useFormStore } from "../stores/formStore";
20
+ import { OnChangeOptions } from "../hooks/useFormItemControl";
21
+ import { ListenerItem, SubmitProps, useFormStore } from "../stores/formStore";
10
22
  import type {
11
23
  PublicFormInstance,
12
24
  UseFormItemStateWatchReturn,
13
25
  } from "../types/public";
14
- import { getAllNoneObjStringPath } from "../utils/obj.util";
26
+ import {
27
+ getAllNoneObjStringPath,
28
+ getAllPathsIncludingContainers,
29
+ getAllPathsStopAtArray,
30
+ } from "../utils/obj.util";
15
31
  export type {
16
32
  FormFieldError,
17
33
  SubmitState,
18
34
  UseFormItemStateWatchReturn,
19
- ValidationRule,
35
+ ValidationRule
20
36
  } from "../types/public";
21
37
 
22
38
  export const FormContext = createContext(null);
23
39
 
40
+ export interface SetFieldValueOptions extends OnChangeOptions {
41
+ deepTrigger?: boolean;
42
+ }
43
+
24
44
  export interface FormProps<T = any> extends Omit<
25
45
  FormHTMLAttributes<HTMLFormElement>,
26
46
  "onSubmit"
27
47
  > {
48
+ collectHiddenFields?: boolean;
28
49
  children: ReactNode;
29
50
  formName: string;
30
51
  initialValues?: T;
@@ -46,10 +67,11 @@ export default function Form<T = any>({
46
67
  onReject,
47
68
  onFinally,
48
69
  FormElement,
70
+ collectHiddenFields: formCollectHiddenFields = true,
49
71
  ...props
50
72
  }: FormProps<T>) {
51
73
  const {
52
- appInitValue,
74
+ // appInitValue,
53
75
  getFormItemValue,
54
76
  setInitData,
55
77
  setData,
@@ -63,7 +85,7 @@ export default function Form<T = any>({
63
85
  clearFormState,
64
86
  } = useFormStore(
65
87
  useShallow((state) => ({
66
- appInitValue: state.initialValues,
88
+ // appInitValue: state.initialValues,
67
89
  setInitData: state.setInitData,
68
90
  getFormValues: state.getFormValues,
69
91
  setFormState: state.setFormState,
@@ -77,11 +99,11 @@ export default function Form<T = any>({
77
99
  clearFormState: state.clearFormState,
78
100
 
79
101
  // Test, nhớ xóa sau khi xong
80
- formStates: state.formStates?.[formName],
102
+ // formStates: state.formStates?.[formName],
81
103
  })),
82
104
  );
83
105
 
84
- const { getListeners } = useFormListeners(
106
+ const { getListeners } = useFormStore(
85
107
  useShallow((state) => {
86
108
  return {
87
109
  getListeners: state.getListeners,
@@ -89,10 +111,68 @@ export default function Form<T = any>({
89
111
  }),
90
112
  );
91
113
 
92
- const setFieldValue = (name, value, options) => {
114
+ /**
115
+ * Deep Trigger thực thi set value đệ quy theo các quy tắc sau:
116
+ *
117
+ * (Quy tắc 1) Với các value có giá trị nguyên thủy (string, number, boolean, null, undefined):
118
+ * - Tìm listener trên chính field đó
119
+ * - Nếu có listener: gọi onChange
120
+ * - Nếu không: setData
121
+ * - KHÔNG đệ quy tiếp
122
+ *
123
+ * (Quy tắc 2) Với các value là Plain Object (object thuần):
124
+ * - TRƯỚC TIÊN trigger listener trên chính field đó (nếu có)
125
+ * - SAU ĐÓ đệ quy vào từng path con bằng cách gọi deepTriggerSetCore
126
+ * - Ví dụ: setFieldValue("user", {name: "John", age: 30})
127
+ * → Trigger "user" listener (nếu có)
128
+ * → Gọi deepTriggerSetCore("user.name", "John")
129
+ * → Gọi deepTriggerSetCore("user.age", 30)
130
+ *
131
+ * (Quy tắc 2.2) Class Instance (non-plain object):
132
+ * - Xử lý như primitive value (không đệ quy)
133
+ *
134
+ * (Quy tắc 3) Với các value là Array:
135
+ * - Kiểm tra kích thước mảng mới và mảng cũ (lấy từ getFieldValue)
136
+ * - Nếu kích thước >= mảng cũ: đệ quy cho TẤT CẢ phần tử
137
+ * - Nếu kích thước < mảng cũ: chỉ đệ quy cho phần tử trong khoảng kích thước mới
138
+ *
139
+ * (Quy tắc 4) Với các field có listener:
140
+ * (Quy tắc 4.1) Nếu là array listener:
141
+ * - Gọi onArrayChange(value)
142
+ * - SAU ĐÓ với TỪNG phần tử tại index i:
143
+ * → TRƯỚC TIÊN trigger listener trên "name[i]" (nếu có)
144
+ * → SAU ĐÓ đệ quy vào property con bằng deepTriggerSetCore("name[i].property", value)
145
+ * (Quy tắc 4.2) Nếu là non-array listener với plain object value:
146
+ * - TRƯỚC TIÊN trigger onChange trên listener
147
+ * - SAU ĐÓ đệ quy vào từng path con
148
+ *
149
+ * (Quy tắc 5) Với các field không có listener:
150
+ * (Quy tắc 5.1) Primitive value + Class instance: setData
151
+ * (Quy tắc 5.2) Plain object hoặc array:
152
+ * - Với array: với TỪNG phần tử tại index i:
153
+ * → TRƯỚC TIÊN trigger listener trên "name[i]" (nếu có)
154
+ * → SAU ĐÓ đệ quy vào property con
155
+ * - Với plain object: đệ quy vào từng path con
156
+ *
157
+ * LƯU Ý QUAN TRỌNG - THỨ TỰ TRIGGER:
158
+ * 1. Trigger listener trên chính field này (nếu có)
159
+ * 2. Trigger listener trên mỗi item/index level (nếu có)
160
+ * 3. Rồi mới đệ quy vào property con
161
+ * → Đảm bảo TOÀN BỘ listener được trigger theo đúng thứ tự hierarchical
162
+ */
163
+
164
+ /**
165
+ * Quy tắc 1: Xử lý primitive value
166
+ */
167
+ const handlePrimitiveValue = (
168
+ name: string,
169
+ value: any,
170
+ options?: SetFieldValueOptions,
171
+ ) => {
93
172
  const listener = getListeners().find(
94
173
  (l) => l.name === name && l.formName === formName,
95
174
  );
175
+
96
176
  if (listener) {
97
177
  listener.onChange(value, options);
98
178
  } else {
@@ -100,24 +180,325 @@ export default function Form<T = any>({
100
180
  }
101
181
  };
102
182
 
183
+ /**
184
+ * Quy tắc 4.1: Xử lý array listener - gọi onArrayChange, trigger item[index] listener, rồi đệ quy con
185
+ *
186
+ * Xử lý 2 trường hợp:
187
+ * 1. Phần tử thay đổi: trigger item level + nested listeners
188
+ * 2. Phần tử bị xóa (previousValues.length > value.length): xóa dữ liệu + cleanup
189
+ */
190
+ const handleArrayListener = (
191
+ name: string,
192
+ value: any[],
193
+ options?: SetFieldValueOptions,
194
+ coreRecursive?: (n: string, v: any, o?: SetFieldValueOptions) => void,
195
+ ) => {
196
+ const listener = getListeners().find(
197
+ (l) => l.name === name && l.formName === formName,
198
+ );
199
+
200
+ if (!listener) return;
201
+
202
+ const currentValue = getFormItemValue(formName, name);
203
+ if (!isEqual(currentValue, value)) {
204
+ // TRƯỚC TIÊN: Gọi onArrayChange trên listener
205
+ listener.onArrayChange(value, options);
206
+
207
+ // SAU ĐÓ: Với TỪNG phần tử, trigger listener trên item level, rồi đệ quy vào con
208
+ value.forEach((item, index) => {
209
+ const itemName = `${name}.${index}`;
210
+
211
+ // TRƯỚC TIÊN trigger listener trên item level (nếu có)
212
+ const itemListener = getListeners().find(
213
+ (l) => l.name === itemName && l.formName === formName,
214
+ );
215
+ if (itemListener) {
216
+ itemListener.onChange(item, options);
217
+ }
218
+
219
+ // SAU ĐÓ lấy tất cả paths bao gồm containers + leaf nodes để trigger nested listeners
220
+ const nestedPaths = getAllPathsIncludingContainers(item);
221
+ nestedPaths.forEach((p) => {
222
+ if (coreRecursive) {
223
+ coreRecursive(`${itemName}.${p}`, get(item, p), options);
224
+ }
225
+ });
226
+ });
227
+
228
+ // Xử lý phần tử bị xóa (currentValue dài hơn value)
229
+ // QUAN TRỌNG: Phải trigger listener đệ quy với undefined
230
+ // Nếu có nested array listener (FormList), chúng cũng được trigger với undefined
231
+ if (isArray(currentValue) && currentValue.length > value.length) {
232
+ for (let index = value.length; index < currentValue.length; index++) {
233
+ const itemName = `${name}.${index}`;
234
+
235
+ // Trigger listener đệ quy với undefined
236
+ // Điều này sẽ trigger tất cả listener (bao gồm nested array listener)
237
+ if (coreRecursive) {
238
+ coreRecursive(itemName, undefined, options);
239
+ }
240
+ }
241
+ }
242
+ }
243
+ };
244
+
245
+ /**
246
+ * Quy tắc 4.2 & 5.2: Xử lý plain object - trigger listener trên field này, rồi đệ quy vào con
247
+ * Xử lý thêm: detect fields bị xóa và trigger undefined
248
+ */
249
+ const handlePlainObject = (
250
+ name: string,
251
+ value: any,
252
+ options?: SetFieldValueOptions,
253
+ coreRecursive?: (n: string, v: any, o?: SetFieldValueOptions) => void,
254
+ ) => {
255
+ // TRƯỚC TIÊN: Trigger listener trên chính field này (nếu có)
256
+ const listener = getListeners().find(
257
+ (l) => l.name === name && l.formName === formName,
258
+ );
259
+ if (listener && listener.type !== "array") {
260
+ listener.onChange(value, options);
261
+ }
262
+
263
+ // SAU ĐÓ: Lấy tất cả paths bao gồm containers + leaf nodes
264
+ // Này sẽ trigger listener trên: "user", "user.profile", "user.profile.name" etc.
265
+ const allPaths = getAllPathsIncludingContainers(value);
266
+ allPaths.forEach((p) => {
267
+ if (coreRecursive) {
268
+ const fullPath = `${name}.${p}`;
269
+ coreRecursive(fullPath, get(value, p), options);
270
+ }
271
+ });
272
+
273
+ // EDGE CASE: Xử lý fields bị xóa (có trong previousValue nhưng không có trong value)
274
+ const previousValue = getFieldValue(name);
275
+ if (isPlainObject(previousValue)) {
276
+ const previousKeys = Object.keys(previousValue);
277
+ const newKeys = Object.keys(value);
278
+ const removedKeys = previousKeys.filter((k) => !newKeys.includes(k));
279
+
280
+ // Trigger undefined cho từng field bị xóa
281
+ removedKeys.forEach((key) => {
282
+ if (coreRecursive) {
283
+ coreRecursive(`${name}.${key}`, undefined, options);
284
+ }
285
+ });
286
+ }
287
+ };
288
+
289
+ /**
290
+ * Quy tắc 5.2: Xử lý array non-listener - trigger item[index] listener, rồi đệ quy con
291
+ *
292
+ * Xử lý 2 trường hợp:
293
+ * 1. Phần tử thay đổi: trigger item level + nested listeners
294
+ * 2. Phần tử bị xóa (previousValues.length > value.length):
295
+ * - Set lại mảng về đúng kích thước mới (watcher phải thấy mảng bị cắt)
296
+ * - Trigger undefined cho các item bị xóa (nếu có listener con)
297
+ */
298
+ const handleNonListenerArray = (
299
+ name: string,
300
+ value: any[],
301
+ options?: SetFieldValueOptions,
302
+ coreRecursive?: (n: string, v: any, o?: SetFieldValueOptions) => void,
303
+ ) => {
304
+ const previousValues = getFieldValue(name);
305
+ if (isArray(previousValues)) {
306
+ // QUAN TRỌNG: Set lại mảng về đúng kích thước mới TRƯỚC
307
+ // Đảm bảo watcher nhìn thấy mảng bị cắt: [1,2,3] → [1]
308
+ if (
309
+ previousValues.length !== value.length ||
310
+ !isEqual(previousValues, value)
311
+ ) {
312
+ setData(formName, name, value);
313
+ }
314
+
315
+ // Trường hợp 1: Xử lý phần tử thay đổi
316
+ value.forEach((item, index) => {
317
+ if (!isEqual(previousValues[index], item)) {
318
+ const itemName = `${name}.${index}`;
319
+
320
+ // TRƯỚC TIÊN trigger listener trên item level (nếu có)
321
+ const itemListener = getListeners().find(
322
+ (l) => l.name === itemName && l.formName === formName,
323
+ );
324
+ if (itemListener) {
325
+ itemListener.onChange(item, options);
326
+ }
327
+
328
+ // SAU ĐÓ lấy tất cả paths bao gồm containers + leaf nodes
329
+ const nestedPaths = getAllPathsIncludingContainers(item);
330
+ nestedPaths.forEach((p) => {
331
+ if (coreRecursive) {
332
+ coreRecursive(`${itemName}.${p}`, get(item, p), options);
333
+ }
334
+ });
335
+ }
336
+ });
337
+
338
+ // Trường hợp 2: Trigger undefined cho phần tử bị xóa
339
+ // QUAN TRỌNG: Phải đệ quy vào nested paths của previousValue
340
+ // Để trigger tất cả nested listeners (bao gồm nested FormList)
341
+ if (previousValues.length > value.length) {
342
+ for (let index = value.length; index < previousValues.length; index++) {
343
+ const itemName = `${name}.${index}`;
344
+ const removedItem = previousValues[index];
345
+
346
+ // Lấy tất cả nested paths từ item BỊ XÓA
347
+ const nestedPaths = getAllPathsIncludingContainers(removedItem);
348
+
349
+ // Trigger undefined cho TỪNG nested path
350
+ // VD: data[1].tags (FormList), data[1].tags[0], data[1].tags[1], ...
351
+ nestedPaths.forEach((p) => {
352
+ if (coreRecursive) {
353
+ coreRecursive(`${itemName}.${p}`, undefined, options);
354
+ }
355
+ });
356
+
357
+ // Cuối cùng trigger cho chính item level
358
+ if (coreRecursive) {
359
+ coreRecursive(itemName, undefined, options);
360
+ }
361
+ }
362
+ }
363
+ }
364
+ };
365
+
366
+ /**
367
+ * Hàm lõi đệ quy: Phân loại value và xử lý theo quy tắc tương ứng
368
+ * Thứ tự trigger: Field level → Item/Index level → Property level
369
+ */
370
+ const deepTriggerSetCore = (
371
+ name: string,
372
+ value: any,
373
+ options?: SetFieldValueOptions,
374
+ ): void => {
375
+ // Quy tắc 1: Primitive values (string, number, boolean, null, undefined)
376
+ if (
377
+ typeof value === "string" ||
378
+ typeof value === "number" ||
379
+ typeof value === "boolean" ||
380
+ value === null ||
381
+ value === undefined
382
+ ) {
383
+ handlePrimitiveValue(name, value, options);
384
+ return;
385
+ }
386
+
387
+ // Quy tắc 2 & 3: Object hoặc Array
388
+ if (isPlainObject(value)) {
389
+ const listener = getListeners().find(
390
+ (l) => l.name === name && l.formName === formName,
391
+ );
392
+
393
+ // Quy tắc 4.2 & 5.2: Plain object - trigger field level, rồi đệ quy con
394
+ handlePlainObject(name, value, options, deepTriggerSetCore);
395
+ return;
396
+ }
397
+
398
+ if (Array.isArray(value)) {
399
+ const listener = getListeners().find(
400
+ (l) => l.name === name && l.formName === formName,
401
+ );
402
+
403
+ // Quy tắc 4.1: Array listener - trigger onArrayChange + item level + con
404
+ if (listener && listener.type === "array") {
405
+ handleArrayListener(name, value, options, deepTriggerSetCore);
406
+ } else if (!listener) {
407
+ // Quy tắc 5.2: Array non-listener - trigger item level + con
408
+ handleNonListenerArray(name, value, options, deepTriggerSetCore);
409
+ } else {
410
+ // Listener không phải array type nhưng value là array
411
+ // Xử lý như plain object
412
+ handlePlainObject(name, value, options, deepTriggerSetCore);
413
+ }
414
+ return;
415
+ }
416
+
417
+ // Quy tắc 2.2 & 5.1: Class Instance (non-plain object)
418
+ handlePrimitiveValue(name, value, options);
419
+ };
420
+
421
+ /**
422
+ * Hàm công khai: Entry point cho Deep Trigger Set Value
423
+ */
424
+ const handleDeepTriggerSet = (
425
+ name: string,
426
+ value: any,
427
+ options?: SetFieldValueOptions,
428
+ ) => {
429
+ deepTriggerSetCore(name, value, options);
430
+ };
431
+
432
+ const setFieldValue = (name, value, options?: SetFieldValueOptions) => {
433
+ // Nếu options.deepTrigger = true, gọi handleDeepTriggerSet
434
+ if (options?.deepTrigger) {
435
+ handleDeepTriggerSet(name, value, options);
436
+ return;
437
+ }
438
+
439
+ // Default behavior: set field value trực tiếp không quan tâm trigger listener
440
+ const listener: ListenerItem | null = getListeners().find(
441
+ (l) => l.name === name && l.formName === formName,
442
+ );
443
+
444
+ if (listener) {
445
+ // Nếu loại listener là array thì gọi onArrayChange
446
+ if (listener.type === "array") {
447
+ // Do nothing if the value is the same for array item to prevent unnecessary re-renders
448
+ if (!isEqual(getFormItemValue(formName, name), value)) {
449
+ listener.onArrayChange(value, options);
450
+
451
+ // Kiểm tra từng path con của array item xem có listener nào không, nếu có thì gọi onChange của listener đó
452
+ const allStringPath = getAllNoneObjStringPath(value);
453
+ allStringPath.forEach((p) => {
454
+ const findListener = getListeners().find(
455
+ (l) => l.name === `${name}.${p}` && l.formName === formName,
456
+ );
457
+ if (findListener) {
458
+ findListener.onChange(get(value, p), options);
459
+ } else {
460
+ setData(formName, `${name}.${p}`, get(value, p));
461
+ }
462
+ });
463
+ }
464
+ } else {
465
+ listener.onChange(value, options);
466
+ }
467
+ } else {
468
+ // set data for non-listener field
469
+ setData(formName, name, value);
470
+ }
471
+ };
472
+
103
473
  const setFieldValues = (values, options = { notTriggerDirty: false }) => {
104
- const allStringPath = getAllNoneObjStringPath(values);
474
+ // Lấy tất cả paths, dừng khi gặp array
475
+ const allPaths = getAllPathsStopAtArray(values);
476
+
477
+ allPaths.forEach((path) => {
478
+ const pathValue = get(values, path);
479
+
480
+ // Nếu value là array => gọi deepTrigger
481
+ if (Array.isArray(pathValue)) {
482
+ handleDeepTriggerSet(path, pathValue, options);
483
+ return;
484
+ }
105
485
 
106
- allStringPath.forEach((p) => {
486
+ // Điều kiện dừng bình thường: primitive hoặc class instance
107
487
  const listener = getListeners().find(
108
- (l) => l.name === p && l.formName === formName,
488
+ (l) => l.name === path && l.formName === formName,
109
489
  );
490
+
110
491
  if (listener) {
111
- listener.onChange(get(values, listener.name), options);
492
+ listener.onChange(pathValue, options);
112
493
  } else {
113
- setData(formName, p, get(values, p));
494
+ setData(formName, path, pathValue);
114
495
  }
115
496
  });
116
497
  };
117
498
 
118
- const getFieldValue = (name) => {
499
+ function getFieldValue(name) {
119
500
  return getFormItemValue(formName, name);
120
- };
501
+ }
121
502
 
122
503
  const getFieldValues = (names = []) => {
123
504
  return names.map((name) => ({
@@ -165,17 +546,37 @@ export default function Form<T = any>({
165
546
  state: _,
166
547
  reset,
167
548
  } = useTask({
168
- async task(props) {
549
+ async task(props?: SubmitProps<T>) {
169
550
  try {
170
551
  flushSync(setFormState({ formName, submitState: "submitting" }));
171
552
  const errorFields = getAllFieldErrors();
172
- const listeners = getListeners().filter((l) => l.formName === formName);
553
+ const listeners: ListenerItem[] = getListeners().filter(
554
+ (l) => l.formName === formName,
555
+ );
173
556
  const formValues = getFormValues(formName);
174
557
 
175
558
  const resultValues = cloneDeep(formValues);
176
559
  const cleanValues = {} as T;
177
- uniq(listeners, (l) => l.name).forEach((l) => {
178
- set(cleanValues, l.name, get(resultValues, l.name));
560
+ uniqBy(
561
+ filter(listeners, (l: ListenerItem) => {
562
+ // console.log("Check collect field hidden: ", {
563
+ // name: l.name,
564
+ // hidden: l.hidden,
565
+ // collectOnHidden: l.collectOnHidden,
566
+ // collectHiddenFields,
567
+ // });
568
+ if (!l.hidden) return true;
569
+ if (isNil(props?.collectHiddenFields)) {
570
+ if (isNil(l.collectOnHidden)) {
571
+ return formCollectHiddenFields;
572
+ }
573
+ return Boolean(l.collectOnHidden);
574
+ }
575
+ return props.collectHiddenFields;
576
+ }),
577
+ (l) => l.name,
578
+ ).forEach((l) => {
579
+ set(cleanValues as any, l.name, get(resultValues, l.name));
179
580
  });
180
581
 
181
582
  const handleIsolateCase = async () => {
@@ -397,7 +798,6 @@ export default function Form<T = any>({
397
798
  {children}
398
799
  </form>
399
800
  )}
400
- <FormCleanUp />
401
801
  </FormContext.Provider>
402
802
  );
403
803
  }
@@ -413,9 +813,11 @@ export function useFormContext() {
413
813
  export function useForm<T = any>(
414
814
  formNameOrFormInstance?: string | PublicFormInstance<T>,
415
815
  ) {
416
- const targetFormName =
417
- typeof formNameOrFormInstance === "object" &&
418
- formNameOrFormInstance !== null
816
+ const formContext = useContext(FormContext);
817
+ const targetFormName = isNil(formNameOrFormInstance)
818
+ ? formContext?.formName
819
+ : typeof formNameOrFormInstance === "object" &&
820
+ formNameOrFormInstance !== null
419
821
  ? (formNameOrFormInstance as PublicFormInstance<T>).formName
420
822
  : (formNameOrFormInstance as string | undefined);
421
823
 
@@ -442,6 +844,30 @@ export function useWatch<T = any>(
442
844
  return value as T[keyof T] | undefined;
443
845
  }
444
846
 
847
+ export function useWatchNormalized<T, TFn extends (v: any) => any>({
848
+ name,
849
+ normalizeFn,
850
+ formNameOrFormInstance,
851
+ }: {
852
+ name: keyof T & string;
853
+ normalizeFn: TFn;
854
+ formNameOrFormInstance?: string | PublicFormInstance<T>;
855
+ }) {
856
+ const [formInstance] = useForm<T>(formNameOrFormInstance as any);
857
+ const rawValue = useFormStore((state) => {
858
+ return state.getFormItemValue(
859
+ formInstance?.formName ?? (formNameOrFormInstance as any),
860
+ name as string,
861
+ );
862
+ });
863
+
864
+ const normalizedValue = useMemo(() => {
865
+ return normalizeFn(rawValue);
866
+ }, [rawValue, normalizeFn]);
867
+
868
+ return normalizedValue as ReturnType<TFn>;
869
+ }
870
+
445
871
  export function useSubmitDataWatch<T = any>({
446
872
  formNameOrFormInstance,
447
873
  triggerWhenChange = false,
@@ -508,7 +934,9 @@ export const useFormItemStateWatch = <T = any,>(
508
934
  };
509
935
 
510
936
  Form.useForm = useForm;
937
+ Form.Item = FormItem;
511
938
  Form.useWatch = useWatch;
939
+ Form.useWatchNormalized = useWatchNormalized;
512
940
  Form.useSubmitDataWatch = useSubmitDataWatch;
513
941
  Form.useFormStateWatch = useFormStateWatch;
514
942
  Form.useFormItemStateWatch = useFormItemStateWatch;