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 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
@@ -157,6 +157,7 @@ Custom validator (async):
157
157
  `FormList` là render-prop component cung cấp `listFields` và các action `add`, `remove`, `move`.
158
158
 
159
159
  Ghi chú:
160
+
160
161
  - `add()` nếu không truyền `index` sẽ tự động append phần tử mới vào cuối danh sách.
161
162
 
162
163
  Ví dụ:
@@ -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 FormInstance, type ListenerItem, type CleanUpItem } from "./stores/formStore";
10
- export { Form, FormItem, FormList, Input, InputWrapper, useFormItemControl, useFormListControl, useForm, useWatch, useSubmitDataWatch, useFormStateWatch, useFormStore, type FormProps, type FormItemProps, type FormListProps, type InputWrapperProps, type ValidationRule, type FormFieldError, type SubmitState, type UseFormItemStateWatchReturn, type FormInstance, type ListenerItem, type CleanUpItem, SUBMIT_STATE, };
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";
@@ -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 (typeof value === "object" && !isNil(value)) {
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
- if (typeof get(values, p) === "object" && !isNil(get(values, p))) {
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
  };
@@ -1 +1 @@
1
- export * from './public';
1
+ export * from "./public";
@@ -1,5 +1,5 @@
1
- import type { GetConstantType } from "./util";
2
1
  import type { SUBMIT_STATE } from "../constants/form";
2
+ import type { GetConstantType } from "./util";
3
3
  export type FormValues<T = any> = T;
4
4
  export interface FormFieldError {
5
5
  ruleName: string | number;
@@ -1,2 +1,2 @@
1
- export declare function getAllNoneObjStringPath(value: any, prevPath?: string): any;
1
+ export declare function getAllNoneObjStringPath(value: any, prevPath?: string): string[];
2
2
  export declare function getAllStringPath(value: any, prevPath?: string): any;
@@ -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 (typeof value === "object") {
4
- return Object.keys(value).reduce((prev, cur) => {
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(value[cur], join(filter([prevPath, cur], (v) => !isNil(v) && v !== ""), "."))
10
+ ...getAllNoneObjStringPath(item, join(filter([prevPath, String(index)], (v) => !isNil(v) && v !== ""), "."))
8
11
  ];
9
12
  }, []);
10
13
  }
11
- return [prevPath];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-manage",
3
- "version": "1.0.8-beta.17",
3
+ "version": "1.0.8-beta.18",
4
4
  "description": "Lightweight React form management with list and listener support.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
- type FormProps,
7
- type ValidationRule,
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
- type FormProps,
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 ValidationRule,
46
- type FormFieldError,
51
+ type ListenerItem,
47
52
  type SubmitState,
48
53
  type UseFormItemStateWatchReturn,
49
- type FormInstance,
50
- type ListenerItem,
51
- type CleanUpItem,
52
- SUBMIT_STATE,
54
+ type ValidationRule,
53
55
  };
54
56
 
55
57
  export default Form;
@@ -1,4 +1,13 @@
1
- import { cloneDeep, get, isEqual, isNil, last, set, uniqBy } from "lodash";
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 (typeof value === "object" && !isNil(value)) {
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(get(values, p));
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
- // Kiểm tra nếu là object và có listener con thì gọi onChange của listener con
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
  };
@@ -1 +1 @@
1
- export * from './public';
1
+ export * from "./public";
@@ -1,5 +1,5 @@
1
- import type { GetConstantType } from "./util";
2
1
  import type { SUBMIT_STATE } from "../constants/form";
2
+ import type { GetConstantType } from "./util";
3
3
 
4
4
  export type FormValues<T = any> = T;
5
5
 
@@ -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(value: any, prevPath: string = "") {
4
- if (typeof value === "object") {
5
- return Object.keys(value).reduce((prev, cur) => {
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
- value[cur],
23
+ item,
11
24
  join(
12
- filter([prevPath, cur], (v) => !isNil(v) && v !== ""),
13
- "."
14
- )
25
+ filter([prevPath, String(index)], (v) => !isNil(v) && v !== ""),
26
+ ".",
27
+ ),
15
28
  ),
16
29
  ];
17
30
  }, []);
18
31
  }
19
- return [prevPath];
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
  }, []);