react-form-manage 1.0.8-beta.23 → 1.0.8-beta.24

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,6 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.0.8-beta.24] - 2026-02-04
6
+
7
+ ### Fixes
8
+ - Fix cleanup item execution for proper FormItem unmounting
9
+
10
+ ### Features
11
+ - Add `Component` prop to FormItem for wrapping input with custom elements
12
+ - Support custom wrapper components in FormItem
13
+
5
14
  ## [1.0.8-beta.23] - 2026-02-04
6
15
 
7
16
  ### Features
@@ -1,6 +1,6 @@
1
- import type { ReactElement } from "react";
1
+ import type { ComponentProps, FC, ReactElement } from "react";
2
2
  import type { ValidationRule } from "../../types/public";
3
- export interface FormItemProps {
3
+ export type BaseFormItemProps = {
4
4
  children: ReactElement<any>;
5
5
  name: string;
6
6
  formName?: string;
@@ -12,5 +12,10 @@ export interface FormItemProps {
12
12
  controlAfterInit?: boolean;
13
13
  hidden?: boolean;
14
14
  collectOnHidden?: boolean;
15
- }
16
- export default function FormItem({ children, name, formName, initialValue, formItemId: externalFormItemId, rules, valuePropName, getValueFromEvent, controlAfterInit, hidden, collectOnHidden, }: FormItemProps): import("react/jsx-runtime").JSX.Element;
15
+ };
16
+ export type FormItemProps<TCustomWrapper extends FC<any> | undefined> = BaseFormItemProps & ({
17
+ Component?: undefined;
18
+ } | ({
19
+ Component: TCustomWrapper;
20
+ } & Omit<ComponentProps<TCustomWrapper>, keyof BaseFormItemProps>));
21
+ export default function FormItem<TCustomWrapper extends FC<any> | undefined>({ children, Component, name, formName, initialValue, formItemId: externalFormItemId, rules, valuePropName, getValueFromEvent, controlAfterInit, hidden, collectOnHidden, ...props }: FormItemProps<TCustomWrapper>): import("react/jsx-runtime").JSX.Element;
@@ -1,23 +1,45 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cloneElement, Fragment, useRef, useState } from "react";
2
+ import { isNil } from "lodash";
3
+ import { cloneElement, Fragment, useEffect, useRef, useState } from "react";
3
4
  import { v4 } from "uuid";
4
5
  import useFormItemControl from "../../hooks/useFormItemControl";
