react-form-manage 1.0.8-beta.17 → 1.0.8-beta.18
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 +48 -4
- package/README.md +1 -0
- package/dist/index.cjs.d.ts +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.esm.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/providers/Form.js +3 -16
- package/dist/types/index.d.ts +1 -1
- package/dist/types/public.d.ts +1 -1
- package/dist/utils/obj.util.d.ts +1 -1
- package/dist/utils/obj.util.js +16 -5
- package/package.json +1 -1
- package/src/index.ts +19 -17
- package/src/providers/Form.tsx +15 -19
- package/src/types/index.ts +1 -1
- package/src/types/public.ts +1 -1
- package/src/utils/obj.util.ts +44 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,20 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.0.8-beta.17] - 2026-02-01
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Add nested path onChange trigger: `setFieldValue` now triggers onChange for child paths when parent path has no listener
|
|
10
|
+
- Support setting array values in FormList with automatic re-render of list items and value updates
|
|
11
|
+
- Enhanced listener types with `type` field (normal/array) and `onArrayChange` callback support
|
|
12
|
+
|
|
13
|
+
### Improvements
|
|
14
|
+
|
|
15
|
+
- `useFormListControl` now tracks item values and handles cache properly
|
|
16
|
+
- Improved `useFormItemControl` with cache path validation using `lodash.has`
|
|
17
|
+
|
|
18
|
+
## [1.0.8-beta.16] - 2026-02-01
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
|
|
22
|
+
- Fix: Guard listener registration to prevent pushing incomplete listeners without name/formName
|
|
23
|
+
- Fix: Guard cleanup execution - only cleanup listeners that actually exist in the store
|
|
24
|
+
- Add formItemId to dependency array for proper listener re-registration in strict mode
|
|
25
|
+
|
|
26
|
+
## [1.0.8-beta.15] - 2026-02-01
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
- Export `useFormStore` and types (`FormInstance`, `ListenerItem`, `CleanUpItem`) to public API
|
|
31
|
+
- Allow advanced users to access and manipulate form state directly
|
|
32
|
+
|
|
33
|
+
## [1.0.8-beta.13] - 2026-02-01
|
|
34
|
+
|
|
35
|
+
### Refactoring
|
|
36
|
+
|
|
37
|
+
- Combine separate Zustand stores (`useFormListeners`, `useFormCleanUp`) into single unified `useFormStore`
|
|
38
|
+
- Implement slice-based store pattern for better code organization and maintainability
|
|
39
|
+
- Update all internal imports to use unified store across hooks and components
|
|
40
|
+
- Maintain backward compatibility with deprecated store aliases
|
|
41
|
+
|
|
42
|
+
## [1.0.8-beta.12] - 2026-01-27
|
|
43
|
+
|
|
44
|
+
### Features
|
|
45
|
+
|
|
46
|
+
- Add slice-based store architecture using Zustand for better scalability
|
|
47
|
+
- Refactor form state management with proper separation of concerns
|
|
48
|
+
|
|
5
49
|
## [1.0.8-beta.3] - 2026-01-22
|
|
6
50
|
|
|
7
51
|
## [1.0.8-beta.7] - 2026-01-24
|
|
52
|
+
|
|
8
53
|
- Add control flag for FormItem to support rendering MUI uncontrolled components when initial value is undefined (control on init).
|
|
9
54
|
- Fix: `onReset` did not restore listener state to init.
|
|
10
55
|
- Fix: reset did not return Form submit state to `idle`.
|
|
11
56
|
- Add `hidden` prop to allow hiding components while still assigning a value.
|
|
12
57
|
|
|
13
58
|
Docs: Update usage notes for `FormItem` control-on-init flag and `hidden` prop.
|
|
59
|
+
|
|
14
60
|
- Add `UseFormItemStateWatchReturn` type export for `useFormItemStateWatch` hook
|
|
15
61
|
|
|
16
62
|
- Add isTouched field to FormItem for tracking user interaction state
|
|
17
63
|
|
|
18
64
|
## [1.0.8-beta.10] - 2026-01-27
|
|
65
|
+
|
|
19
66
|
- `useForm()` giờ có thể trả về form instance từ provider lân cận nếu không truyền `formName` (không còn bắt buộc truyền tên form khi đã wrap trong `Form`).
|
|
20
67
|
- `FormList.add` khi không truyền `index` sẽ mặc định append vào cuối danh sách.
|
|
21
68
|
- Docs: cập nhật hướng dẫn cho `useForm` và `FormList.add`.
|
|
@@ -23,6 +70,7 @@ Docs: Update usage notes for `FormItem` control-on-init flag and `hidden` prop.
|
|
|
23
70
|
## [1.0.8-beta.1] - 2026-01-22
|
|
24
71
|
|
|
25
72
|
- Fix UseFormItemControlReturn errors type to properly export FormFieldError[]
|
|
73
|
+
|
|
26
74
|
## [1.0.7-beta.1] - 2026-01-22
|
|
27
75
|
|
|
28
76
|
- Add `FormFieldError` type export for typed error handling
|
|
@@ -103,7 +151,3 @@ Docs: Update usage notes for `FormItem` control-on-init flag and `hidden` prop.
|
|
|
103
151
|
## [1.0.5] - 2026-01-21
|
|
104
152
|
|
|
105
153
|
- Export `Form` as a named export in addition to default to improve import ergonomics for consumers.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
package/README.md
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./index";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import Form, { useForm, useWatch, useSubmitDataWatch, useFormStateWatch, type FormProps, type ValidationRule, type FormFieldError, type SubmitState, type UseFormItemStateWatchReturn } from "./providers/Form";
|
|
2
1
|
import { SUBMIT_STATE } from "./constants/form";
|
|
2
|
+
import Form, { useForm, useFormStateWatch, useSubmitDataWatch, useWatch, type FormFieldError, type FormProps, type SubmitState, type UseFormItemStateWatchReturn, type ValidationRule } from "./providers/Form";
|
|
3
3
|
import FormItem, { type FormItemProps } from "./components/Form/FormItem";
|
|
4
4
|
import FormList, { type FormListProps } from "./components/Form/FormList";
|
|
5
|
-
import Input from "./components/Input";
|
|
6
5
|
import InputWrapper, { type InputWrapperProps } from "./components/Form/InputWrapper";
|
|
6
|
+
import Input from "./components/Input";
|
|
7
7
|
import useFormItemControl from "./hooks/useFormItemControl";
|
|
8
8
|
import useFormListControl from "./hooks/useFormListControl";
|
|
9
|
-
import { useFormStore, type
|
|
10
|
-
export { Form, FormItem, FormList, Input, InputWrapper,
|
|
9
|
+
import { useFormStore, type CleanUpItem, type FormInstance, type ListenerItem } from "./stores/formStore";
|
|
10
|
+
export { Form, FormItem, FormList, Input, InputWrapper, SUBMIT_STATE, useForm, useFormItemControl, useFormListControl, useFormStateWatch, useFormStore, useSubmitDataWatch, useWatch, type CleanUpItem, type FormFieldError, type FormInstance, type FormItemProps, type FormListProps, type FormProps, type InputWrapperProps, type ListenerItem, type SubmitState, type UseFormItemStateWatchReturn, type ValidationRule, };
|
|
11
11
|
export default Form;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./index";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import Form, { useForm, useWatch, useSubmitDataWatch, useFormStateWatch } from "./providers/Form";
|
|
2
1
|
import { SUBMIT_STATE } from "./constants/form";
|
|
2
|
+
import Form, { useForm, useFormStateWatch, useSubmitDataWatch, useWatch } from "./providers/Form";
|
|
3
3
|
import FormItem from "./components/Form/FormItem";
|
|
4
4
|
import FormList from "./components/Form/FormList";
|
|
5
|
-
import Input from "./components/Input";
|
|
6
5
|
import InputWrapper from "./components/Form/InputWrapper";
|
|
6
|
+
import Input from "./components/Input";
|
|
7
7
|
import useFormItemControl from "./hooks/useFormItemControl";
|
|
8
8
|
import useFormListControl from "./hooks/useFormListControl";
|
|
9
9
|
import { useFormStore } from "./stores/formStore";
|
package/dist/providers/Form.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { cloneDeep, get, isEqual, isNil, last, set, uniqBy } from "lodash";
|
|
2
|
+
import { cloneDeep, get, isEqual, isNil, isPlainObject, last, set, uniqBy } from "lodash";
|
|
3
3
|
import { useTask } from "minh-custom-hooks-release";
|
|
4
4
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
5
5
|
import { flushSync } from "react-dom";
|
|
@@ -64,7 +64,7 @@ function Form({ children, formName, initialValues, onFinish, onReject, onFinally
|
|
|
64
64
|
listener.onChange(value, options);
|
|
65
65
|
}
|
|
66
66
|
} else {
|
|
67
|
-
if (
|
|
67
|
+
if (isPlainObject(value) || Array.isArray(value)) {
|
|
68
68
|
const allStringPath = getAllNoneObjStringPath(value);
|
|
69
69
|
allStringPath.forEach((p) => {
|
|
70
70
|
const findListener = getListeners().find((l) => l.name === `${name}.${p}` && l.formName === formName);
|
|
@@ -103,20 +103,7 @@ function Form({ children, formName, initialValues, onFinish, onReject, onFinally
|
|
|
103
103
|
listener.onChange(get(values, listener.name), options);
|
|
104
104
|
}
|
|
105
105
|
} else {
|
|
106
|
-
|
|
107
|
-
const nestedAllStringPath = getAllNoneObjStringPath(get(values, p));
|
|
108
|
-
nestedAllStringPath.forEach((np) => {
|
|
109
|
-
{
|
|
110
|
-
const findListener = getListeners().find((l) => l.name === `${p}.${np}` && l.formName === formName);
|
|
111
|
-
if (findListener) {
|
|
112
|
-
findListener.onChange(get(values, `${p}.${np}`), options);
|
|
113
|
-
} else {
|
|
114
|
-
setData(formName, `${p}.${np}`, get(values, `${p}.${np}`));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
} else
|
|
119
|
-
setData(formName, p, get(values, p));
|
|
106
|
+
setData(formName, p, get(values, p));
|
|
120
107
|
}
|
|
121
108
|
});
|
|
122
109
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "./public";
|
package/dist/types/public.d.ts
CHANGED
package/dist/utils/obj.util.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function getAllNoneObjStringPath(value: any, prevPath?: string):
|
|
1
|
+
export declare function getAllNoneObjStringPath(value: any, prevPath?: string): string[];
|
|
2
2
|
export declare function getAllStringPath(value: any, prevPath?: string): any;
|
package/dist/utils/obj.util.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { filter, isNil, join } from "lodash";
|
|
1
|
+
import { filter, isNil, isPlainObject, join } from "lodash";
|
|
2
2
|
function getAllNoneObjStringPath(value, prevPath = "") {
|
|
3
|
-
if (
|
|
4
|
-
return
|
|
3
|
+
if (value === null || value === void 0 || typeof value !== "object" || typeof value === "function") {
|
|
4
|
+
return [prevPath];
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.reduce((prev, item, index) => {
|
|
5
8
|
return [
|
|
6
9
|
...prev,
|
|
7
|
-
...getAllNoneObjStringPath(
|
|
10
|
+
...getAllNoneObjStringPath(item, join(filter([prevPath, String(index)], (v) => !isNil(v) && v !== ""), "."))
|
|
8
11
|
];
|
|
9
12
|
}, []);
|
|
10
13
|
}
|
|
11
|
-
|
|
14
|
+
if (!isPlainObject(value)) {
|
|
15
|
+
return [prevPath];
|
|
16
|
+
}
|
|
17
|
+
return Object.keys(value).reduce((prev, cur) => {
|
|
18
|
+
return [
|
|
19
|
+
...prev,
|
|
20
|
+
...getAllNoneObjStringPath(value[cur], join(filter([prevPath, cur], (v) => !isNil(v) && v !== ""), "."))
|
|
21
|
+
];
|
|
22
|
+
}, []);
|
|
12
23
|
}
|
|
13
24
|
function getAllStringPath(value, prevPath = "") {
|
|
14
25
|
if (typeof value === "object") {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
|
+
import { SUBMIT_STATE } from "./constants/form";
|
|
1
2
|
import Form, {
|
|
2
3
|
useForm,
|
|
3
|
-
useWatch,
|
|
4
|
-
useSubmitDataWatch,
|
|
5
4
|
useFormStateWatch,
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
useSubmitDataWatch,
|
|
6
|
+
useWatch,
|
|
8
7
|
type FormFieldError,
|
|
8
|
+
type FormProps,
|
|
9
9
|
type SubmitState,
|
|
10
10
|
type UseFormItemStateWatchReturn,
|
|
11
|
+
type ValidationRule,
|
|
11
12
|
} from "./providers/Form";
|
|
12
|
-
import { SUBMIT_STATE } from "./constants/form";
|
|
13
13
|
|
|
14
14
|
import FormItem, { type FormItemProps } from "./components/Form/FormItem";
|
|
15
15
|
import FormList, { type FormListProps } from "./components/Form/FormList";
|
|
16
|
+
import InputWrapper, {
|
|
17
|
+
type InputWrapperProps,
|
|
18
|
+
} from "./components/Form/InputWrapper";
|
|
16
19
|
import Input from "./components/Input";
|
|
17
|
-
import InputWrapper, { type InputWrapperProps } from "./components/Form/InputWrapper";
|
|
18
20
|
|
|
19
21
|
import useFormItemControl from "./hooks/useFormItemControl";
|
|
20
22
|
import useFormListControl from "./hooks/useFormListControl";
|
|
21
23
|
import {
|
|
22
24
|
useFormStore,
|
|
25
|
+
type CleanUpItem,
|
|
23
26
|
type FormInstance,
|
|
24
27
|
type ListenerItem,
|
|
25
|
-
type CleanUpItem,
|
|
26
28
|
} from "./stores/formStore";
|
|
27
29
|
|
|
28
30
|
export {
|
|
@@ -31,25 +33,25 @@ export {
|
|
|
31
33
|
FormList,
|
|
32
34
|
Input,
|
|
33
35
|
InputWrapper,
|
|
36
|
+
SUBMIT_STATE,
|
|
37
|
+
useForm,
|
|
34
38
|
useFormItemControl,
|
|
35
39
|
useFormListControl,
|
|
36
|
-
useForm,
|
|
37
|
-
useWatch,
|
|
38
|
-
useSubmitDataWatch,
|
|
39
40
|
useFormStateWatch,
|
|
40
41
|
useFormStore,
|
|
41
|
-
|
|
42
|
+
useSubmitDataWatch,
|
|
43
|
+
useWatch,
|
|
44
|
+
type CleanUpItem,
|
|
45
|
+
type FormFieldError,
|
|
46
|
+
type FormInstance,
|
|
42
47
|
type FormItemProps,
|
|
43
48
|
type FormListProps,
|
|
49
|
+
type FormProps,
|
|
44
50
|
type InputWrapperProps,
|
|
45
|
-
type
|
|
46
|
-
type FormFieldError,
|
|
51
|
+
type ListenerItem,
|
|
47
52
|
type SubmitState,
|
|
48
53
|
type UseFormItemStateWatchReturn,
|
|
49
|
-
type
|
|
50
|
-
type ListenerItem,
|
|
51
|
-
type CleanUpItem,
|
|
52
|
-
SUBMIT_STATE,
|
|
54
|
+
type ValidationRule,
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
export default Form;
|
package/src/providers/Form.tsx
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
cloneDeep,
|
|
3
|
+
get,
|
|
4
|
+
isEqual,
|
|
5
|
+
isNil,
|
|
6
|
+
isPlainObject,
|
|
7
|
+
last,
|
|
8
|
+
set,
|
|
9
|
+
uniqBy,
|
|
10
|
+
} from "lodash";
|
|
2
11
|
import { useTask } from "minh-custom-hooks-release";
|
|
3
12
|
import type { ComponentType, FormHTMLAttributes, ReactNode } from "react";
|
|
4
13
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
@@ -118,7 +127,7 @@ export default function Form<T = any>({
|
|
|
118
127
|
}
|
|
119
128
|
} else {
|
|
120
129
|
// set data for non-listener field
|
|
121
|
-
if (
|
|
130
|
+
if (isPlainObject(value) || Array.isArray(value)) {
|
|
122
131
|
// Nếu là object hoặc array thì set từng path con
|
|
123
132
|
const allStringPath = getAllNoneObjStringPath(value);
|
|
124
133
|
|
|
@@ -152,7 +161,9 @@ export default function Form<T = any>({
|
|
|
152
161
|
listener.onArrayChange(get(values, listener.name), options);
|
|
153
162
|
|
|
154
163
|
// 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 đó
|
|
155
|
-
const nestedAllStringPath = getAllNoneObjStringPath(
|
|
164
|
+
const nestedAllStringPath: string[] = getAllNoneObjStringPath(
|
|
165
|
+
get(values, p),
|
|
166
|
+
);
|
|
156
167
|
nestedAllStringPath.forEach((np) => {
|
|
157
168
|
{
|
|
158
169
|
const findListener = getListeners().find(
|
|
@@ -170,22 +181,7 @@ export default function Form<T = any>({
|
|
|
170
181
|
listener.onChange(get(values, listener.name), options);
|
|
171
182
|
}
|
|
172
183
|
} else {
|
|
173
|
-
|
|
174
|
-
if (typeof get(values, p) === "object" && !isNil(get(values, p))) {
|
|
175
|
-
const nestedAllStringPath = getAllNoneObjStringPath(get(values, p));
|
|
176
|
-
nestedAllStringPath.forEach((np) => {
|
|
177
|
-
{
|
|
178
|
-
const findListener = getListeners().find(
|
|
179
|
-
(l) => l.name === `${p}.${np}` && l.formName === formName,
|
|
180
|
-
);
|
|
181
|
-
if (findListener) {
|
|
182
|
-
findListener.onChange(get(values, `${p}.${np}`), options);
|
|
183
|
-
} else {
|
|
184
|
-
setData(formName, `${p}.${np}`, get(values, `${p}.${np}`));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
} else setData(formName, p, get(values, p));
|
|
184
|
+
setData(formName, p, get(values, p));
|
|
189
185
|
}
|
|
190
186
|
});
|
|
191
187
|
};
|
package/src/types/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "./public";
|
package/src/types/public.ts
CHANGED
package/src/utils/obj.util.ts
CHANGED
|
@@ -1,22 +1,53 @@
|
|
|
1
|
-
import { filter, isNil, join } from "lodash";
|
|
1
|
+
import { filter, isNil, isPlainObject, join } from "lodash";
|
|
2
2
|
|
|
3
|
-
export function getAllNoneObjStringPath(
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export function getAllNoneObjStringPath(
|
|
4
|
+
value: any,
|
|
5
|
+
prevPath: string = "",
|
|
6
|
+
): string[] {
|
|
7
|
+
// primitive / function / null / undefined => dừng
|
|
8
|
+
if (
|
|
9
|
+
value === null ||
|
|
10
|
+
value === undefined ||
|
|
11
|
+
typeof value !== "object" ||
|
|
12
|
+
typeof value === "function"
|
|
13
|
+
) {
|
|
14
|
+
return [prevPath];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// array thì đi sâu
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return value.reduce((prev: string[], item, index) => {
|
|
6
20
|
return [
|
|
7
21
|
...prev,
|
|
8
|
-
|
|
9
22
|
...getAllNoneObjStringPath(
|
|
10
|
-
|
|
23
|
+
item,
|
|
11
24
|
join(
|
|
12
|
-
filter([prevPath,
|
|
13
|
-
"."
|
|
14
|
-
)
|
|
25
|
+
filter([prevPath, String(index)], (v) => !isNil(v) && v !== ""),
|
|
26
|
+
".",
|
|
27
|
+
),
|
|
15
28
|
),
|
|
16
29
|
];
|
|
17
30
|
}, []);
|
|
18
31
|
}
|
|
19
|
-
|
|
32
|
+
|
|
33
|
+
// class instance (non-plain object) => dừng
|
|
34
|
+
if (!isPlainObject(value)) {
|
|
35
|
+
return [prevPath];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// plain object => đi sâu
|
|
39
|
+
return Object.keys(value).reduce((prev: string[], cur) => {
|
|
40
|
+
return [
|
|
41
|
+
...prev,
|
|
42
|
+
...getAllNoneObjStringPath(
|
|
43
|
+
value[cur],
|
|
44
|
+
join(
|
|
45
|
+
filter([prevPath, cur], (v) => !isNil(v) && v !== ""),
|
|
46
|
+
".",
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
];
|
|
50
|
+
}, []);
|
|
20
51
|
}
|
|
21
52
|
|
|
22
53
|
export function getAllStringPath(value: any, prevPath: string = "") {
|
|
@@ -26,14 +57,14 @@ export function getAllStringPath(value: any, prevPath: string = "") {
|
|
|
26
57
|
...prev,
|
|
27
58
|
join(
|
|
28
59
|
filter([prevPath, cur], (v) => !isNil(v) && v !== ""),
|
|
29
|
-
"."
|
|
60
|
+
".",
|
|
30
61
|
),
|
|
31
62
|
...getAllStringPath(
|
|
32
63
|
value[cur],
|
|
33
64
|
join(
|
|
34
65
|
filter([prevPath, cur], (v) => !isNil(v) && v !== ""),
|
|
35
|
-
"."
|
|
36
|
-
)
|
|
66
|
+
".",
|
|
67
|
+
),
|
|
37
68
|
),
|
|
38
69
|
];
|
|
39
70
|
}, []);
|