react-form-manage 1.0.8-beta.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +173 -4
- package/README.md +8 -4
- package/dist/components/Form/FormCleanUp.js +3 -3
- package/dist/components/Form/FormItem.d.ts +10 -4
- package/dist/components/Form/FormItem.js +52 -14
- package/dist/components/Form/FormList.d.ts +2 -2
- package/dist/components/Form/FormList.js +2 -2
- package/dist/constants/form.d.ts +1 -1
- package/dist/hooks/useFormItemControl.d.ts +8 -3
- package/dist/hooks/useFormItemControl.js +64 -28
- package/dist/hooks/useFormListControl.d.ts +2 -1
- package/dist/hooks/useFormListControl.js +85 -19
- package/dist/index.cjs.d.ts +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.esm.d.ts +1 -0
- package/dist/index.js +4 -2
- package/dist/providers/Form.d.ts +15 -2
- package/dist/providers/Form.js +226 -41
- package/dist/stores/formStore.d.ts +44 -4
- package/dist/stores/formStore.js +42 -7
- package/dist/test/CommonTest.d.ts +3 -0
- package/dist/test/CommonTest.js +49 -0
- package/dist/test/TestDialog.d.ts +3 -0
- package/dist/test/TestDialog.js +21 -0
- package/dist/test/TestListener.d.ts +3 -0
- package/dist/test/TestListener.js +17 -0
- package/dist/test/TestNotFormWrapper.d.ts +3 -0
- package/dist/test/TestNotFormWrapper.js +15 -0
- package/dist/test/TestSelect.d.ts +6 -0
- package/dist/test/TestSelect.js +24 -0
- package/dist/test/TestWatchNormalize.d.ts +3 -0
- package/dist/test/TestWatchNormalize.js +23 -0
- package/dist/test/TestWrapperFormItem.d.ts +3 -0
- package/dist/test/TestWrapperFormItem.js +13 -0
- package/dist/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.d.ts +21 -0
- package/dist/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.js +61 -0
- package/dist/test/testSetValue/TestCase1_PlainObjectToPrimitives.d.ts +16 -0
- package/dist/test/testSetValue/TestCase1_PlainObjectToPrimitives.js +18 -0
- package/dist/test/testSetValue/TestCase2_PlainObjectToFormList.d.ts +21 -0
- package/dist/test/testSetValue/TestCase2_PlainObjectToFormList.js +33 -0
- package/dist/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.d.ts +21 -0
- package/dist/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.js +26 -0
- package/dist/test/testSetValue/TestCase4_PlainObjectRemovedFields.d.ts +20 -0
- package/dist/test/testSetValue/TestCase4_PlainObjectRemovedFields.js +32 -0
- package/dist/test/testSetValue/TestCase5_FormListRemovedItems.d.ts +22 -0
- package/dist/test/testSetValue/TestCase5_FormListRemovedItems.js +29 -0
- package/dist/test/testSetValue/TestCase6_NestedFormListRemoved.d.ts +28 -0
- package/dist/test/testSetValue/TestCase6_NestedFormListRemoved.js +36 -0
- package/dist/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.d.ts +17 -0
- package/dist/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.js +33 -0
- package/dist/test/testSetValue/TestCase8_SetFieldValues_NestedObject.d.ts +27 -0
- package/dist/test/testSetValue/TestCase8_SetFieldValues_NestedObject.js +57 -0
- package/dist/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.d.ts +25 -0
- package/dist/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.js +46 -0
- package/dist/test/testSetValue/index.d.ts +2 -0
- package/dist/test/testSetValue/index.js +28 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/public.d.ts +1 -1
- package/dist/utils/obj.util.d.ts +29 -1
- package/dist/utils/obj.util.js +59 -5
- package/package.json +2 -1
- package/src/App.tsx +39 -156
- package/src/DEEP_TRIGGER_LOGIC.md +573 -0
- package/src/components/Form/FormCleanUp.tsx +4 -8
- package/src/components/Form/FormItem.tsx +174 -57
- package/src/components/Form/FormList.tsx +17 -4
- package/src/constants/form.ts +1 -1
- package/src/hooks/useFormItemControl.ts +78 -32
- package/src/hooks/useFormListControl.ts +133 -43
- package/src/index.ts +25 -13
- package/src/main.tsx +6 -1
- package/src/providers/Form.tsx +454 -26
- package/src/stores/formStore.ts +363 -283
- package/src/test/CommonTest.tsx +177 -0
- package/src/test/TestDialog.tsx +52 -0
- package/src/test/TestListener.tsx +21 -0
- package/src/test/TestNotFormWrapper.tsx +43 -0
- package/src/test/TestSelect.tsx +38 -0
- package/src/test/TestWatchNormalize.tsx +32 -0
- package/src/test/TestWrapperFormItem.tsx +34 -0
- package/src/test/testSetValue/TestCase10_SetFieldValues_ComplexNested.tsx +203 -0
- package/src/test/testSetValue/TestCase1_PlainObjectToPrimitives.tsx +72 -0
- package/src/test/testSetValue/TestCase2_PlainObjectToFormList.tsx +114 -0
- package/src/test/testSetValue/TestCase3_ArrayNonListenerToPrimitives.tsx +99 -0
- package/src/test/testSetValue/TestCase4_PlainObjectRemovedFields.tsx +112 -0
- package/src/test/testSetValue/TestCase5_FormListRemovedItems.tsx +119 -0
- package/src/test/testSetValue/TestCase6_NestedFormListRemoved.tsx +185 -0
- package/src/test/testSetValue/TestCase7_SetFieldValues_MixedStructure.tsx +110 -0
- package/src/test/testSetValue/TestCase8_SetFieldValues_NestedObject.tsx +162 -0
- package/src/test/testSetValue/TestCase9_SetFieldValues_MultipleArrays.tsx +169 -0
- package/src/test/testSetValue/index.tsx +100 -0
- package/src/types/index.ts +1 -1
- package/src/types/public.ts +1 -1
- package/src/utils/obj.util.ts +153 -13
package/src/providers/Form.tsx
CHANGED
|
@@ -1,30 +1,51 @@
|
|
|
1
|
-
import {
|
|
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
|
|
18
|
+
import FormItem from "../components/Form/FormItem";
|
|
8
19
|
import { SUBMIT_STATE } from "../constants/form";
|
|
9
|
-
import {
|
|
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 {
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
488
|
+
(l) => l.name === path && l.formName === formName,
|
|
109
489
|
);
|
|
490
|
+
|
|
110
491
|
if (listener) {
|
|
111
|
-
listener.onChange(
|
|
492
|
+
listener.onChange(pathValue, options);
|
|
112
493
|
} else {
|
|
113
|
-
setData(formName,
|
|
494
|
+
setData(formName, path, pathValue);
|
|
114
495
|
}
|
|
115
496
|
});
|
|
116
497
|
};
|
|
117
498
|
|
|
118
|
-
|
|
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(
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
417
|
-
|
|
418
|
-
|
|
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;
|