5
- function FormItem({ children, name, formName, initialValue, formItemId: externalFormItemId, rules, valuePropName = "value", getValueFromEvent, controlAfterInit = false, hidden, collectOnHidden }) {
6
- const elRef = useRef(null);
7
- const [formItemId] = useState(externalFormItemId != null ? externalFormItemId : v4());
8
- const { value, onChange, errors, state, onFocus, isDirty, submitState, isTouched, isInitied } = useFormItemControl({
9
- formName,
6
+ function BaseFormItem({ children, name, elementRef: elRef, valuePropName, getValueFromEvent, onChange, value, isDirty, errors, onFocus, state, submitState, isTouched }) {
7
+ useEffect(() => {
8
+ }, [value]);
9
+ return cloneElement(children, {
10
10
  name,
11
- initialValue,
12
- formItemId,
13
- rules,
14
- elementRef: elRef,
15
- hidden,
16
- collectOnHidden
11
+ ref: elRef,
12
+ [valuePropName]: value,
13
+ onChange: (...args) => {
14
+ let val = args[0];
15
+ if (getValueFromEvent && typeof getValueFromEvent === "function") {
16
+ val = getValueFromEvent(...args);
17
+ } else {
18
+ const e = args[0];
19
+ if (e && e.target) {
20
+ val = e.target.value;
21
+ }
22
+ }
23
+ onChange(val);
24
+ },
25
+ // onFocus: () => {
26
+ // setIsTouched(true);
27
+ // },
28
+ // isTouched: isTouched,
29
+ isDirty,
30
+ // errors: errors,
31
+ // formState,
32
+ errors,
33
+ onFocus,
34
+ validateState: state,
35
+ submitState,
36
+ isTouched
17
37
  });
18
- return _jsx(Fragment, { children: !hidden && children ? cloneElement(children, {
38
+ }
39
+ function FormItemWithWrapper({ children, name, elementRef: elRef, valuePropName, getValueFromEvent, onChange, value, isDirty, errors, onFocus, state, submitState, isTouched, Component, ...props }) {
40
+ return _jsx(Component, { ...props, children: cloneElement(children, {
19
41
  name,
20
- // ref: inputRef,
42
+ ref: elRef,
21
43
  [valuePropName]: value,
22
44
  onChange: (...args) => {
23
45
  let val = args[0];
@@ -41,10 +63,24 @@ function FormItem({ children, name, formName, initialValue, formItemId: external
41
63
  errors,
42
64
  onFocus,
43
65
  validateState: state,
44
- ref: elRef,
45
66
  submitState,
46
67
  isTouched
47
- }) : null }, `control-after-init-${Boolean(controlAfterInit && isInitied) ? "1" : "0"}-${formItemId}`);
68
+ }) });
69
+ }
70
+ function FormItem({ children, Component, name, formName, initialValue, formItemId: externalFormItemId, rules, valuePropName = "value", getValueFromEvent, controlAfterInit = false, hidden, collectOnHidden, ...props }) {
71
+ const [formItemId] = useState(externalFormItemId != null ? externalFormItemId : v4());
72
+ const elRef = useRef(null);
73
+ const { value, onChange, errors, state, onFocus, isDirty, submitState, isTouched, isInitied } = useFormItemControl({
74
+ formName,
75
+ name,
76
+ initialValue,
77
+ formItemId,
78
+ rules,
79
+ elementRef: elRef,
80
+ hidden,
81
+ collectOnHidden
82
+ });
83
+ return _jsx(Fragment, { children: !hidden && children ? isNil(Component) ? _jsx(BaseFormItem, { elementRef: elRef, name, valuePropName, getValueFromEvent, value, onChange, errors, state, onFocus, isDirty, submitState, isTouched, formItemId, children }) : _jsx(FormItemWithWrapper, { elementRef: elRef, name, valuePropName, getValueFromEvent, value, onChange, errors, state, onFocus, isDirty, submitState, isTouched, formItemId, Component, ...props, children }) : null }, `control-after-init-${Boolean(controlAfterInit && isInitied) ? "1" : "0"}-${externalFormItemId}`);
48
84
  }
49
85
  export {
50
86
  FormItem as default
@@ -1,6 +1,6 @@
1
1
  import { get, has, isNil } from "lodash";
2
2
  import { useTaskEffect } from "minh-custom-hooks-release";
3
- import { useEffect, useMemo } from "react";
3
+ import { useEffect, useLayoutEffect, useMemo } from "react";
4
4
  import { useShallow } from "zustand/react/shallow";
5
5
  import { IS_ALPHABET_STRING_AND_NUMBER_REGEX, IS_EMAIL_REGEX, IS_NAME_REGEX, IS_NO_SPACE_ALPHABET_STRING_AND_NUMBER_REGEX, IS_NO_SPACE_ALPHABET_STRING_REGEX, IS_NOSPACE_STRING_AND_NUMBER_REGEX, IS_PASSWORD_REGEX, IS_POSITIVE_INTEGER_STRING_NUMBER_REGEX, IS_POSITIVE_STRING_NUMBER_REGEX, IS_STRING_AND_NUMBER_REGEX, IS_STRING_NUMBER_REGEX, IS_USERNAME_REGEX, IS_VIETNAMESE_PHONE_NUMBER_REGEX } from "../constants/validation";
6
6
  import { useFormContext } from "../providers/Form";
@@ -15,7 +15,10 @@ function useFormItemControl({ formName, form, name, initialValue, formItemId, ru
15
15
  getFormValues,
16
16
  // getFormState,
17
17
  isStateInitied,
18
- submitState
18
+ submitState,
19
+ getListener,
20
+ revokeListener,
21
+ clearObjKeyItem
19
22
  } = useFormStore(useShallow((state2) => {
20
23
  var _a, _b, _c, _d;
21
24
  return {
@@ -25,7 +28,10 @@ function useFormItemControl({ formName, form, name, initialValue, formItemId, ru
25
28
  getFormValues: state2.getFormValues,
26
29
  // getFormState: state.getFormState,
27
30
  isStateInitied: (_b = (_a = state2.formStates) == null ? void 0 : _a[formName || (form == null ? void 0 : form.formName) || (contextForm == null ? void 0 : contextForm.formName)]) == null ? void 0 : _b.isInitied,
28
- submitState: (_d = (_c = state2.formStates) == null ? void 0 : _c[formName || (form == null ? void 0 : form.formName) || (contextForm == null ? void 0 : contextForm.formName)]) == null ? void 0 : _d.submitState
31
+ submitState: (_d = (_c = state2.formStates) == null ? void 0 : _c[formName || (form == null ? void 0 : form.formName) || (contextForm == null ? void 0 : contextForm.formName)]) == null ? void 0 : _d.submitState,
32
+ getListener: state2.getListener,
33
+ revokeListener: state2.revokeListener,
34
+ clearObjKeyItem: state2.clearObjKeyItem
29
35
  };
30
36
  }));
31
37
  const { setCleanUpStack } = useFormStore(useShallow((state2) => ({
@@ -295,7 +301,7 @@ function useFormItemControl({ formName, form, name, initialValue, formItemId, ru
295
301
  return;
296
302
  }
297
303
  }, [isStateInitied]);
298
- useEffect(() => {
304
+ useLayoutEffect(() => {
299
305
  if (!listener) {
300
306
  setListener({
301
307
  onChange,
@@ -312,14 +318,22 @@ function useFormItemControl({ formName, form, name, initialValue, formItemId, ru
312
318
  });
313
319
  }
314
320
  return () => {
315
- if (listener) {
316
- setCleanUpStack({
317
- name: listener.name,
318
- itemKey: listener.formItemId
319
- });
320
- }
321
+ revokeListener(formItemId, (listener2, same) => {
322
+ if (!same.length) {
323
+ clearObjKeyItem(listener2.formName, listener2.name);
324
+ }
325
+ });
321
326
  };
322
327
  }, []);
328
+ useEffect(() => {
329
+ if (listener) {
330
+ setListener({
331
+ formItemId,
332
+ hidden,
333
+ collectOnHidden
334
+ });
335
+ }
336
+ }, [hidden, collectOnHidden]);
323
337
  useEffect(() => {
324
338
  if (listener) {
325
339
  setListener({
@@ -302,7 +302,7 @@ function Form({ children, formName, initialValues, onFinish, onReject, onFinally
302
302
  }, []);
303
303
  return _jsxs(FormContext.Provider, { value: {
304
304
  formName
305
- }, children: [FormElement ? _jsx(FormElement, { onSubmit: (e) => {
305
+ }, children: [_jsx(FormCleanUp, {}), FormElement ? _jsx(FormElement, { onSubmit: (e) => {
306
306
  e.preventDefault();
307
307
  e.stopPropagation();
308
308
  runSubmit(void 0);
@@ -310,7 +310,7 @@ function Form({ children, formName, initialValues, onFinish, onReject, onFinally
310
310
  e.preventDefault();
311
311
  e.stopPropagation();
312
312
  runSubmit(void 0);
313
- }, ...props, children }), _jsx(FormCleanUp, {})] });
313
+ }, ...props, children })] });
314
314
  }
315
315
  function useFormContext() {
316
316
  const c = useContext(FormContext);
@@ -0,0 +1,3 @@
1
+ type Props = {};
2
+ declare function CommonTest({}: Props): import("react/jsx-runtime").JSX.Element;
3
+ export default CommonTest;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Checkbox, Input } from "antd";
3
+ import { motion } from "framer-motion";
4
+ import FormItem from "../components/Form/FormItem";
5
+ import FormList from "../components/Form/FormList";
6
+ import InputWrapper from "../components/Form/InputWrapper";
7
+ import Form from "../providers/Form";
8
+ function CommonTest({}) {
9
+ const [form] = Form.useForm("form1");
10
+ const watchCheckBox = Form.useWatch("checkControlledAfterInit", "form1");
11
+ return _jsxs(Form, { initialValues: {
12
+ TestData: "",
13
+ numericCode: ""
14
+ // arr: [{ el: "Item 1" }, { el: "Item 2" }],
15
+ }, onFinish: (values) => {
16
+ }, formName: "form1", children: [_jsx(FormItem, { name: "username", rules: [
17
+ {
18
+ required: true,
19
+ message: "Test"
20
+ }
21
+ ], initialValue: "283746", children: _jsx(InputWrapper, { children: _jsx(Input, {}) }) }), _jsx(FormItem, { name: "numericCode", rules: [
22
+ { required: true, message: "Vui l\xF2ng nh\u1EADp m\xE3" },
23
+ {
24
+ pattern: /^\d+$/,
25
+ message: "Ch\u1EC9 cho ph\xE9p chu\u1ED7i s\u1ED1"
26
+ }
27
+ ], children: _jsx(InputWrapper, { children: _jsx(Input, { placeholder: "M\xE3 ch\u1EC9 g\u1ED3m s\u1ED1", style: { width: 200 } }) }) }), _jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 1 }, children: _jsx(FormItem, { name: "motionTest", controlAfterInit: true, initialValue: "1234134", children: _jsx(InputWrapper, { children: _jsx(Input, { placeholder: "Motion Test", style: { width: 200 } }) }) }) }), _jsx(FormList, { initialValues: [
28
+ {
29
+ el: "sdfsdf",
30
+ d: { child: "Test Child" }
31
+ }
32
+ ], name: "arr", children: (fields, { add, remove, move }) => _jsxs("div", { children: [fields.map((field, index) => _jsxs("div", { style: { marginBottom: 8 }, children: [_jsx(FormItem, { name: `${field.name}.el`, initialValue: "Ch\xE9m gi\xF3", children: _jsx(InputWrapper, { children: _jsx(Input, { placeholder: "Item value", style: { width: 200 } }) }) }), _jsx(FormItem, { name: `${field.name}.d.child`, initialValue: "Con c\u1EE7a item", children: _jsx(InputWrapper, { children: _jsx(Input, { placeholder: "Item value", style: { width: 200 } }) }) }), _jsx(Button, { onClick: () => remove({ index }), style: { marginLeft: 8 }, children: "Remove" }), index > 0 && _jsx(Button, { onClick: () => move({ from: index, to: index - 1 }), style: { marginLeft: 8 }, children: "Move Up" }), index < fields.length - 1 && _jsx(Button, { onClick: () => move({ from: index, to: index + 1 }), style: { marginLeft: 8 }, children: "Move Down" })] }, field.key)), _jsx(Button, { onClick: () => add(fields.length), children: "Add Item" })] }) }), _jsx(Button, { onClick: () => {
33
+ form == null ? void 0 : form.setFieldValue("arr", [
34
+ { el: "Set Item 1" },
35
+ { el: "Set Item 2" },
36
+ { el: "Set Item 3" }
37
+ ]);
38
+ }, children: "Test set array list value" }), _jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 1.5 }, children: _jsx(Form.Item, { valuePropName: "checked", getValueFromEvent: (_, checked) => checked, name: "checkControlledAfterInit", controlAfterInit: true, initialValue: true, hidden: true, children: _jsx(Checkbox, {}) }) }), _jsx(Button, { onClick: () => {
39
+ const current = form == null ? void 0 : form.getFieldValue("checkControlledAfterInit");
40
+ form == null ? void 0 : form.setFieldValue("checkControlledAfterInit", !current);
41
+ }, children: "Toggle" }), _jsx(Button, { htmlType: "submit", children: "Submit" }), _jsx(Button, { onClick: () => {
42
+ var _a;
43
+ (_a = form == null ? void 0 : form.resetFields) == null ? void 0 : _a.call(form);
44
+ }, children: "Reset" })] });
45
+ }
46
+ var stdin_default = CommonTest;
47
+ export {
48
+ stdin_default as default
49
+ };
@@ -0,0 +1,3 @@
1
+ type Props = {};
2
+ declare function TestWrapperFormItem({}: Props): import("react/jsx-runtime").JSX.Element;
3
+ export default TestWrapperFormItem;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Col, Input, Row } from "antd";
3
+ import { useToggle } from "minh-custom-hooks-release";
4
+ import Form from "../providers/Form";
5
+ function TestWrapperFormItem({}) {
6
+ const { state, toggle } = useToggle(false);
7
+ return _jsxs(Form, { onFinish: (values, all) => {
8
+ }, formName: "testWrapper", children: [_jsx(Row, { children: _jsx(Form.Item, { hidden: !state, name: "test", collectOnHidden: false, initialValue: "test", Component: Col, xs: 12, children: _jsx(Input, {}) }) }), _jsx(Button, { htmlType: "submit", children: "Submit" }), _jsx(Button, { onClick: toggle, children: state ? "Off" : "On" })] });
9
+ }
10
+ var stdin_default = TestWrapperFormItem;
11
+ export {
12
+ stdin_default as default
13
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-manage",
3
- "version": "1.0.8-beta.23",
3
+ "version": "1.0.8-beta.24",
4
4
  "description": "Lightweight React form management with list and listener support.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/App.tsx CHANGED
@@ -1,13 +1,9 @@
1
- import { Checkbox } from "@mui/material";
2
- import { Button, Input } from "antd";
1
+ import { Input } from "antd";
3
2
  import { motion } from "framer-motion";
4
- import { useEffect } from "react";
5
- import FormItem from "./components/Form/FormItem";
6
- import FormList from "./components/Form/FormList";
7
- import InputWrapper from "./components/Form/InputWrapper";
8
- import Form, { useForm } from "./providers/Form";
9
- import TestDialog from "./test/TestDialog";
3
+ import Form from "./providers/Form";
4
+ import CommonTest from "./test/CommonTest";
10
5
  import TestListener from "./test/TestListener";
6
+ import TestWrapperFormItem from "./test/TestWrapperFormItem";
11
7
 
12
8
  import { Form as AntdForm } from "antd";
13
9
 
@@ -18,17 +14,6 @@ function TestFormWatch() {
18
14
  }
19
15
 
20
16
  const App = () => {
21
- const [form] = useForm("form1");
22
-
23
- const watchCheckBox = Form.useWatch("checkControlledAfterInit", "form1");
24
-
25
- useEffect(() => {
26
- if (form) {
27
- // setTimeout(() => {
28
- // form.setFieldValue("TestData", "HelloWorld");
29
- // }, 500);
30
- }
31
- }, [form]);
32
17
  return (
33
18
  <div>
34
19
  <AntdForm>
@@ -45,167 +30,13 @@ const App = () => {
45
30
  </AntdForm>
46
31
 
47
32
  {/* Hidden Test */}
48
- <Form
49
- initialValues={{
50
- TestData: "",
51
- numericCode: "",
52
- // arr: [{ el: "Item 1" }, { el: "Item 2" }],
53
- }}
54
- onFinish={(values) => {
55
- console.log(values);
56
- }}
57
- formName={"form1"}
58
- // hidden
59
- >
60
- <FormItem
61
- name={"username"}
62
- rules={[
63
- {
64
- required: true,
65
- message: "Test",
66
- },
67
- ]}
68
- initialValue={"283746"}
69
- // hidden
70
- >
71
- <InputWrapper>
72
- <Input />
73
- </InputWrapper>
74
- </FormItem>
75
-
76
- {/* Numberic test */}
77
- <FormItem
78
- name={"numericCode"}
79
- rules={[
80
- { required: true, message: "Vui lòng nhập mã" },
81
- {
82
- pattern: /^\d+$/,
83
- message: "Chỉ cho phép chuỗi số",
84
- },
85
- ]}
86
- >
87
- <InputWrapper>
88
- <Input placeholder="Mã chỉ gồm số" style={{ width: 200 }} />
89
- </InputWrapper>
90
- </FormItem>
91
-
92
- {/* Motion Test */}
93
- <motion.div
94
- initial={{ opacity: 0 }}
95
- animate={{ opacity: 1 }}
96
- exit={{ opacity: 0 }}
97
- transition={{ duration: 1 }}
98
- >
99
- <FormItem name="motionTest" controlAfterInit initialValue={"1234134"}>
100
- <InputWrapper>
101
- <Input placeholder="Motion Test" style={{ width: 200 }} />
102
- </InputWrapper>
103
- </FormItem>
104
- </motion.div>
105
- <FormList
106
- initialValues={[
107
- {
108
- el: "sdfsdf",
109
- d: { child: "Test Child" },
110
- },
111
- ]}
112
- name="arr"
113
- >
114
- {(fields, { add, remove, move }) => (
115
- <div>
116
- {fields.map((field, index) => (
117
- <div key={field.key} style={{ marginBottom: 8 }}>
118
- <FormItem name={`${field.name}.el`} initialValue={"Chém gió"}>
119
- <InputWrapper>
120
- <Input placeholder="Item value" style={{ width: 200 }} />
121
- </InputWrapper>
122
- </FormItem>
123
- <FormItem
124
- name={`${field.name}.d.child`}
125
- initialValue={"Con của item"}
126
- >
127
- <InputWrapper>
128
- <Input placeholder="Item value" style={{ width: 200 }} />
129
- </InputWrapper>
130
- </FormItem>
131
- <Button
132
- onClick={() => remove({ index })}
133
- style={{ marginLeft: 8 }}
134
- >
135
- Remove
136
- </Button>
137
- {index > 0 && (
138
- <Button
139
- onClick={() => move({ from: index, to: index - 1 })}
140
- style={{ marginLeft: 8 }}
141
- >
142
- Move Up
143
- </Button>
144
- )}
145
- {index < fields.length - 1 && (
146
- <Button
147
- onClick={() => move({ from: index, to: index + 1 })}
148
- style={{ marginLeft: 8 }}
149
- >
150
- Move Down
151
- </Button>
152
- )}
153
- </div>
154
- ))}
155
- <Button onClick={() => add(fields.length)}>Add Item</Button>
156
- </div>
157
- )}
158
- </FormList>
159
- <TestDialog />
160
- <Button
161
- onClick={() => {
162
- form?.setFieldValue("arr", [
163
- { el: "Set Item 1" },
164
- { el: "Set Item 2" },
165
- { el: "Set Item 3" },
166
- ]);
167
- }}
168
- >
169
- Test set array list value
170
- </Button>
171
- <motion.div
172
- initial={{ opacity: 0 }}
173
- animate={{ opacity: 1 }}
174
- exit={{ opacity: 0 }}
175
- transition={{ duration: 1.5 }}
176
- >
177
- <Form.Item
178
- valuePropName="checked"
179
- getValueFromEvent={(_, checked) => checked}
180
- name="checkControlledAfterInit"
181
- controlAfterInit={true}
182
- initialValue={true}
183
- hidden
184
- >
185
- <Checkbox />
186
- </Form.Item>
187
- </motion.div>
188
- <TestFormWatch />
189
- <Button
190
- onClick={() => {
191
- const current = form?.getFieldValue("checkControlledAfterInit");
192
- console.log("Toggle controlled after init: ", current);
193
- form?.setFieldValue("checkControlledAfterInit", !current);
194
- }}
195
- >
196
- Toggle
197
- </Button>
198
- <Button htmlType="submit">Submit</Button>
199
- <Button
200
- onClick={() => {
201
- form?.resetFields?.();
202
- }}
203
- >
204
- Reset
205
- </Button>
206
- </Form>
33
+ <CommonTest />
34
+ {/* <Form formName="TestDialog" >
35
+ <TestDialog />
36
+ </Form> */}
207
37
 
208
38
  <TestListener />
39
+ <TestWrapperFormItem />
209
40
  </div>
210
41
  );
211
42
  };
@@ -38,7 +38,7 @@ const FormCleanUp = () => {
38
38
  });
39
39
  } else {
40
40
  if (c.type === "array") {
41
- console.log(c);
41
+ // console.log(c);
42
42
  clearArrItem(c.formName, c.name);
43
43
  } else {
44
44
  }
@@ -1,10 +1,11 @@
1
- import type { ReactElement } from "react";
2
- import { cloneElement, Fragment, useRef, useState } from "react";
1
+ import { isNil } from "lodash";
2
+ import type { ComponentProps, FC, ReactElement } from "react";
3
+ import { cloneElement, Fragment, useEffect, useRef, useState } from "react";
3
4
  import { v4 } from "uuid";
4
5
  import useFormItemControl from "../../hooks/useFormItemControl";
5
6
  import type { ValidationRule } from "../../types/public";
6
7
 
7
- export interface FormItemProps {
8
+ export type BaseFormItemProps = {
8
9
  children: ReactElement<any>;
9
10
  name: string;
10
11
  formName?: string;
@@ -16,10 +17,129 @@ export interface FormItemProps {
16
17
  controlAfterInit?: boolean;
17
18
  hidden?: boolean;
18
19
  collectOnHidden?: boolean;
20
+ };
21
+
22
+ export type FormItemProps<TCustomWrapper extends FC<any> | undefined> =
23
+ BaseFormItemProps &
24
+ (
25
+ | {
26
+ Component?: undefined;
27
+ }
28
+ | ({
29
+ Component: TCustomWrapper;
30
+ } & Omit<ComponentProps<TCustomWrapper>, keyof BaseFormItemProps>)
31
+ );
32
+
33
+ function BaseFormItem({
34
+ children,
35
+
36
+ name,
37
+ elementRef: elRef,
38
+
39
+ valuePropName,
40
+ getValueFromEvent,
41
+ onChange,
42
+ value,
43
+ isDirty,
44
+ errors,
45
+ onFocus,
46
+ state,
47
+ submitState,
48
+ isTouched,
49
+ }: any) {
50
+ useEffect(() => {
51
+ console.log("Base Form Item: ", { name, value });
52
+ }, [value]);
53
+ return cloneElement(children, {
54
+ name,
55
+ ref: elRef,
56
+ [valuePropName]: value,
57
+ onChange: (...args: any[]) => {
58
+ let val = args[0];
59
+ if (getValueFromEvent && typeof getValueFromEvent === "function") {
60
+ val = getValueFromEvent(...args);
61
+ } else {
62
+ const e = args[0];
63
+ if (e && e.target) {
64
+ val = e.target.value;
65
+ }
66
+ }
67
+ onChange(val);
68
+ },
69
+ // onFocus: () => {
70
+ // setIsTouched(true);
71
+ // },
72
+ // isTouched: isTouched,
73
+ isDirty: isDirty,
74
+ // errors: errors,
75
+ // formState,
76
+
77
+ errors,
78
+ onFocus,
79
+ validateState: state,
80
+ submitState,
81
+ isTouched: isTouched,
82
+ } as any);
19
83
  }
20
84
 
21
- export default function FormItem({
85
+ function FormItemWithWrapper<TComponent extends FC<any>>({
22
86
  children,
87
+
88
+ name,
89
+ elementRef: elRef,
90
+
91
+ valuePropName,
92
+ getValueFromEvent,
93
+ onChange,
94
+ value,
95
+ isDirty,
96
+ errors,
97
+ onFocus,
98
+ state,
99
+ submitState,
100
+ isTouched,
101
+ Component,
102
+ ...props
103
+ }: any) {
104
+ return (
105
+ <Component {...props}>
106
+ {cloneElement(children, {
107
+ name,
108
+ ref: elRef,
109
+ [valuePropName]: value,
110
+ onChange: (...args: any[]) => {
111
+ let val = args[0];
112
+ if (getValueFromEvent && typeof getValueFromEvent === "function") {
113
+ val = getValueFromEvent(...args);
114
+ } else {
115
+ const e = args[0];
116
+ if (e && e.target) {
117
+ val = e.target.value;
118
+ }
119
+ }
120
+ onChange(val);
121
+ },
122
+ // onFocus: () => {
123
+ // setIsTouched(true);
124
+ // },
125
+ // isTouched: isTouched,
126
+ isDirty: isDirty,
127
+ // errors: errors,
128
+ // formState,
129
+
130
+ errors,
131
+ onFocus,
132
+ validateState: state,
133
+ submitState,
134
+ isTouched: isTouched,
135
+ } as any)}
136
+ </Component>
137
+ );
138
+ }
139
+
140
+ export default function FormItem<TCustomWrapper extends FC<any> | undefined>({
141
+ children,
142
+ Component,
23
143
  name,
24
144
  formName,
25
145
  initialValue,
@@ -30,10 +150,12 @@ export default function FormItem({
30
150
  controlAfterInit = false,
31
151
  hidden,
32
152
  collectOnHidden,
33
- }: FormItemProps) {
153
+ ...props
154
+ }: FormItemProps<TCustomWrapper>) {
155
+ const [formItemId] = useState(externalFormItemId ?? v4());
156
+
34
157
  const elRef = useRef<any>(null);
35
158
 
36
- const [formItemId] = useState(externalFormItemId ?? v4());
37
159
  const {
38
160
  value,
39
161
  onChange,
@@ -54,60 +176,52 @@ export default function FormItem({
54
176
  hidden,
55
177
  collectOnHidden,
56
178
  });
57
- // console.log("re-render", formName, name);
58
-
59
- // useEffect(() => {
60
- // console.log({ value });
61
- // }, [value]);
62
179
 
63
- // useEffect(() => {
64
- // console.log("isInitied changed: ", {
65
- // isInitied,
66
- // name,
67
- // value,
68
- // controlAfterInit,
69
- // });
70
- // }, [isInitied]);
71
180
  return (
72
181
  <Fragment
73
- key={`control-after-init-${Boolean(controlAfterInit && isInitied) ? "1" : "0"}-${formItemId}`}
182
+ key={`control-after-init-${Boolean(controlAfterInit && isInitied) ? "1" : "0"}-${externalFormItemId}`}
74
183
  >
75
- {!hidden && children
76
- ? cloneElement(children, {
77
- name,
78
- // ref: inputRef,
79
- [valuePropName]: value,
80
- onChange: (...args: any[]) => {
81
- let val = args[0];
82
- if (
83
- getValueFromEvent &&
84
- typeof getValueFromEvent === "function"
85
- ) {
86
- val = getValueFromEvent(...args);
87
- } else {
88
- const e = args[0];
89
- if (e && e.target) {
90
- val = e.target.value;
91
- }
92
- }
93
- onChange(val);
94
- },
95
- // onFocus: () => {
96
- // setIsTouched(true);
97
- // },
98
- // isTouched: isTouched,
99
- isDirty: isDirty,
100
- // errors: errors,
101
- // formState,
102
-
103
- errors,
104
- onFocus,
105
- validateState: state,
106
- ref: elRef,
107
- submitState,
108
- isTouched: isTouched,
109
- } as any)
110
- : null}
184
+ {!hidden && children ? (
185
+ isNil(Component) ? (
186
+ <BaseFormItem
187
+ elementRef={elRef}
188
+ name={name}
189
+ valuePropName={valuePropName}
190
+ getValueFromEvent={getValueFromEvent}
191
+ value={value}
192
+ onChange={onChange}
193
+ errors={errors}
194
+ state={state}
195
+ onFocus={onFocus}
196
+ isDirty={isDirty}
197
+ submitState={submitState}
198
+ isTouched={isTouched}
199
+ formItemId={formItemId}
200
+ >
201
+ {children}
202
+ </BaseFormItem>
203
+ ) : (
204
+ <FormItemWithWrapper
205
+ elementRef={elRef}
206
+ name={name}
207
+ valuePropName={valuePropName}
208
+ getValueFromEvent={getValueFromEvent}
209
+ value={value}
210
+ onChange={onChange}
211
+ errors={errors}
212
+ state={state}
213
+ onFocus={onFocus}
214
+ isDirty={isDirty}
215
+ submitState={submitState}
216
+ isTouched={isTouched}
217
+ formItemId={formItemId}
218
+ Component={Component}
219
+ {...props}
220
+ >
221
+ {children}
222
+ </FormItemWithWrapper>
223
+ )
224
+ ) : null}
111
225
  </Fragment>
112
226
  );
113
227
  }
@@ -1,6 +1,6 @@
1
1
  import { get, has, isNil } from "lodash";
2
2
  import { useTaskEffect } from "minh-custom-hooks-release";
3
- import { useEffect, useMemo, type RefObject } from "react";
3
+ import { useEffect, useLayoutEffect, useMemo, type RefObject } from "react";
4
4
  import { useShallow } from "zustand/react/shallow"; // Import useShallow
5
5
  import {
6
6
  IS_ALPHABET_STRING_AND_NUMBER_REGEX,
@@ -75,6 +75,9 @@ export default function useFormItemControl<T = any>({
75
75
  // getFormState,
76
76
  isStateInitied,
77
77
  submitState,
78
+ getListener,
79
+ revokeListener,
80
+ clearObjKeyItem,
78
81
  } = useFormStore(
79
82
  useShallow((state) => {
80
83
  return {
@@ -94,6 +97,9 @@ export default function useFormItemControl<T = any>({
94
97
  state.formStates?.[
95
98
  formName || form?.formName || contextForm?.formName
96
99
  ]?.submitState,
100
+ getListener: state.getListener,
101
+ revokeListener: state.revokeListener,
102
+ clearObjKeyItem: state.clearObjKeyItem,
97
103
  };
98
104
  }),
99
105
  );
@@ -566,7 +572,8 @@ export default function useFormItemControl<T = any>({
566
572
  }
567
573
  }, [isStateInitied]);
568
574
 
569
- useEffect(() => {
575
+ useLayoutEffect(() => {
576
+ console.log("Component mount: ", name, formItemId, listener);
570
577
  if (!listener) {
571
578
  setListener({
572
579
  onChange,
@@ -584,12 +591,12 @@ export default function useFormItemControl<T = any>({
584
591
  }
585
592
 
586
593
  return () => {
587
- if (listener) {
588
- setCleanUpStack({
589
- name: listener.name,
590
- itemKey: listener.formItemId,
591
- });
592
- }
594
+ revokeListener(formItemId, (listener, same) => {
595
+ if (!same.length) {
596
+ console.log("Trigger after revoke: ", listener, same);
597
+ clearObjKeyItem(listener.formName, listener.name);
598
+ }
599
+ });
593
600
  };
594
601
 
595
602
  // return () => {
@@ -598,15 +605,15 @@ export default function useFormItemControl<T = any>({
598
605
  // };
599
606
  }, []);
600
607
 
601
- // useEffect(() => {
602
- // if (listener) {
603
- // setListener({
604
- // formItemId,
605
- // hidden,
606
- // collectOnHidden,
607
- // });
608
- // }
609
- // }, [hidden, collectOnHidden]);
608
+ useEffect(() => {
609
+ if (listener) {
610
+ setListener({
611
+ formItemId,
612
+ hidden,
613
+ collectOnHidden,
614
+ });
615
+ }
616
+ }, [hidden, collectOnHidden]);
610
617
 
611
618
  useEffect(() => {
612
619
  if (listener) {
@@ -467,6 +467,8 @@ export default function Form<T = any>({
467
467
  formName,
468
468
  }}
469
469
  >
470
+ <FormCleanUp />
471
+
470
472
  {FormElement ? (
471
473
  <FormElement
472
474
  onSubmit={(e) => {
@@ -492,7 +494,6 @@ export default function Form<T = any>({
492
494
  {children}
493
495
  </form>
494
496
  )}
495
- <FormCleanUp />
496
497
  </FormContext.Provider>
497
498
  );
498
499
  }
@@ -0,0 +1,177 @@
1
+ import { Button, Checkbox, Input } from "antd";
2
+ import { motion } from "framer-motion";
3
+ import FormItem from "../components/Form/FormItem";
4
+ import FormList from "../components/Form/FormList";
5
+ import InputWrapper from "../components/Form/InputWrapper";
6
+ import Form from "../providers/Form";
7
+
8
+ type Props = {};
9
+
10
+ function CommonTest({}: Props) {
11
+ const [form] = Form.useForm("form1");
12
+
13
+ const watchCheckBox = Form.useWatch("checkControlledAfterInit", "form1");
14
+
15
+ return (
16
+ <Form
17
+ initialValues={{
18
+ TestData: "",
19
+ numericCode: "",
20
+ // arr: [{ el: "Item 1" }, { el: "Item 2" }],
21
+ }}
22
+ onFinish={(values) => {
23
+ console.log(values);
24
+ }}
25
+ formName={"form1"}
26
+ // hidden
27
+ >
28
+ <FormItem
29
+ name={"username"}
30
+ rules={[
31
+ {
32
+ required: true,
33
+ message: "Test",
34
+ },
35
+ ]}
36
+ initialValue={"283746"}
37
+ // hidden
38
+ >
39
+ <InputWrapper>
40
+ <Input />
41
+ </InputWrapper>
42
+ </FormItem>
43
+
44
+ {/* Numberic test */}
45
+ <FormItem
46
+ name={"numericCode"}
47
+ rules={[
48
+ { required: true, message: "Vui lòng nhập mã" },
49
+ {
50
+ pattern: /^\d+$/,
51
+ message: "Chỉ cho phép chuỗi số",
52
+ },
53
+ ]}
54
+ >
55
+ <InputWrapper>
56
+ <Input placeholder="Mã chỉ gồm số" style={{ width: 200 }} />
57
+ </InputWrapper>
58
+ </FormItem>
59
+
60
+ {/* Motion Test */}
61
+ <motion.div
62
+ initial={{ opacity: 0 }}
63
+ animate={{ opacity: 1 }}
64
+ exit={{ opacity: 0 }}
65
+ transition={{ duration: 1 }}
66
+ >
67
+ <FormItem name="motionTest" controlAfterInit initialValue={"1234134"}>
68
+ <InputWrapper>
69
+ <Input placeholder="Motion Test" style={{ width: 200 }} />
70
+ </InputWrapper>
71
+ </FormItem>
72
+ </motion.div>
73
+ <FormList
74
+ initialValues={[
75
+ {
76
+ el: "sdfsdf",
77
+ d: { child: "Test Child" },
78
+ },
79
+ ]}
80
+ name="arr"
81
+ >
82
+ {(fields, { add, remove, move }) => (
83
+ <div>
84
+ {fields.map((field, index) => (
85
+ <div key={field.key} style={{ marginBottom: 8 }}>
86
+ <FormItem name={`${field.name}.el`} initialValue={"Chém gió"}>
87
+ <InputWrapper>
88
+ <Input placeholder="Item value" style={{ width: 200 }} />
89
+ </InputWrapper>
90
+ </FormItem>
91
+ <FormItem
92
+ name={`${field.name}.d.child`}
93
+ initialValue={"Con của item"}
94
+ >
95
+ <InputWrapper>
96
+ <Input placeholder="Item value" style={{ width: 200 }} />
97
+ </InputWrapper>
98
+ </FormItem>
99
+ <Button
100
+ onClick={() => remove({ index })}
101
+ style={{ marginLeft: 8 }}
102
+ >
103
+ Remove
104
+ </Button>
105
+ {index > 0 && (
106
+ <Button
107
+ onClick={() => move({ from: index, to: index - 1 })}
108
+ style={{ marginLeft: 8 }}
109
+ >
110
+ Move Up
111
+ </Button>
112
+ )}
113
+ {index < fields.length - 1 && (
114
+ <Button
115
+ onClick={() => move({ from: index, to: index + 1 })}
116
+ style={{ marginLeft: 8 }}
117
+ >
118
+ Move Down
119
+ </Button>
120
+ )}
121
+ </div>
122
+ ))}
123
+ <Button onClick={() => add(fields.length)}>Add Item</Button>
124
+ </div>
125
+ )}
126
+ </FormList>
127
+ <Button
128
+ onClick={() => {
129
+ form?.setFieldValue("arr", [
130
+ { el: "Set Item 1" },
131
+ { el: "Set Item 2" },
132
+ { el: "Set Item 3" },
133
+ ]);
134
+ }}
135
+ >
136
+ Test set array list value
137
+ </Button>
138
+ <motion.div
139
+ initial={{ opacity: 0 }}
140
+ animate={{ opacity: 1 }}
141
+ exit={{ opacity: 0 }}
142
+ transition={{ duration: 1.5 }}
143
+ >
144
+ <Form.Item
145
+ valuePropName="checked"
146
+ getValueFromEvent={(_, checked) => checked}
147
+ name="checkControlledAfterInit"
148
+ controlAfterInit={true}
149
+ initialValue={true}
150
+ hidden
151
+ >
152
+ <Checkbox />
153
+ </Form.Item>
154
+ </motion.div>
155
+ {/* <TestFormWatch /> */}
156
+ <Button
157
+ onClick={() => {
158
+ const current = form?.getFieldValue("checkControlledAfterInit");
159
+ console.log("Toggle controlled after init: ", current);
160
+ form?.setFieldValue("checkControlledAfterInit", !current);
161
+ }}
162
+ >
163
+ Toggle
164
+ </Button>
165
+ <Button htmlType="submit">Submit</Button>
166
+ <Button
167
+ onClick={() => {
168
+ form?.resetFields?.();
169
+ }}
170
+ >
171
+ Reset
172
+ </Button>
173
+ </Form>
174
+ );
175
+ }
176
+
177
+ export default CommonTest;
@@ -0,0 +1,34 @@
1
+ import { Button, Col, Input, Row } from "antd";
2
+ import { useToggle } from "minh-custom-hooks-release";
3
+ import Form from "../providers/Form";
4
+
5
+ type Props = {};
6
+
7
+ function TestWrapperFormItem({}: Props) {
8
+ const { state, toggle } = useToggle(false);
9
+ return (
10
+ <Form
11
+ onFinish={(values, all) => {
12
+ console.log(values, all);
13
+ }}
14
+ formName="testWrapper"
15
+ >
16
+ <Row>
17
+ <Form.Item
18
+ hidden={!state}
19
+ name="test"
20
+ collectOnHidden={false}
21
+ initialValue={"test"}
22
+ Component={Col}
23
+ xs={12}
24
+ >
25
+ <Input />
26
+ </Form.Item>
27
+ </Row>
28
+ <Button htmlType="submit">Submit</Button>
29
+ <Button onClick={toggle}>{state ? "Off" : "On"}</Button>
30
+ </Form>
31
+ );
32
+ }
33
+
34
+ export default TestWrapperFormItem;