react-form-manage 1.0.8-beta.1 → 1.0.8-beta.11
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 +19 -0
- package/README.md +7 -4
- package/dist/components/Form/FormItem.d.ts +4 -2
- package/dist/components/Form/FormItem.js +9 -6
- package/dist/components/Form/FormList.d.ts +1 -1
- package/dist/components/Form/FormList.js +2 -2
- package/dist/hooks/useFormItemControl.d.ts +2 -0
- package/dist/hooks/useFormItemControl.js +20 -4
- package/dist/hooks/useFormListControl.d.ts +1 -1
- package/dist/hooks/useFormListControl.js +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/providers/Form.d.ts +4 -2
- package/dist/providers/Form.js +49 -23
- package/dist/stores/formStore.js +8 -4
- package/dist/types/public.d.ts +5 -0
- package/package.json +3 -1
- package/src/App.tsx +82 -2
- package/src/components/Form/FormItem.tsx +74 -39
- package/src/components/Form/FormList.tsx +17 -4
- package/src/components/Form/InputWrapper.tsx +5 -0
- package/src/hooks/useFormItemControl.ts +28 -3
- package/src/hooks/useFormListControl.ts +19 -10
- package/src/index.ts +2 -0
- package/src/providers/Form.tsx +49 -11
- package/src/stores/formStore.ts +8 -2
- package/src/types/public.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.0.8-beta.3] - 2026-01-22
|
|
6
|
+
|
|
7
|
+
## [1.0.8-beta.7] - 2026-01-24
|
|
8
|
+
- Add control flag for FormItem to support rendering MUI uncontrolled components when initial value is undefined (control on init).
|
|
9
|
+
- Fix: `onReset` did not restore listener state to init.
|
|
10
|
+
- Fix: reset did not return Form submit state to `idle`.
|
|
11
|
+
- Add `hidden` prop to allow hiding components while still assigning a value.
|
|
12
|
+
|
|
13
|
+
Docs: Update usage notes for `FormItem` control-on-init flag and `hidden` prop.
|
|
14
|
+
- Add `UseFormItemStateWatchReturn` type export for `useFormItemStateWatch` hook
|
|
15
|
+
|
|
16
|
+
- Add isTouched field to FormItem for tracking user interaction state
|
|
17
|
+
|
|
18
|
+
## [1.0.8-beta.10] - 2026-01-27
|
|
19
|
+
- `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
|
+
- `FormList.add` khi không truyền `index` sẽ mặc định append vào cuối danh sách.
|
|
21
|
+
- Docs: cập nhật hướng dẫn cho `useForm` và `FormList.add`.
|
|
22
|
+
|
|
4
23
|
## [1.0.8-beta.1] - 2026-01-22
|
|
5
24
|
|
|
6
25
|
- Fix UseFormItemControlReturn errors type to properly export FormFieldError[]
|
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Thư viện form nhẹ dành cho React — hỗ trợ form, field, list (array fields), và cơ chế listener để giảm re-render.
|
|
4
4
|
|
|
5
|
-
Phiên bản này xuất phát từ mã nguồn JS. Typings (.d.ts) được cung cấp trong `src/types/components.d.ts`.
|
|
6
|
-
|
|
7
5
|
---
|
|
8
6
|
|
|
9
7
|
## Cài đặt
|
|
@@ -80,6 +78,8 @@ export default Example;
|
|
|
80
78
|
|
|
81
79
|
Bạn có thể lấy instance form để gọi phương thức chương trình như `submit`, `resetFields`, `setFieldValue`…
|
|
82
80
|
|
|
81
|
+
`useForm` ưu tiên lấy instance từ provider gần nhất nếu không truyền `formName`, giúp bạn không cần truyền tên form khi đã bọc trong `Form`.
|
|
82
|
+
|
|
83
83
|
Ví dụ sử dụng hook attached trên `Form`:
|
|
84
84
|
|
|
85
85
|
```jsx
|
|
@@ -87,7 +87,7 @@ import React from "react";
|
|
|
87
87
|
import Form from "react-form-manage";
|
|
88
88
|
|
|
89
89
|
function CtrlExample() {
|
|
90
|
-
const [form] = Form.useForm(
|
|
90
|
+
const [form] = Form.useForm(); // hoặc Form.useForm('myForm') khi muốn chỉ định form khác
|
|
91
91
|
|
|
92
92
|
return (
|
|
93
93
|
<div>
|
|
@@ -156,6 +156,9 @@ Custom validator (async):
|
|
|
156
156
|
|
|
157
157
|
`FormList` là render-prop component cung cấp `listFields` và các action `add`, `remove`, `move`.
|
|
158
158
|
|
|
159
|
+
Ghi chú:
|
|
160
|
+
- `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
|
+
|
|
159
162
|
Ví dụ:
|
|
160
163
|
|
|
161
164
|
```jsx
|
|
@@ -192,7 +195,7 @@ Lưu ý: `FormList` quản lý các `key` nội bộ và push các mục cần c
|
|
|
192
195
|
|
|
193
196
|
## Hooks (tóm tắt)
|
|
194
197
|
|
|
195
|
-
- `useForm(formNameOrFormInstance)` —
|
|
198
|
+
- `useForm(formNameOrFormInstance?)` — nếu không truyền thì lấy instance từ `Form` provider gần nhất; nếu truyền `formName` thì chọn form tương ứng.
|
|
196
199
|
- `useWatch(name, formNameOrFormInstance)` — subscribe giá trị field.
|
|
197
200
|
- `useSubmitDataWatch` — watch dữ liệu submit cuối cùng.
|
|
198
201
|
- `useFormStateWatch` — watch state của form (isInitied, submitState…).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
2
|
import type { ValidationRule } from "../../types/public";
|
|
3
3
|
export interface FormItemProps {
|
|
4
|
-
children: ReactElement
|
|
4
|
+
children: ReactElement<any>;
|
|
5
5
|
name: string;
|
|
6
6
|
formName?: string;
|
|
7
7
|
initialValue?: any;
|
|
@@ -9,5 +9,7 @@ export interface FormItemProps {
|
|
|
9
9
|
rules?: ValidationRule[];
|
|
10
10
|
valuePropName?: string;
|
|
11
11
|
getValueFromEvent?: (...args: any[]) => any;
|
|
12
|
+
controlAfterInit?: boolean;
|
|
13
|
+
hidden?: boolean;
|
|
12
14
|
}
|
|
13
|
-
export default function FormItem({ children, name, formName, initialValue, formItemId: externalFormItemId, rules, valuePropName, getValueFromEvent, }: FormItemProps):
|
|
15
|
+
export default function FormItem({ children, name, formName, initialValue, formItemId: externalFormItemId, rules, valuePropName, getValueFromEvent, controlAfterInit, hidden, }: FormItemProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement, Fragment, useRef, useState } from "react";
|
|
2
3
|
import { v4 } from "uuid";
|
|
3
4
|
import useFormItemControl from "../../hooks/useFormItemControl";
|
|
4
|
-
function FormItem({ children, name, formName, initialValue, formItemId: externalFormItemId, rules, valuePropName = "value", getValueFromEvent }) {
|
|
5
|
+
function FormItem({ children, name, formName, initialValue, formItemId: externalFormItemId, rules, valuePropName = "value", getValueFromEvent, controlAfterInit = false, hidden }) {
|
|
5
6
|
const elRef = useRef(null);
|
|
6
7
|
const [formItemId] = useState(externalFormItemId != null ? externalFormItemId : v4());
|
|
7
|
-
const { value, onChange, errors, state, onFocus, isDirty, submitState } = useFormItemControl({
|
|
8
|
+
const { value, onChange, errors, state, onFocus, isDirty, submitState, isTouched, isInitied } = useFormItemControl({
|
|
8
9
|
formName,
|
|
9
10
|
name,
|
|
10
11
|
initialValue,
|
|
@@ -12,7 +13,8 @@ function FormItem({ children, name, formName, initialValue, formItemId: external
|
|
|
12
13
|
rules,
|
|
13
14
|
elementRef: elRef
|
|
14
15
|
});
|
|
15
|
-
return cloneElement(children, {
|
|
16
|
+
return _jsx(Fragment, { children: !hidden && children ? cloneElement(children, {
|
|
17
|
+
name,
|
|
16
18
|
// ref: inputRef,
|
|
17
19
|
[valuePropName]: value,
|
|
18
20
|
onChange: (...args) => {
|
|
@@ -38,8 +40,9 @@ function FormItem({ children, name, formName, initialValue, formItemId: external
|
|
|
38
40
|
onFocus,
|
|
39
41
|
validateState: state,
|
|
40
42
|
ref: elRef,
|
|
41
|
-
submitState
|
|
42
|
-
|
|
43
|
+
submitState,
|
|
44
|
+
isTouched
|
|
45
|
+
}) : null }, `control-after-init-${Boolean(controlAfterInit && isInitied) ? "1" : "0"}-${formItemId}`);
|
|
43
46
|
}
|
|
44
47
|
export {
|
|
45
48
|
FormItem as default
|
|
@@ -20,5 +20,5 @@ export interface FormListProps<T = any> {
|
|
|
20
20
|
}) => void;
|
|
21
21
|
}) => React.ReactNode;
|
|
22
22
|
}
|
|
23
|
-
declare const FormList: <T = any>({ name, initialValues, form, formName, children }: FormListProps<T>) => import("react").ReactNode;
|
|
23
|
+
declare const FormList: <T = any>({ name, initialValues, form, formName, children, }: FormListProps<T>) => import("react").ReactNode;
|
|
24
24
|
export default FormList;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import useFormListControl from "../../hooks/useFormListControl";
|
|
2
2
|
const FormList = ({ name, initialValues, form, formName, children }) => {
|
|
3
|
-
const { listFields, ...actions } =
|
|
3
|
+
const { listFields, ...actions } = useFormListControl({
|
|
4
4
|
name,
|
|
5
5
|
initialValues,
|
|
6
6
|
form,
|
|
@@ -18,7 +18,9 @@ export interface UseFormItemControlReturn {
|
|
|
18
18
|
errors: FormFieldError[];
|
|
19
19
|
onFocus: () => void;
|
|
20
20
|
isDirty?: boolean;
|
|
21
|
+
isTouched?: boolean;
|
|
21
22
|
submitState?: SubmitState;
|
|
23
|
+
isInitied?: boolean;
|
|
22
24
|
}
|
|
23
25
|
export default function useFormItemControl<T = any>({ formName, form, name, initialValue, formItemId, rules, elementRef, }: UseFormItemControlProps): UseFormItemControlReturn;
|
|
24
26
|
export {};
|
|
@@ -38,7 +38,7 @@ function useFormItemControl({ formName, form, name, initialValue, formItemId, ru
|
|
|
38
38
|
}));
|
|
39
39
|
const onInitData = (value2) => {
|
|
40
40
|
setInitData(formName || (form == null ? void 0 : form.formName) || (contextForm == null ? void 0 : contextForm.formName), name, value2);
|
|
41
|
-
onChange(value2, { notTriggerDirty: true });
|
|
41
|
+
onChange(value2, { notTriggerDirty: true, initiedData: true });
|
|
42
42
|
};
|
|
43
43
|
const onFocus = () => {
|
|
44
44
|
setListener({
|
|
@@ -60,10 +60,21 @@ function useFormItemControl({ formName, form, name, initialValue, formItemId, ru
|
|
|
60
60
|
isTouched: listener == null ? void 0 : listener.isTouched
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
+
if ((options == null ? void 0 : options.initiedData) === true) {
|
|
64
|
+
setListener({
|
|
65
|
+
formItemId,
|
|
66
|
+
isInitied: true
|
|
67
|
+
});
|
|
68
|
+
}
|
|
63
69
|
setData(formName || (form == null ? void 0 : form.formName) || (contextForm == null ? void 0 : contextForm.formName), name, value2);
|
|
64
70
|
};
|
|
65
71
|
const onReset = (value2) => {
|
|
66
|
-
|
|
72
|
+
setListener({
|
|
73
|
+
formItemId,
|
|
74
|
+
isDirty: false,
|
|
75
|
+
isTouched: false
|
|
76
|
+
});
|
|
77
|
+
onChange(isNil(value2) ? getInitData(formName || (form == null ? void 0 : form.formName) || (contextForm == null ? void 0 : contextForm.formName), name) : value2, { notTriggerDirty: true, initiedData: true });
|
|
67
78
|
};
|
|
68
79
|
const internalRules = useMemo(() => {
|
|
69
80
|
return rules || [];
|
|
@@ -262,7 +273,10 @@ function useFormItemControl({ formName, form, name, initialValue, formItemId, ru
|
|
|
262
273
|
onInitData(initialValue);
|
|
263
274
|
}
|
|
264
275
|
} else {
|
|
265
|
-
onChange(internalInitValue, {
|
|
276
|
+
onChange(internalInitValue, {
|
|
277
|
+
notTriggerDirty: true,
|
|
278
|
+
initiedData: true
|
|
279
|
+
});
|
|
266
280
|
}
|
|
267
281
|
}
|
|
268
282
|
return;
|
|
@@ -319,7 +333,9 @@ function useFormItemControl({ formName, form, name, initialValue, formItemId, ru
|
|
|
319
333
|
errors,
|
|
320
334
|
onFocus,
|
|
321
335
|
isDirty: listener == null ? void 0 : listener.isDirty,
|
|
322
|
-
|
|
336
|
+
isTouched: listener == null ? void 0 : listener.isTouched,
|
|
337
|
+
submitState,
|
|
338
|
+
isInitied: listener == null ? void 0 : listener.isInitied
|
|
323
339
|
};
|
|
324
340
|
}
|
|
325
341
|
export {
|
|
@@ -55,11 +55,7 @@ function useFormListControl({ name, form, initialValues, formName }) {
|
|
|
55
55
|
};
|
|
56
56
|
const add = (index) => {
|
|
57
57
|
setListFields((prev) => {
|
|
58
|
-
if (index
|
|
59
|
-
return prev;
|
|
60
|
-
if (index < 0)
|
|
61
|
-
return prev;
|
|
62
|
-
if (index === prev.length) {
|
|
58
|
+
if (isNil(index) || index === prev.length) {
|
|
63
59
|
const newName = `${name}.${prev.length}`;
|
|
64
60
|
const newKey = v4();
|
|
65
61
|
const result2 = [
|
|
@@ -71,6 +67,10 @@ function useFormListControl({ name, form, initialValues, formName }) {
|
|
|
71
67
|
];
|
|
72
68
|
return result2;
|
|
73
69
|
}
|
|
70
|
+
if (index > prev.length)
|
|
71
|
+
return prev;
|
|
72
|
+
if (index < 0)
|
|
73
|
+
return prev;
|
|
74
74
|
const clonePrev = [...prev];
|
|
75
75
|
const result = clonePrev.reduce((prev2, cur, curIndex) => {
|
|
76
76
|
const newKey = v4();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Form, { useForm, useWatch, useSubmitDataWatch, useFormStateWatch, type FormProps, type ValidationRule, type FormFieldError, type SubmitState } from "./providers/Form";
|
|
1
|
+
import Form, { useForm, useWatch, useSubmitDataWatch, useFormStateWatch, type FormProps, type ValidationRule, type FormFieldError, type SubmitState, type UseFormItemStateWatchReturn } from "./providers/Form";
|
|
2
2
|
import { SUBMIT_STATE } from "./constants/form";
|
|
3
3
|
import FormItem, { type FormItemProps } from "./components/Form/FormItem";
|
|
4
4
|
import FormList, { type FormListProps } from "./components/Form/FormList";
|
|
@@ -6,5 +6,5 @@ import Input from "./components/Input";
|
|
|
6
6
|
import InputWrapper, { type InputWrapperProps } from "./components/Form/InputWrapper";
|
|
7
7
|
import useFormItemControl from "./hooks/useFormItemControl";
|
|
8
8
|
import useFormListControl from "./hooks/useFormListControl";
|
|
9
|
-
export { Form, FormItem, FormList, Input, InputWrapper, useFormItemControl, useFormListControl, useForm, useWatch, useSubmitDataWatch, useFormStateWatch, type FormProps, type FormItemProps, type FormListProps, type InputWrapperProps, type ValidationRule, type FormFieldError, type SubmitState, SUBMIT_STATE, };
|
|
9
|
+
export { Form, FormItem, FormList, Input, InputWrapper, useFormItemControl, useFormListControl, useForm, useWatch, useSubmitDataWatch, useFormStateWatch, type FormProps, type FormItemProps, type FormListProps, type InputWrapperProps, type ValidationRule, type FormFieldError, type SubmitState, type UseFormItemStateWatchReturn, SUBMIT_STATE, };
|
|
10
10
|
export default Form;
|
package/dist/providers/Form.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentType, FormHTMLAttributes, ReactNode } from "react";
|
|
2
|
-
import type { PublicFormInstance } from "../types/public";
|
|
3
|
-
export type {
|
|
2
|
+
import type { PublicFormInstance, UseFormItemStateWatchReturn } from "../types/public";
|
|
3
|
+
export type { FormFieldError, SubmitState, UseFormItemStateWatchReturn, ValidationRule, } from "../types/public";
|
|
4
4
|
export declare const FormContext: import("react").Context<any>;
|
|
5
5
|
export interface FormProps<T = any> extends Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
|
|
6
6
|
children: ReactNode;
|
|
@@ -21,6 +21,7 @@ declare namespace Form {
|
|
|
21
21
|
var useWatch: typeof import("./Form").useWatch;
|
|
22
22
|
var useSubmitDataWatch: typeof import("./Form").useSubmitDataWatch;
|
|
23
23
|
var useFormStateWatch: <T = any>(formNameOrFormInstance?: string | PublicFormInstance<T>) => any;
|
|
24
|
+
var useFormItemStateWatch: <T = any>(nameOrFormItemId: string, formNameOrFormInstance?: string | PublicFormInstance<T>) => UseFormItemStateWatchReturn;
|
|
24
25
|
}
|
|
25
26
|
export default Form;
|
|
26
27
|
export declare function useFormContext(): any;
|
|
@@ -32,3 +33,4 @@ export declare function useSubmitDataWatch<T = any>({ formNameOrFormInstance, tr
|
|
|
32
33
|
mapFn?: (v: any, prev: any) => any;
|
|
33
34
|
}): T | undefined;
|
|
34
35
|
export declare const useFormStateWatch: <T = any>(formNameOrFormInstance?: string | PublicFormInstance<T>) => any;
|
|
36
|
+
export declare const useFormItemStateWatch: <T = any>(nameOrFormItemId: string, formNameOrFormInstance?: string | PublicFormInstance<T>) => UseFormItemStateWatchReturn;
|
package/dist/providers/Form.js
CHANGED
|
@@ -1,33 +1,44 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { cloneDeep, get, isEqual, last, set,
|
|
2
|
+
import { cloneDeep, get, isEqual, isNil, 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";
|
|
6
6
|
import { useShallow } from "zustand/react/shallow";
|
|
7
7
|
import FormCleanUp from "../components/Form/FormCleanUp";
|
|
8
|
+
import { SUBMIT_STATE } from "../constants/form";
|
|
8
9
|
import { useFormListeners, useFormStore } from "../stores/formStore";
|
|
9
10
|
import { getAllNoneObjStringPath } from "../utils/obj.util";
|
|
10
11
|
const FormContext = createContext(null);
|
|
11
12
|
function Form({ children, formName, initialValues, onFinish, onReject, onFinally, FormElement, ...props }) {
|
|
12
|
-
const {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
const {
|
|
14
|
+
// appInitValue,
|
|
15
|
+
getFormItemValue,
|
|
16
|
+
setInitData,
|
|
17
|
+
setData,
|
|
18
|
+
getFormValues,
|
|
19
|
+
setFormState,
|
|
20
|
+
setFormInstance,
|
|
21
|
+
revokeFormInstance,
|
|
22
|
+
setSubmitHistory,
|
|
23
|
+
clearFormValues,
|
|
24
|
+
clearFormInitialValues,
|
|
25
|
+
clearFormState
|
|
26
|
+
} = useFormStore(useShallow((state) => ({
|
|
27
|
+
// appInitValue: state.initialValues,
|
|
28
|
+
setInitData: state.setInitData,
|
|
29
|
+
getFormValues: state.getFormValues,
|
|
30
|
+
setFormState: state.setFormState,
|
|
31
|
+
setFormInstance: state.setFormInstance,
|
|
32
|
+
revokeFormInstance: state.revokeFormInstance,
|
|
33
|
+
setData: state.setData,
|
|
34
|
+
setSubmitHistory: state.setSubmitHistory,
|
|
35
|
+
getFormItemValue: state.getFormItemValue,
|
|
36
|
+
clearFormValues: state.clearFormValues,
|
|
37
|
+
clearFormInitialValues: state.clearFormInitialValues,
|
|
38
|
+
clearFormState: state.clearFormState
|
|
39
|
+
// Test, nhớ xóa sau khi xong
|
|
40
|
+
// formStates: state.formStates?.[formName],
|
|
41
|
+
})));
|
|
31
42
|
const { getListeners } = useFormListeners(useShallow((state) => {
|
|
32
43
|
return {
|
|
33
44
|
getListeners: state.getListeners
|
|
@@ -95,7 +106,7 @@ function Form({ children, formName, initialValues, onFinish, onReject, onFinally
|
|
|
95
106
|
const formValues = getFormValues(formName);
|
|
96
107
|
const resultValues = cloneDeep(formValues);
|
|
97
108
|
const cleanValues = {};
|
|
98
|
-
|
|
109
|
+
uniqBy(listeners, (l) => l.name).forEach((l) => {
|
|
99
110
|
set(cleanValues, l.name, get(resultValues, l.name));
|
|
100
111
|
});
|
|
101
112
|
const handleIsolateCase = async () => {
|
|
@@ -167,7 +178,8 @@ function Form({ children, formName, initialValues, onFinish, onReject, onFinally
|
|
|
167
178
|
reset();
|
|
168
179
|
flushSync(setFormState({
|
|
169
180
|
formName,
|
|
170
|
-
isInitied: false
|
|
181
|
+
isInitied: false,
|
|
182
|
+
submitState: SUBMIT_STATE.IDLE
|
|
171
183
|
}));
|
|
172
184
|
const totalListenerFields = getListeners();
|
|
173
185
|
if (Array.isArray(resetOptions)) {
|
|
@@ -252,7 +264,8 @@ function useFormContext() {
|
|
|
252
264
|
return c;
|
|
253
265
|
}
|
|
254
266
|
function useForm(formNameOrFormInstance) {
|
|
255
|
-
const
|
|
267
|
+
const formContext = useContext(FormContext);
|
|
268
|
+
const targetFormName = isNil(formNameOrFormInstance) ? formContext == null ? void 0 : formContext.formName : typeof formNameOrFormInstance === "object" && formNameOrFormInstance !== null ? formNameOrFormInstance.formName : formNameOrFormInstance;
|
|
256
269
|
const formInstance = useFormStore((state) => {
|
|
257
270
|
return state.formInstances.find((i) => i.formName === targetFormName);
|
|
258
271
|
});
|
|
@@ -287,15 +300,28 @@ const useFormStateWatch = (formNameOrFormInstance) => {
|
|
|
287
300
|
});
|
|
288
301
|
return formState;
|
|
289
302
|
};
|
|
303
|
+
const useFormItemStateWatch = (nameOrFormItemId, formNameOrFormInstance) => {
|
|
304
|
+
const [formInstance] = useForm(formNameOrFormInstance);
|
|
305
|
+
const listener = useFormStore((state) => {
|
|
306
|
+
return state.getListeners().find((l) => l.formName === (formInstance == null ? void 0 : formInstance.formName) && (l.name === nameOrFormItemId || l.formItemId === nameOrFormItemId));
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
isTouched: listener == null ? void 0 : listener.isTouched,
|
|
310
|
+
isDirty: listener == null ? void 0 : listener.isDirty,
|
|
311
|
+
errors: (listener == null ? void 0 : listener.internalErrors) || []
|
|
312
|
+
};
|
|
313
|
+
};
|
|
290
314
|
Form.useForm = useForm;
|
|
291
315
|
Form.useWatch = useWatch;
|
|
292
316
|
Form.useSubmitDataWatch = useSubmitDataWatch;
|
|
293
317
|
Form.useFormStateWatch = useFormStateWatch;
|
|
318
|
+
Form.useFormItemStateWatch = useFormItemStateWatch;
|
|
294
319
|
export {
|
|
295
320
|
FormContext,
|
|
296
321
|
Form as default,
|
|
297
322
|
useForm,
|
|
298
323
|
useFormContext,
|
|
324
|
+
useFormItemStateWatch,
|
|
299
325
|
useFormStateWatch,
|
|
300
326
|
useSubmitDataWatch,
|
|
301
327
|
useWatch
|
package/dist/stores/formStore.js
CHANGED
|
@@ -200,7 +200,7 @@ const useFormListeners = create((storeSet, storeGet) => ({
|
|
|
200
200
|
getListeners() {
|
|
201
201
|
return storeGet().listeners;
|
|
202
202
|
},
|
|
203
|
-
setListener({ formName, name, onChange, onReset, isTouched, isDirty, formItemId, internalErrors, onFocus, emitFocus }) {
|
|
203
|
+
setListener({ formName, name, onChange, onReset, isTouched, isDirty, formItemId, internalErrors, onFocus, emitFocus, isInitied }) {
|
|
204
204
|
return storeSet(produce((state) => {
|
|
205
205
|
const storeListeners = state.listeners;
|
|
206
206
|
const findListenerIndex = state.listeners.findIndex((l) => l.formItemId === formItemId);
|
|
@@ -232,17 +232,21 @@ const useFormListeners = create((storeSet, storeGet) => ({
|
|
|
232
232
|
if (!isNil(emitFocus)) {
|
|
233
233
|
storeListeners[findListenerIndex].emitFocus = emitFocus;
|
|
234
234
|
}
|
|
235
|
+
if (!isNil(isInitied)) {
|
|
236
|
+
storeListeners[findListenerIndex].isInitied = isInitied;
|
|
237
|
+
}
|
|
235
238
|
return;
|
|
236
239
|
}
|
|
237
240
|
storeListeners.push({
|
|
238
241
|
name,
|
|
239
242
|
formName,
|
|
240
|
-
isTouched,
|
|
241
|
-
isDirty,
|
|
243
|
+
isTouched: Boolean(isTouched),
|
|
244
|
+
isDirty: Boolean(isDirty),
|
|
242
245
|
formItemId,
|
|
243
246
|
internalErrors,
|
|
244
247
|
onChange,
|
|
245
|
-
onReset
|
|
248
|
+
onReset,
|
|
249
|
+
isInitied: Boolean(isInitied)
|
|
246
250
|
});
|
|
247
251
|
}));
|
|
248
252
|
},
|
package/dist/types/public.d.ts
CHANGED
|
@@ -61,6 +61,11 @@ export interface UseFormItemReturn<T = any> {
|
|
|
61
61
|
isDirty?: boolean;
|
|
62
62
|
submitState?: SubmitState;
|
|
63
63
|
}
|
|
64
|
+
export interface UseFormItemStateWatchReturn {
|
|
65
|
+
isTouched?: boolean;
|
|
66
|
+
isDirty?: boolean;
|
|
67
|
+
errors: FormFieldError[];
|
|
68
|
+
}
|
|
64
69
|
export interface UseFormListProps<T = any> {
|
|
65
70
|
name?: string;
|
|
66
71
|
form?: PublicFormInstance<T>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-form-manage",
|
|
3
|
-
"version": "1.0.8-beta.
|
|
3
|
+
"version": "1.0.8-beta.11",
|
|
4
4
|
"description": "Lightweight React form management with list and listener support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"@emotion/styled": "^11.14.1",
|
|
37
37
|
"@eslint/js": "^9.14.0",
|
|
38
38
|
"@mui/material": "^7.3.7",
|
|
39
|
+
"@types/lodash": "^4.17.23",
|
|
39
40
|
"@types/node": "^22.9.3",
|
|
40
41
|
"@types/react": "^19.2.9",
|
|
41
42
|
"@types/react-dom": "^19.2.3",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"eslint": "^9.14.0",
|
|
46
47
|
"eslint-plugin-react-hooks": "^5.1.0",
|
|
47
48
|
"eslint-plugin-react-refresh": "^0.4.14",
|
|
49
|
+
"framer-motion": "^12.29.0",
|
|
48
50
|
"globals": "^15.12.0",
|
|
49
51
|
"react": "^19.0.0",
|
|
50
52
|
"react-dom": "^19.0.0",
|
package/src/App.tsx
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
+
import { Checkbox } from "@mui/material";
|
|
1
2
|
import { Button, Input } from "antd";
|
|
3
|
+
import { motion } from "framer-motion";
|
|
2
4
|
import { useEffect } from "react";
|
|
3
5
|
import FormItem from "./components/Form/FormItem";
|
|
4
6
|
import FormList from "./components/Form/FormList";
|
|
5
7
|
import InputWrapper from "./components/Form/InputWrapper";
|
|
6
8
|
import Form, { useForm } from "./providers/Form";
|
|
7
9
|
|
|
10
|
+
import { Form as AntdForm } from "antd";
|
|
11
|
+
|
|
12
|
+
function TestFormWatch() {
|
|
13
|
+
const watchValue = Form.useWatch("numericCode");
|
|
14
|
+
|
|
15
|
+
return <div>TestFormWatch</div>;
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
const App = () => {
|
|
9
19
|
const [form] = useForm("form1");
|
|
10
20
|
|
|
21
|
+
const watchCheckBox = Form.useWatch("checkControlledAfterInit", "form1");
|
|
22
|
+
|
|
11
23
|
useEffect(() => {
|
|
12
24
|
if (form) {
|
|
13
25
|
// setTimeout(() => {
|
|
@@ -17,6 +29,20 @@ const App = () => {
|
|
|
17
29
|
}, [form]);
|
|
18
30
|
return (
|
|
19
31
|
<div>
|
|
32
|
+
<AntdForm>
|
|
33
|
+
<motion.div
|
|
34
|
+
initial={{ opacity: 0 }}
|
|
35
|
+
animate={{ opacity: 1 }}
|
|
36
|
+
exit={{ opacity: 0 }}
|
|
37
|
+
transition={{ duration: 1 }}
|
|
38
|
+
>
|
|
39
|
+
<AntdForm.Item name={"234"} initialValue={"23432"} label="Antd Input">
|
|
40
|
+
<Input />
|
|
41
|
+
</AntdForm.Item>
|
|
42
|
+
</motion.div>
|
|
43
|
+
</AntdForm>
|
|
44
|
+
|
|
45
|
+
{/* Hidden Test */}
|
|
20
46
|
<Form
|
|
21
47
|
initialValues={{
|
|
22
48
|
TestData: "",
|
|
@@ -27,13 +53,25 @@ const App = () => {
|
|
|
27
53
|
console.log(values);
|
|
28
54
|
}}
|
|
29
55
|
formName={"form1"}
|
|
56
|
+
// hidden
|
|
30
57
|
>
|
|
31
|
-
<FormItem
|
|
58
|
+
<FormItem
|
|
59
|
+
name={"username"}
|
|
60
|
+
rules={[
|
|
61
|
+
{
|
|
62
|
+
required: true,
|
|
63
|
+
message: "Test",
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
initialValue={"283746"}
|
|
67
|
+
// hidden
|
|
68
|
+
>
|
|
32
69
|
<InputWrapper>
|
|
33
70
|
<Input />
|
|
34
71
|
</InputWrapper>
|
|
35
72
|
</FormItem>
|
|
36
73
|
|
|
74
|
+
{/* Numberic test */}
|
|
37
75
|
<FormItem
|
|
38
76
|
name={"numericCode"}
|
|
39
77
|
rules={[
|
|
@@ -48,6 +86,20 @@ const App = () => {
|
|
|
48
86
|
<Input placeholder="Mã chỉ gồm số" style={{ width: 200 }} />
|
|
49
87
|
</InputWrapper>
|
|
50
88
|
</FormItem>
|
|
89
|
+
|
|
90
|
+
{/* Motion Test */}
|
|
91
|
+
<motion.div
|
|
92
|
+
initial={{ opacity: 0 }}
|
|
93
|
+
animate={{ opacity: 1 }}
|
|
94
|
+
exit={{ opacity: 0 }}
|
|
95
|
+
transition={{ duration: 1 }}
|
|
96
|
+
>
|
|
97
|
+
<FormItem name="motionTest" controlAfterInit initialValue={"1234134"}>
|
|
98
|
+
<InputWrapper>
|
|
99
|
+
<Input placeholder="Motion Test" style={{ width: 200 }} />
|
|
100
|
+
</InputWrapper>
|
|
101
|
+
</FormItem>
|
|
102
|
+
</motion.div>
|
|
51
103
|
<FormList
|
|
52
104
|
initialValues={[
|
|
53
105
|
{
|
|
@@ -93,12 +145,40 @@ const App = () => {
|
|
|
93
145
|
</div>
|
|
94
146
|
)}
|
|
95
147
|
</FormList>
|
|
148
|
+
<motion.div
|
|
149
|
+
initial={{ opacity: 0 }}
|
|
150
|
+
animate={{ opacity: 1 }}
|
|
151
|
+
exit={{ opacity: 0 }}
|
|
152
|
+
transition={{ duration: 1.5 }}
|
|
153
|
+
>
|
|
154
|
+
<FormItem
|
|
155
|
+
valuePropName="checked"
|
|
156
|
+
getValueFromEvent={(_, checked) => checked}
|
|
157
|
+
name="checkControlledAfterInit"
|
|
158
|
+
controlAfterInit={true}
|
|
159
|
+
initialValue={true}
|
|
160
|
+
>
|
|
161
|
+
<Checkbox />
|
|
162
|
+
</FormItem>
|
|
163
|
+
</motion.div>
|
|
164
|
+
<TestFormWatch />
|
|
165
|
+
<Button
|
|
166
|
+
onClick={() => {
|
|
167
|
+
const current = form?.getFieldValue("checkControlledAfterInit");
|
|
168
|
+
console.log("Toggle controlled after init: ", current);
|
|
169
|
+
form?.setFieldValue("checkControlledAfterInit", !current);
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
Toggle
|
|
173
|
+
</Button>
|
|
96
174
|
<Button htmlType="submit">Submit</Button>
|
|
97
175
|
<Button
|
|
98
176
|
onClick={() => {
|
|
99
177
|
form?.resetFields?.();
|
|
100
178
|
}}
|
|
101
|
-
|
|
179
|
+
>
|
|
180
|
+
Reset
|
|
181
|
+
</Button>
|
|
102
182
|
</Form>
|
|
103
183
|
</div>
|
|
104
184
|
);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
-
import { cloneElement, useRef, useState } from "react";
|
|
2
|
+
import { cloneElement, Fragment, useRef, useState } from "react";
|
|
3
3
|
import { v4 } from "uuid";
|
|
4
4
|
import useFormItemControl from "../../hooks/useFormItemControl";
|
|
5
5
|
import type { ValidationRule } from "../../types/public";
|
|
6
6
|
|
|
7
7
|
export interface FormItemProps {
|
|
8
|
-
children: ReactElement
|
|
8
|
+
children: ReactElement<any>;
|
|
9
9
|
name: string;
|
|
10
10
|
formName?: string;
|
|
11
11
|
initialValue?: any;
|
|
@@ -13,6 +13,8 @@ export interface FormItemProps {
|
|
|
13
13
|
rules?: ValidationRule[];
|
|
14
14
|
valuePropName?: string;
|
|
15
15
|
getValueFromEvent?: (...args: any[]) => any;
|
|
16
|
+
controlAfterInit?: boolean;
|
|
17
|
+
hidden?: boolean;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export default function FormItem({
|
|
@@ -24,52 +26,85 @@ export default function FormItem({
|
|
|
24
26
|
rules,
|
|
25
27
|
valuePropName = "value",
|
|
26
28
|
getValueFromEvent,
|
|
29
|
+
controlAfterInit = false,
|
|
30
|
+
hidden,
|
|
27
31
|
}: FormItemProps) {
|
|
28
32
|
const elRef = useRef<any>(null);
|
|
29
33
|
|
|
30
34
|
const [formItemId] = useState(externalFormItemId ?? v4());
|
|
31
|
-
const {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
const {
|
|
36
|
+
value,
|
|
37
|
+
onChange,
|
|
38
|
+
errors,
|
|
39
|
+
state,
|
|
40
|
+
onFocus,
|
|
41
|
+
isDirty,
|
|
42
|
+
submitState,
|
|
43
|
+
isTouched,
|
|
44
|
+
isInitied,
|
|
45
|
+
} = useFormItemControl({
|
|
46
|
+
formName,
|
|
47
|
+
name,
|
|
48
|
+
initialValue,
|
|
49
|
+
formItemId,
|
|
50
|
+
rules,
|
|
51
|
+
elementRef: elRef,
|
|
52
|
+
});
|
|
40
53
|
// console.log("re-render", formName, name);
|
|
41
54
|
|
|
42
55
|
// useEffect(() => {
|
|
43
56
|
// console.log({ value });
|
|
44
57
|
// }, [value]);
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
// useEffect(() => {
|
|
60
|
+
// console.log("isInitied changed: ", {
|
|
61
|
+
// isInitied,
|
|
62
|
+
// name,
|
|
63
|
+
// value,
|
|
64
|
+
// controlAfterInit,
|
|
65
|
+
// });
|
|
66
|
+
// }, [isInitied]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Fragment
|
|
70
|
+
key={`control-after-init-${Boolean(controlAfterInit && isInitied) ? "1" : "0"}-${formItemId}`}
|
|
71
|
+
>
|
|
72
|
+
{!hidden && children
|
|
73
|
+
? cloneElement(children, {
|
|
74
|
+
name,
|
|
75
|
+
// ref: inputRef,
|
|
76
|
+
[valuePropName]: value,
|
|
77
|
+
onChange: (...args: any[]) => {
|
|
78
|
+
let val = args[0];
|
|
79
|
+
if (
|
|
80
|
+
getValueFromEvent &&
|
|
81
|
+
typeof getValueFromEvent === "function"
|
|
82
|
+
) {
|
|
83
|
+
val = getValueFromEvent(...args);
|
|
84
|
+
} else {
|
|
85
|
+
const e = args[0];
|
|
86
|
+
if (e && e.target) {
|
|
87
|
+
val = e.target.value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
onChange(val);
|
|
91
|
+
},
|
|
92
|
+
// onFocus: () => {
|
|
93
|
+
// setIsTouched(true);
|
|
94
|
+
// },
|
|
95
|
+
// isTouched: isTouched,
|
|
96
|
+
isDirty: isDirty,
|
|
97
|
+
// errors: errors,
|
|
98
|
+
// formState,
|
|
68
99
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
errors,
|
|
101
|
+
onFocus,
|
|
102
|
+
validateState: state,
|
|
103
|
+
ref: elRef,
|
|
104
|
+
submitState,
|
|
105
|
+
isTouched: isTouched,
|
|
106
|
+
} as any)
|
|
107
|
+
: null}
|
|
108
|
+
</Fragment>
|
|
109
|
+
);
|
|
75
110
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import useFormListControl from "../../hooks/useFormListControl";
|
|
2
2
|
import type { FormInstance } from "../../stores/formStore";
|
|
3
3
|
|
|
4
4
|
export interface FormListProps<T = any> {
|
|
@@ -6,11 +6,24 @@ export interface FormListProps<T = any> {
|
|
|
6
6
|
initialValues?: T[];
|
|
7
7
|
form?: FormInstance;
|
|
8
8
|
formName?: string;
|
|
9
|
-
children: (
|
|
9
|
+
children: (
|
|
10
|
+
fields: Array<{ name: string; key: string }>,
|
|
11
|
+
operations: {
|
|
12
|
+
add: (index: number) => void;
|
|
13
|
+
remove: (opts: { index?: number; key?: string }) => void;
|
|
14
|
+
move: (opts: { from?: number; fromKey?: string; to: number }) => void;
|
|
15
|
+
},
|
|
16
|
+
) => React.ReactNode;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
const FormList = <T = any
|
|
13
|
-
|
|
19
|
+
const FormList = <T = any,>({
|
|
20
|
+
name,
|
|
21
|
+
initialValues,
|
|
22
|
+
form,
|
|
23
|
+
formName,
|
|
24
|
+
children,
|
|
25
|
+
}: FormListProps<T>) => {
|
|
26
|
+
const { listFields, ...actions } = useFormListControl({
|
|
14
27
|
name,
|
|
15
28
|
initialValues,
|
|
16
29
|
form,
|
|
@@ -13,6 +13,11 @@ const InputWrapper = ({
|
|
|
13
13
|
errors = [],
|
|
14
14
|
...props
|
|
15
15
|
}: InputWrapperProps) => {
|
|
16
|
+
// useEffect(() => {
|
|
17
|
+
// console.log("InputWrapper submitState changed: ", {
|
|
18
|
+
// submitState: props.submitState,
|
|
19
|
+
// });
|
|
20
|
+
// }, [props.submitState]);
|
|
16
21
|
return (
|
|
17
22
|
<div>
|
|
18
23
|
{cloneElement(children, props)}
|
|
@@ -50,7 +50,9 @@ export interface UseFormItemControlReturn {
|
|
|
50
50
|
errors: FormFieldError[];
|
|
51
51
|
onFocus: () => void;
|
|
52
52
|
isDirty?: boolean;
|
|
53
|
+
isTouched?: boolean;
|
|
53
54
|
submitState?: SubmitState;
|
|
55
|
+
isInitied?: boolean;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const VALID_PREMITIVE_TYPE = ["string", "number", "undefined"];
|
|
@@ -153,7 +155,7 @@ export default function useFormItemControl<T = any>({
|
|
|
153
155
|
name,
|
|
154
156
|
value,
|
|
155
157
|
);
|
|
156
|
-
onChange(value, { notTriggerDirty: true });
|
|
158
|
+
onChange(value, { notTriggerDirty: true, initiedData: true });
|
|
157
159
|
};
|
|
158
160
|
|
|
159
161
|
const onFocus = () => {
|
|
@@ -170,7 +172,13 @@ export default function useFormItemControl<T = any>({
|
|
|
170
172
|
}
|
|
171
173
|
};
|
|
172
174
|
|
|
173
|
-
const onChange = (
|
|
175
|
+
const onChange = (
|
|
176
|
+
value: T,
|
|
177
|
+
options?: {
|
|
178
|
+
notTriggerDirty?: boolean;
|
|
179
|
+
initiedData?: boolean;
|
|
180
|
+
},
|
|
181
|
+
) => {
|
|
174
182
|
if (options?.notTriggerDirty !== true) {
|
|
175
183
|
setListener({
|
|
176
184
|
formItemId,
|
|
@@ -178,6 +186,12 @@ export default function useFormItemControl<T = any>({
|
|
|
178
186
|
isTouched: listener?.isTouched,
|
|
179
187
|
});
|
|
180
188
|
}
|
|
189
|
+
if (options?.initiedData === true) {
|
|
190
|
+
setListener({
|
|
191
|
+
formItemId,
|
|
192
|
+
isInitied: true,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
181
195
|
setData(formName || form?.formName || contextForm?.formName, name, value);
|
|
182
196
|
};
|
|
183
197
|
|
|
@@ -187,10 +201,16 @@ export default function useFormItemControl<T = any>({
|
|
|
187
201
|
value,
|
|
188
202
|
getInitData(formName || form?.formName || contextForm?.formName, name),
|
|
189
203
|
);
|
|
204
|
+
setListener({
|
|
205
|
+
formItemId,
|
|
206
|
+
isDirty: false,
|
|
207
|
+
isTouched: false,
|
|
208
|
+
});
|
|
190
209
|
onChange(
|
|
191
210
|
isNil(value)
|
|
192
211
|
? getInitData(formName || form?.formName || contextForm?.formName, name)
|
|
193
212
|
: value,
|
|
213
|
+
{ notTriggerDirty: true, initiedData: true },
|
|
194
214
|
);
|
|
195
215
|
};
|
|
196
216
|
|
|
@@ -521,7 +541,10 @@ export default function useFormItemControl<T = any>({
|
|
|
521
541
|
onInitData(initialValue);
|
|
522
542
|
}
|
|
523
543
|
} else {
|
|
524
|
-
onChange(internalInitValue, {
|
|
544
|
+
onChange(internalInitValue, {
|
|
545
|
+
notTriggerDirty: true,
|
|
546
|
+
initiedData: true,
|
|
547
|
+
});
|
|
525
548
|
}
|
|
526
549
|
}
|
|
527
550
|
return;
|
|
@@ -596,6 +619,8 @@ export default function useFormItemControl<T = any>({
|
|
|
596
619
|
errors,
|
|
597
620
|
onFocus,
|
|
598
621
|
isDirty: listener?.isDirty,
|
|
622
|
+
isTouched: listener?.isTouched,
|
|
599
623
|
submitState,
|
|
624
|
+
isInitied: listener?.isInitied,
|
|
600
625
|
};
|
|
601
626
|
}
|
|
@@ -19,7 +19,7 @@ interface UseFormListControlProps {
|
|
|
19
19
|
interface UseFormListControlReturn {
|
|
20
20
|
listFields: ListField[];
|
|
21
21
|
move: (opts: { from?: number; fromKey?: string; to: number }) => void;
|
|
22
|
-
add: (index
|
|
22
|
+
add: (index?: number) => void;
|
|
23
23
|
remove: (opts: { index?: number; key?: string }) => void;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -31,7 +31,9 @@ export default function useFormListControl<T = any>({
|
|
|
31
31
|
}: UseFormListControlProps): UseFormListControlReturn {
|
|
32
32
|
const contextForm = useFormContext();
|
|
33
33
|
const getFormValues = useFormStore((state) => state.getFormValues);
|
|
34
|
-
const [listFormInitValues, setListFormInitValues] = useState<
|
|
34
|
+
const [listFormInitValues, setListFormInitValues] = useState<
|
|
35
|
+
any[] | undefined
|
|
36
|
+
>(undefined);
|
|
35
37
|
const { clearCacheData, setCacheData } = useFormStore(
|
|
36
38
|
useShallow((state) => ({
|
|
37
39
|
cacheData: state.cacheData,
|
|
@@ -135,13 +137,9 @@ export default function useFormListControl<T = any>({
|
|
|
135
137
|
);
|
|
136
138
|
};
|
|
137
139
|
|
|
138
|
-
const add = (index
|
|
140
|
+
const add = (index?: number) => {
|
|
139
141
|
setListFields((prev) => {
|
|
140
|
-
if (index
|
|
141
|
-
|
|
142
|
-
if (index < 0) return prev;
|
|
143
|
-
|
|
144
|
-
if (index === prev.length) {
|
|
142
|
+
if (isNil(index) || index === prev.length) {
|
|
145
143
|
const newName = `${name}.${prev.length}`;
|
|
146
144
|
const newKey = v4();
|
|
147
145
|
const result = [
|
|
@@ -154,6 +152,9 @@ export default function useFormListControl<T = any>({
|
|
|
154
152
|
|
|
155
153
|
return result;
|
|
156
154
|
}
|
|
155
|
+
if (index > prev.length) return prev;
|
|
156
|
+
|
|
157
|
+
if (index < 0) return prev;
|
|
157
158
|
|
|
158
159
|
const clonePrev = [...prev];
|
|
159
160
|
|
|
@@ -217,7 +218,15 @@ export default function useFormListControl<T = any>({
|
|
|
217
218
|
});
|
|
218
219
|
};
|
|
219
220
|
|
|
220
|
-
const move = ({
|
|
221
|
+
const move = ({
|
|
222
|
+
from,
|
|
223
|
+
fromKey,
|
|
224
|
+
to,
|
|
225
|
+
}: {
|
|
226
|
+
from?: number;
|
|
227
|
+
fromKey?: string;
|
|
228
|
+
to: number;
|
|
229
|
+
}) => {
|
|
221
230
|
setListFields((prev) => {
|
|
222
231
|
console.log("move list item: ", { from, to });
|
|
223
232
|
if (
|
|
@@ -286,7 +295,7 @@ export default function useFormListControl<T = any>({
|
|
|
286
295
|
useEffect(() => {
|
|
287
296
|
console.log("Trigger init", formState?.isInitied, internalInitValue);
|
|
288
297
|
if (formState?.isInitied) {
|
|
289
|
-
|
|
298
|
+
if (Array.isArray(listFormInitValues)) {
|
|
290
299
|
const result = listFormInitValues?.map((_, i) => {
|
|
291
300
|
const itemName = `${name}.${i}`;
|
|
292
301
|
const key = v4();
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import Form, {
|
|
|
7
7
|
type ValidationRule,
|
|
8
8
|
type FormFieldError,
|
|
9
9
|
type SubmitState,
|
|
10
|
+
type UseFormItemStateWatchReturn,
|
|
10
11
|
} from "./providers/Form";
|
|
11
12
|
import { SUBMIT_STATE } from "./constants/form";
|
|
12
13
|
|
|
@@ -37,6 +38,7 @@ export {
|
|
|
37
38
|
type ValidationRule,
|
|
38
39
|
type FormFieldError,
|
|
39
40
|
type SubmitState,
|
|
41
|
+
type UseFormItemStateWatchReturn,
|
|
40
42
|
SUBMIT_STATE,
|
|
41
43
|
};
|
|
42
44
|
|
package/src/providers/Form.tsx
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import { cloneDeep, get, isEqual, last, set,
|
|
1
|
+
import { cloneDeep, get, isEqual, isNil, last, set, uniqBy } from "lodash";
|
|
2
2
|
import { useTask } from "minh-custom-hooks-release";
|
|
3
3
|
import type { ComponentType, FormHTMLAttributes, ReactNode } from "react";
|
|
4
4
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
5
5
|
import { flushSync } from "react-dom";
|
|
6
6
|
import { useShallow } from "zustand/react/shallow"; // Import useShallow
|
|
7
7
|
import FormCleanUp from "../components/Form/FormCleanUp";
|
|
8
|
+
import { SUBMIT_STATE } from "../constants/form";
|
|
8
9
|
import { useFormListeners, useFormStore } from "../stores/formStore";
|
|
9
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
PublicFormInstance,
|
|
12
|
+
UseFormItemStateWatchReturn,
|
|
13
|
+
} from "../types/public";
|
|
10
14
|
import { getAllNoneObjStringPath } from "../utils/obj.util";
|
|
11
|
-
export type {
|
|
15
|
+
export type {
|
|
16
|
+
FormFieldError,
|
|
17
|
+
SubmitState,
|
|
18
|
+
UseFormItemStateWatchReturn,
|
|
19
|
+
ValidationRule,
|
|
20
|
+
} from "../types/public";
|
|
12
21
|
|
|
13
22
|
export const FormContext = createContext(null);
|
|
14
23
|
|
|
@@ -40,7 +49,7 @@ export default function Form<T = any>({
|
|
|
40
49
|
...props
|
|
41
50
|
}: FormProps<T>) {
|
|
42
51
|
const {
|
|
43
|
-
appInitValue,
|
|
52
|
+
// appInitValue,
|
|
44
53
|
getFormItemValue,
|
|
45
54
|
setInitData,
|
|
46
55
|
setData,
|
|
@@ -54,7 +63,7 @@ export default function Form<T = any>({
|
|
|
54
63
|
clearFormState,
|
|
55
64
|
} = useFormStore(
|
|
56
65
|
useShallow((state) => ({
|
|
57
|
-
appInitValue: state.initialValues,
|
|
66
|
+
// appInitValue: state.initialValues,
|
|
58
67
|
setInitData: state.setInitData,
|
|
59
68
|
getFormValues: state.getFormValues,
|
|
60
69
|
setFormState: state.setFormState,
|
|
@@ -68,7 +77,7 @@ export default function Form<T = any>({
|
|
|
68
77
|
clearFormState: state.clearFormState,
|
|
69
78
|
|
|
70
79
|
// Test, nhớ xóa sau khi xong
|
|
71
|
-
formStates: state.formStates?.[formName],
|
|
80
|
+
// formStates: state.formStates?.[formName],
|
|
72
81
|
})),
|
|
73
82
|
);
|
|
74
83
|
|
|
@@ -165,8 +174,8 @@ export default function Form<T = any>({
|
|
|
165
174
|
|
|
166
175
|
const resultValues = cloneDeep(formValues);
|
|
167
176
|
const cleanValues = {} as T;
|
|
168
|
-
|
|
169
|
-
set(cleanValues, l.name, get(resultValues, l.name));
|
|
177
|
+
uniqBy(listeners, (l: any) => l.name).forEach((l) => {
|
|
178
|
+
set(cleanValues as any, l.name, get(resultValues, l.name));
|
|
170
179
|
});
|
|
171
180
|
|
|
172
181
|
const handleIsolateCase = async () => {
|
|
@@ -271,6 +280,7 @@ export default function Form<T = any>({
|
|
|
271
280
|
setFormState({
|
|
272
281
|
formName,
|
|
273
282
|
isInitied: false,
|
|
283
|
+
submitState: SUBMIT_STATE.IDLE,
|
|
274
284
|
}),
|
|
275
285
|
);
|
|
276
286
|
const totalListenerFields = getListeners();
|
|
@@ -403,9 +413,11 @@ export function useFormContext() {
|
|
|
403
413
|
export function useForm<T = any>(
|
|
404
414
|
formNameOrFormInstance?: string | PublicFormInstance<T>,
|
|
405
415
|
) {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
416
|
+
const formContext = useContext(FormContext);
|
|
417
|
+
const targetFormName = isNil(formNameOrFormInstance)
|
|
418
|
+
? formContext?.formName
|
|
419
|
+
: typeof formNameOrFormInstance === "object" &&
|
|
420
|
+
formNameOrFormInstance !== null
|
|
409
421
|
? (formNameOrFormInstance as PublicFormInstance<T>).formName
|
|
410
422
|
: (formNameOrFormInstance as string | undefined);
|
|
411
423
|
|
|
@@ -472,7 +484,33 @@ export const useFormStateWatch = <T = any,>(
|
|
|
472
484
|
return formState as any;
|
|
473
485
|
};
|
|
474
486
|
|
|
487
|
+
// Get Form Item State Using name (make sure no same name listener before using this) or formItemId, formNameOrFormInstance
|
|
488
|
+
// reutrn formItem state like isTouched, isDirty, errors
|
|
489
|
+
export const useFormItemStateWatch = <T = any,>(
|
|
490
|
+
nameOrFormItemId: string,
|
|
491
|
+
formNameOrFormInstance?: string | PublicFormInstance<T>,
|
|
492
|
+
): UseFormItemStateWatchReturn => {
|
|
493
|
+
const [formInstance] = useForm<T>(formNameOrFormInstance as any);
|
|
494
|
+
|
|
495
|
+
const listener = useFormStore((state) => {
|
|
496
|
+
return state
|
|
497
|
+
.getListeners()
|
|
498
|
+
.find(
|
|
499
|
+
(l) =>
|
|
500
|
+
l.formName === formInstance?.formName &&
|
|
501
|
+
(l.name === nameOrFormItemId || l.formItemId === nameOrFormItemId),
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
isTouched: listener?.isTouched,
|
|
507
|
+
isDirty: listener?.isDirty,
|
|
508
|
+
errors: listener?.internalErrors || [],
|
|
509
|
+
};
|
|
510
|
+
};
|
|
511
|
+
|
|
475
512
|
Form.useForm = useForm;
|
|
476
513
|
Form.useWatch = useWatch;
|
|
477
514
|
Form.useSubmitDataWatch = useSubmitDataWatch;
|
|
478
515
|
Form.useFormStateWatch = useFormStateWatch;
|
|
516
|
+
Form.useFormItemStateWatch = useFormItemStateWatch;
|
package/src/stores/formStore.ts
CHANGED
|
@@ -375,6 +375,7 @@ export const useFormListeners = create<any>((storeSet: any, storeGet: any) => ({
|
|
|
375
375
|
internalErrors,
|
|
376
376
|
onFocus,
|
|
377
377
|
emitFocus,
|
|
378
|
+
isInitied,
|
|
378
379
|
}) {
|
|
379
380
|
return storeSet(
|
|
380
381
|
produce<any>((state: any) => {
|
|
@@ -412,17 +413,22 @@ export const useFormListeners = create<any>((storeSet: any, storeGet: any) => ({
|
|
|
412
413
|
storeListeners[findListenerIndex].emitFocus = emitFocus;
|
|
413
414
|
}
|
|
414
415
|
|
|
416
|
+
if (!isNil(isInitied)) {
|
|
417
|
+
storeListeners[findListenerIndex].isInitied = isInitied;
|
|
418
|
+
}
|
|
419
|
+
|
|
415
420
|
return;
|
|
416
421
|
}
|
|
417
422
|
storeListeners.push({
|
|
418
423
|
name,
|
|
419
424
|
formName,
|
|
420
|
-
isTouched,
|
|
421
|
-
isDirty,
|
|
425
|
+
isTouched: Boolean(isTouched),
|
|
426
|
+
isDirty: Boolean(isDirty),
|
|
422
427
|
formItemId,
|
|
423
428
|
internalErrors,
|
|
424
429
|
onChange,
|
|
425
430
|
onReset,
|
|
431
|
+
isInitied: Boolean(isInitied),
|
|
426
432
|
});
|
|
427
433
|
}),
|
|
428
434
|
);
|
package/src/types/public.ts
CHANGED
|
@@ -67,6 +67,12 @@ export interface UseFormItemReturn<T = any> {
|
|
|
67
67
|
submitState?: SubmitState;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export interface UseFormItemStateWatchReturn {
|
|
71
|
+
isTouched?: boolean;
|
|
72
|
+
isDirty?: boolean;
|
|
73
|
+
errors: FormFieldError[];
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
export interface UseFormListProps<T = any> {
|
|
71
77
|
name?: string;
|
|
72
78
|
form?: PublicFormInstance<T>;
|