react-form-manage 1.0.8 → 1.1.0-beta.1

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/providers/Form.d.ts +30 -15
  3. package/dist/providers/Form.js +142 -25
  4. package/dist/stores/formStore.js +3 -0
  5. package/dist/test/testFormInstance/TestCase1_FormWithInstance.d.ts +14 -0
  6. package/dist/test/testFormInstance/TestCase1_FormWithInstance.js +21 -0
  7. package/dist/test/testFormInstance/TestCase2_AutoGenerateFormName.d.ts +14 -0
  8. package/dist/test/testFormInstance/TestCase2_AutoGenerateFormName.js +23 -0
  9. package/dist/test/testFormInstance/TestCase3_SafeFormMethods.d.ts +14 -0
  10. package/dist/test/testFormInstance/TestCase3_SafeFormMethods.js +66 -0
  11. package/dist/test/testFormInstance/TestCase4_UseFormStatusFlag.d.ts +15 -0
  12. package/dist/test/testFormInstance/TestCase4_UseFormStatusFlag.js +59 -0
  13. package/dist/test/testFormInstance/TestCase5_UseFormUtils.d.ts +1 -0
  14. package/dist/test/testFormInstance/TestCase5_UseFormUtils.js +65 -0
  15. package/dist/test/testFormInstance/TestCase6_FormWithoutWrapper.d.ts +15 -0
  16. package/dist/test/testFormInstance/TestCase6_FormWithoutWrapper.js +45 -0
  17. package/dist/test/testFormInstance/index.d.ts +12 -0
  18. package/dist/test/testFormInstance/index.js +49 -0
  19. package/dist/test/testSetValue/TestSetValueInEffect.d.ts +3 -0
  20. package/dist/test/testSetValue/TestSetValueInEffect.js +30 -0
  21. package/dist/types/public.d.ts +1 -1
  22. package/package.json +2 -1
  23. package/src/App.tsx +7 -7
  24. package/src/providers/Form.tsx +247 -28
  25. package/src/stores/formStore.ts +3 -1
  26. package/src/test/testFormInstance/TestCase1_FormWithInstance.tsx +112 -0
  27. package/src/test/testFormInstance/TestCase2_AutoGenerateFormName.tsx +139 -0
  28. package/src/test/testFormInstance/TestCase3_SafeFormMethods.tsx +203 -0
  29. package/src/test/testFormInstance/TestCase4_UseFormStatusFlag.tsx +252 -0
  30. package/src/test/testFormInstance/TestCase5_UseFormUtils.tsx +224 -0
  31. package/src/test/testFormInstance/TestCase6_FormWithoutWrapper.tsx +204 -0
  32. package/src/test/testFormInstance/index.tsx +116 -0
  33. package/src/test/testSetValue/TestSetValueInEffect.tsx +54 -0
  34. package/src/types/public.ts +1 -1
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Space, Typography, Card, Input, message } from "antd";
3
+ import { useState } from "react";
4
+ import Form from "../../providers/Form";
5
+ const { Title, Paragraph, Text } = Typography;
6
+ function ControlPanel() {
7
+ const { getFormInstance, setFieldValue, setFieldValues } = Form.useFormUtils();
8
+ const [targetFormName, setTargetFormName] = useState("form1");
9
+ const handleSetSingleValue = () => {
10
+ try {
11
+ setFieldValue(targetFormName, "username", "admin");
12
+ message.success(`Set username='admin' for ${targetFormName}`);
13
+ } catch (error) {
14
+ message.error(error.message);
15
+ }
16
+ };
17
+ const handleSetMultipleValues = () => {
18
+ try {
19
+ setFieldValues(targetFormName, {
20
+ username: "john_doe",
21
+ email: "john@example.com"
22
+ });
23
+ message.success(`Set multiple values for ${targetFormName}`);
24
+ } catch (error) {
25
+ message.error(error.message);
26
+ }
27
+ };
28
+ const handleGetFormInstance = () => {
29
+ try {
30
+ const formInstance = getFormInstance(targetFormName);
31
+ message.info("Check console for form instance details");
32
+ } catch (error) {
33
+ message.error(error.message);
34
+ }
35
+ };
36
+ const handleReset = () => {
37
+ try {
38
+ const formInstance = getFormInstance(targetFormName);
39
+ formInstance == null ? void 0 : formInstance.resetFields();
40
+ message.success(`Reset ${targetFormName}`);
41
+ } catch (error) {
42
+ message.error(error.message);
43
+ }
44
+ };
45
+ const handleSubmit = () => {
46
+ try {
47
+ const formInstance = getFormInstance(targetFormName);
48
+ formInstance == null ? void 0 : formInstance.submit();
49
+ message.info(`Submitting ${targetFormName}`);
50
+ } catch (error) {
51
+ message.error(error.message);
52
+ }
53
+ };
54
+ return _jsxs(Card, { title: "\u{1F3AE} Control Panel (useFormUtils)", style: { marginBottom: 24 }, extra: _jsxs(Space, { children: [_jsx(Text, { children: "Target Form:" }), _jsx(Input, { value: targetFormName, onChange: (e) => setTargetFormName(e.target.value), placeholder: "Enter form name", style: { width: 150 } })] }), children: [_jsxs(Space, { wrap: true, children: [_jsx(Button, { onClick: handleSetSingleValue, type: "primary", children: "Set Single Value" }), _jsx(Button, { onClick: handleSetMultipleValues, type: "primary", children: "Set Multiple Values" }), _jsx(Button, { onClick: handleGetFormInstance, children: "Get Form Instance" }), _jsx(Button, { onClick: handleReset, danger: true, children: "Reset Form" }), _jsx(Button, { onClick: handleSubmit, type: "dashed", children: "Submit Form" })] }), _jsx("div", { style: { marginTop: 16, padding: 12, background: "#f0f0f0" }, children: _jsx(Text, { type: "secondary", children: '\u{1F4A1} Tip: Change the target form name to "form1" or "form2" to control different forms' }) })] });
55
+ }
56
+ function TestCase5_UseFormUtils() {
57
+ return _jsxs("div", { style: { padding: 24 }, children: [_jsx(Title, { level: 3, children: "TestCase5: useFormUtils Hook" }), _jsxs(Paragraph, { children: [_jsx(Text, { strong: true, children: "New Hook:" }), " Form.useFormUtils() cho ph\xE9p thao t\xE1c v\u1EDBi form t\u1EEB b\xEAn ngo\xE0i"] }), _jsx(Paragraph, { children: _jsx(Text, { type: "secondary", children: "Hook n\xE0y cung c\u1EA5p c\xE1c utility methods \u0111\u1EC3 get/set form values t\u1EEB b\u1EA5t k\u1EF3 \u0111\xE2u trong app, kh\xF4ng c\u1EA7n ph\u1EA3i \u1EDF trong Form context. H\u1EEFu \xEDch cho global control panels, external triggers, ho\u1EB7c cross-component form manipulation." }) }), _jsx(ControlPanel, {}), _jsxs("div", { style: { display: "flex", gap: 24 }, children: [_jsx("div", { style: { flex: 1 }, children: _jsx(Card, { title: "Form 1 (formName='form1')", children: _jsxs(Form, { formName: "form1", initialValues: { username: "", email: "" }, onFinish: (values) => {
58
+ message.success(`Form 1 submitted: ${JSON.stringify(values)}`);
59
+ }, children: [_jsx(Form.Item, { name: "username", label: "Username", children: _jsx(Input, { placeholder: "Username" }) }), _jsx(Form.Item, { name: "email", label: "Email", children: _jsx(Input, { type: "email", placeholder: "Email" }) }), _jsx(Button, { type: "primary", htmlType: "submit", children: "Submit Form 1" })] }) }) }), _jsx("div", { style: { flex: 1 }, children: _jsx(Card, { title: "Form 2 (formName='form2')", children: _jsxs(Form, { formName: "form2", initialValues: { username: "", email: "" }, onFinish: (values) => {
60
+ message.success(`Form 2 submitted: ${JSON.stringify(values)}`);
61
+ }, children: [_jsx(Form.Item, { name: "username", label: "Username", children: _jsx(Input, { placeholder: "Username" }) }), _jsx(Form.Item, { name: "email", label: "Email", children: _jsx(Input, { type: "email", placeholder: "Email" }) }), _jsx(Button, { type: "primary", htmlType: "submit", children: "Submit Form 2" })] }) }) })] }), _jsxs("div", { style: { marginTop: 24, padding: 16, background: "#f5f5f5" }, children: [_jsx(Title, { level: 5, children: "Expected Results:" }), _jsxs("ul", { children: [_jsx("li", { children: '\u2705 Control panel starts with target "form1"' }), _jsx("li", { children: `\u2705 Click "Set Single Value" \u2192 Form 1's username field updates to "admin"` }), _jsx("li", { children: `\u2705 Click "Set Multiple Values" \u2192 Form 1's both fields update` }), _jsx("li", { children: '\u2705 Click "Get Form Instance" \u2192 Console logs form instance and current values' }), _jsx("li", { children: '\u2705 Change target to "form2" in input' }), _jsx("li", { children: `\u2705 Click "Set Single Value" \u2192 Form 2's username field updates` }), _jsx("li", { children: '\u2705 Click "Reset Form" \u2192 Target form resets to initialValues' }), _jsx("li", { children: '\u2705 Click "Submit Form" \u2192 Target form submits programmatically' }), _jsx("li", { children: '\u26A0\uFE0F Enter invalid formName \u2192 Error message shows "Form instance not found"' })] })] }), _jsxs("div", { style: { marginTop: 16, padding: 16, background: "#e6f7ff", border: "1px solid #91d5ff" }, children: [_jsx(Title, { level: 5, children: "Use Cases:" }), _jsxs("ul", { style: { marginBottom: 0 }, children: [_jsx("li", { children: "Global control panels \u0111\u1EC3 qu\u1EA3n l\xFD multiple forms" }), _jsx("li", { children: "External triggers (t\u1EEB API response, WebSocket, events...)" }), _jsx("li", { children: "Cross-component form manipulation" }), _jsx("li", { children: "Testing v\xE0 debugging tools" }), _jsx("li", { children: "Admin dashboards v\u1EDBi form management" })] })] })] });
62
+ }
63
+ export {
64
+ TestCase5_UseFormUtils as default
65
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * TestCase6: Form Without Wrapper Component
3
+ *
4
+ * Mô tả:
5
+ * - Test use case: Sử dụng form methods mà không cần render Form component
6
+ * - Form instance có thể được sử dụng độc lập để quản lý state
7
+ * - Hữu ích cho: custom form implementations, headless forms, non-standard UI
8
+ *
9
+ * Expected Behavior:
10
+ * - Form instance hoạt động như một state manager
11
+ * - Có thể set/get values mà không cần Form component
12
+ * - Có thể dùng Form.useWatch để reactive update
13
+ * - Warning được log vì không có Form component để validate
14
+ */
15
+ export default function TestCase6_FormWithoutWrapper(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Card, Input, Space, Typography } from "antd";
3
+ import { useState } from "react";
4
+ import Form from "../../providers/Form";
5
+ const { Title, Paragraph, Text } = Typography;
6
+ function TestCase6_FormWithoutWrapper() {
7
+ const [form] = Form.useForm();
8
+ const firstName = Form.useWatch("firstName", form);
9
+ const lastName = Form.useWatch("lastName", form);
10
+ const age = Form.useWatch("age", form);
11
+ const [manualFirstName, setManualFirstName] = useState("");
12
+ const [manualLastName, setManualLastName] = useState("");
13
+ const [manualAge, setManualAge] = useState("");
14
+ const handleSetFromManualInputs = () => {
15
+ form.setFieldValue("firstName", manualFirstName);
16
+ form.setFieldValue("lastName", manualLastName);
17
+ form.setFieldValue("age", Number(manualAge) || 0);
18
+ };
19
+ const handleSetBatch = () => {
20
+ form.setFieldValues({
21
+ firstName: "John",
22
+ lastName: "Doe",
23
+ age: 30
24
+ });
25
+ };
26
+ const handleGetValues = () => {
27
+ const values = {
28
+ firstName: form.getFieldValue("firstName"),
29
+ lastName: form.getFieldValue("lastName"),
30
+ age: form.getFieldValue("age")
31
+ };
32
+ alert(JSON.stringify(values, null, 2));
33
+ };
34
+ const handleReset = () => {
35
+ form.resetFields();
36
+ };
37
+ const handleSubmit = () => {
38
+ form.submit();
39
+ };
40
+ const fullName = `${firstName || ""} ${lastName || ""}`.trim() || "(empty)";
41
+ return _jsxs("div", { style: { padding: 24 }, children: [_jsx(Title, { level: 3, children: "TestCase6: Form Without Wrapper Component" }), _jsxs(Paragraph, { children: [_jsx(Text, { strong: true, children: "Use Case:" }), " S\u1EED d\u1EE5ng form instance nh\u01B0 state manager m\xE0 kh\xF4ng c\u1EA7n Form component"] }), _jsx(Paragraph, { children: _jsx(Text, { type: "secondary", children: "Form instance c\xF3 th\u1EC3 ho\u1EA1t \u0111\u1ED9ng \u0111\u1ED9c l\u1EADp nh\u01B0 m\u1ED9t state manager. H\u1EEFu \xEDch cho custom form implementations, headless forms, ho\u1EB7c khi b\u1EA1n mu\u1ED1n t\u1EF1 build UI m\xE0 kh\xF4ng d\xF9ng Form component wrapper." }) }), _jsxs("div", { style: { display: "flex", gap: 24 }, children: [_jsx(Card, { title: "\u{1F4DD} Manual Inputs (Not Connected to Form)", style: { flex: 1 }, children: _jsxs(Space, { direction: "vertical", style: { width: "100%" }, children: [_jsxs("div", { children: [_jsx(Text, { strong: true, children: "First Name:" }), _jsx(Input, { value: manualFirstName, onChange: (e) => setManualFirstName(e.target.value), placeholder: "Enter first name" })] }), _jsxs("div", { children: [_jsx(Text, { strong: true, children: "Last Name:" }), _jsx(Input, { value: manualLastName, onChange: (e) => setManualLastName(e.target.value), placeholder: "Enter last name" })] }), _jsxs("div", { children: [_jsx(Text, { strong: true, children: "Age:" }), _jsx(Input, { type: "number", value: manualAge, onChange: (e) => setManualAge(e.target.value), placeholder: "Enter age" })] }), _jsx(Button, { type: "primary", onClick: handleSetFromManualInputs, block: true, children: "Set Form Values" })] }) }), _jsx(Card, { title: "\u{1F441}\uFE0F Form State (via useWatch)", style: { flex: 1 }, children: _jsxs(Space, { direction: "vertical", style: { width: "100%" }, children: [_jsxs("div", { children: [_jsx(Text, { strong: true, children: "First Name: " }), _jsx(Text, { code: true, children: firstName || "(empty)" })] }), _jsxs("div", { children: [_jsx(Text, { strong: true, children: "Last Name: " }), _jsx(Text, { code: true, children: lastName || "(empty)" })] }), _jsxs("div", { children: [_jsx(Text, { strong: true, children: "Age: " }), _jsx(Text, { code: true, children: age != null ? age : "(empty)" })] }), _jsxs("div", { style: { marginTop: 16, padding: 12, background: "#f0f0f0" }, children: [_jsx(Text, { strong: true, children: "Computed Full Name: " }), _jsx(Text, { style: { fontSize: 16 }, children: fullName })] })] }) })] }), _jsxs(Card, { title: "\u{1F3AE} Form Controls", style: { marginTop: 24 }, children: [_jsxs(Space, { wrap: true, children: [_jsx(Button, { onClick: handleSetBatch, type: "primary", children: "Set Batch Values (John Doe, 30)" }), _jsx(Button, { onClick: handleGetValues, children: "Get All Values (Alert)" }), _jsx(Button, { onClick: handleReset, danger: true, children: "Reset Form" }), _jsx(Button, { onClick: handleSubmit, type: "dashed", children: "Submit Form (Will Log Warning)" })] }), _jsxs("div", { style: { marginTop: 16, padding: 12, background: "#fff7e6", border: "1px solid #ffd591" }, children: [_jsx(Text, { strong: true, children: "\u26A0\uFE0F Note: " }), _jsx(Text, { children: "Kh\xF4ng c\xF3 Form component \u0111\u01B0\u1EE3c render. Form instance ho\u1EA1t \u0111\u1ED9ng nh\u01B0 pure state manager. Submit s\u1EBD log warning v\xEC kh\xF4ng c\xF3 onFinish handler." })] })] }), _jsxs("div", { style: { marginTop: 24, padding: 16, background: "#f5f5f5" }, children: [_jsx(Title, { level: 5, children: "Expected Results:" }), _jsxs("ul", { children: [_jsx("li", { children: "\u2705 Type in manual inputs \u2192 Values NOT auto-sync to form state" }), _jsx("li", { children: '\u2705 Click "Set Form Values" \u2192 Form state updates, display panel shows new values' }), _jsx("li", { children: "\u2705 Display panel updates reactively via useWatch" }), _jsx("li", { children: '\u2705 Click "Set Batch Values" \u2192 All fields update at once' }), _jsx("li", { children: '\u2705 Click "Get All Values" \u2192 Alert shows current form values' }), _jsx("li", { children: '\u2705 Click "Reset Form" \u2192 All fields reset (empty)' }), _jsx("li", { children: "\u2705 Computed Full Name updates based on firstName + lastName" }), _jsx("li", { children: '\u26A0\uFE0F Click "Submit Form" \u2192 Console shows warning (no Form component mounted)' })] })] }), _jsxs("div", { style: { marginTop: 16, padding: 16, background: "#e6f7ff", border: "1px solid #91d5ff" }, children: [_jsx(Title, { level: 5, children: "Use Cases:" }), _jsxs("ul", { style: { marginBottom: 0 }, children: [_jsxs("li", { children: [_jsx("strong", { children: "Headless Forms:" }), " Build custom UI m\xE0 kh\xF4ng b\u1ECB constraint b\u1EDFi Form component"] }), _jsxs("li", { children: [_jsx("strong", { children: "State Manager:" }), " S\u1EED d\u1EE5ng nh\u01B0 lightweight state manager v\u1EDBi validation support"] }), _jsxs("li", { children: [_jsx("strong", { children: "Multi-step Forms:" }), " Qu\u1EA3n l\xFD form state across multiple steps/pages"] }), _jsxs("li", { children: [_jsx("strong", { children: "Form Wizards:" }), " Share form instance between different wizard steps"] }), _jsxs("li", { children: [_jsx("strong", { children: "Custom Integrations:" }), " Integrate v\u1EDBi third-party UI libraries"] })] })] })] });
42
+ }
43
+ export {
44
+ TestCase6_FormWithoutWrapper as default
45
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Form Instance Test Suite
3
+ *
4
+ * Test suite cho breaking changes và new features liên quan đến Form Instance:
5
+ * - Form component nhận prop "form" thay vì chỉ có "formName"
6
+ * - Auto-generate UUID cho formName
7
+ * - Safe form methods với proxy instance
8
+ * - useForm trả về [instance, isReady] tuple
9
+ * - useFormUtils hook mới
10
+ * - Form instance hoạt động độc lập như state manager
11
+ */
12
+ export default function FormInstanceTestSuite(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Tabs, Typography } from "antd";
3
+ import { useState } from "react";
4
+ import TestCase1_FormWithInstance from "./TestCase1_FormWithInstance";
5
+ import TestCase2_AutoGenerateFormName from "./TestCase2_AutoGenerateFormName";
6
+ import TestCase3_SafeFormMethods from "./TestCase3_SafeFormMethods";
7
+ import TestCase4_UseFormStatusFlag from "./TestCase4_UseFormStatusFlag";
8
+ import TestCase5_UseFormUtils from "./TestCase5_UseFormUtils";
9
+ import TestCase6_FormWithoutWrapper from "./TestCase6_FormWithoutWrapper";
10
+ const { Title, Paragraph, Text } = Typography;
11
+ function FormInstanceTestSuite() {
12
+ const [activeTab, setActiveTab] = useState("1");
13
+ const items = [
14
+ {
15
+ key: "1",
16
+ label: "TestCase1: Form with Instance",
17
+ children: _jsx(TestCase1_FormWithInstance, {})
18
+ },
19
+ {
20
+ key: "2",
21
+ label: "TestCase2: Auto-generate FormName",
22
+ children: _jsx(TestCase2_AutoGenerateFormName, {})
23
+ },
24
+ {
25
+ key: "3",
26
+ label: "TestCase3: Safe Form Methods",
27
+ children: _jsx(TestCase3_SafeFormMethods, {})
28
+ },
29
+ {
30
+ key: "4",
31
+ label: "TestCase4: useForm Status Flag",
32
+ children: _jsx(TestCase4_UseFormStatusFlag, {})
33
+ },
34
+ {
35
+ key: "5",
36
+ label: "TestCase5: useFormUtils Hook",
37
+ children: _jsx(TestCase5_UseFormUtils, {})
38
+ },
39
+ {
40
+ key: "6",
41
+ label: "TestCase6: Form Without Wrapper",
42
+ children: _jsx(TestCase6_FormWithoutWrapper, {})
43
+ }
44
+ ];
45
+ return _jsxs("div", { style: { padding: 24, maxWidth: 1400, margin: "0 auto" }, children: [_jsxs("div", { style: { marginBottom: 32 }, children: [_jsx(Title, { level: 2, children: "\u{1F9EA} Form Instance Test Suite" }), _jsx(Paragraph, { children: _jsx(Text, { children: "Test suite cho breaking changes v\xE0 new features trong version m\u1EDBi. M\u1ED7i test case cover m\u1ED9t aspect c\u1EE5 th\u1EC3 c\u1EE7a Form Instance behavior." }) })] }), _jsxs("div", { style: { marginBottom: 24, padding: 16, background: "#e6f7ff", border: "1px solid #91d5ff" }, children: [_jsx(Title, { level: 4, children: "\u{1F4CB} Breaking Changes & New Features:" }), _jsxs("ul", { style: { marginBottom: 0 }, children: [_jsxs("li", { children: [_jsx(Text, { strong: true, children: 'Form Prop "form":' }), " Form component c\xF3 th\u1EC3 nh\u1EADn ", _jsx(Text, { code: true, children: "form" }), " instance thay v\xEC ", _jsx(Text, { code: true, children: "formName" })] }), _jsxs("li", { children: [_jsx(Text, { strong: true, children: "Auto-generate FormName:" }), " Form.useForm() t\u1EF1 \u0111\u1ED9ng t\u1EA1o UUID n\u1EBFu kh\xF4ng truy\u1EC1n tham s\u1ED1"] }), _jsxs("li", { children: [_jsx(Text, { strong: true, children: "Safe Methods:" }), " Form methods c\xF3 th\u1EC3 g\u1ECDi an to\xE0n tr\u01B0\u1EDBc khi Form mount (proxy instance)"] }), _jsxs("li", { children: [_jsx(Text, { strong: true, children: "useForm Tuple:" }), " useForm() tr\u1EA3 v\u1EC1 ", _jsx(Text, { code: true, children: "[instance, isReady]" }), " thay v\xEC ch\u1EC9 ", _jsx(Text, { code: true, children: "[instance]" })] }), _jsxs("li", { children: [_jsx(Text, { strong: true, children: "useFormUtils Hook:" }), " Hook m\u1EDBi cho ph\xE9p thao t\xE1c form t\u1EEB b\xEAn ngo\xE0i component"] }), _jsxs("li", { children: [_jsx(Text, { strong: true, children: "Headless Forms:" }), " Form instance ho\u1EA1t \u0111\u1ED9ng \u0111\u1ED9c l\u1EADp nh\u01B0 state manager"] })] })] }), _jsx(Tabs, { activeKey: activeTab, onChange: setActiveTab, items, size: "large", type: "card" }), _jsxs("div", { style: { marginTop: 32, padding: 16, background: "#f5f5f5" }, children: [_jsx(Title, { level: 5, children: "\u{1F4A1} Testing Tips:" }), _jsxs("ul", { style: { marginBottom: 0 }, children: [_jsx("li", { children: "M\u1EDF Browser Console \u0111\u1EC3 xem detailed logs v\xE0 warnings" }), _jsx("li", { children: "M\u1ED7i test case c\xF3 Expected Results section \u0111\u1EC3 verify behavior" }), _jsx("li", { children: "Test cases c\xF3 th\u1EC3 test \u0111\u1ED9c l\u1EADp, kh\xF4ng \u1EA3nh h\u01B0\u1EDFng l\u1EABn nhau" }), _jsx("li", { children: "Ch\xFA \xFD c\xE1c warning messages khi form methods \u0111\u01B0\u1EE3c g\u1ECDi sai timing" }), _jsx("li", { children: "Use Cases sections gi\u1EA3i th\xEDch khi n\xE0o n\xEAn d\xF9ng feature \u0111\xF3" })] })] })] });
46
+ }
47
+ export {
48
+ FormInstanceTestSuite as default
49
+ };
@@ -0,0 +1,3 @@
1
+ type Props = {};
2
+ declare function TestSetValueInEffect({}: Props): import("react/jsx-runtime").JSX.Element;
3
+ export default TestSetValueInEffect;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Input } from "antd";
3
+ import axios from "axios";
4
+ import { useEffect, useEffectEvent, useState } from "react";
5
+ import { useShallow } from "zustand/react/shallow";
6
+ import Form from "../../providers/Form";
7
+ import { useFormStore } from "../../stores/formStore";
8
+ function TestSetValueInEffect({}) {
9
+ const { getFormInstance } = useFormStore(useShallow((state) => ({
10
+ getFormInstance: state.getFormInstance
11
+ })));
12
+ const [stateRender, setStateRender] = useState(1);
13
+ const [form] = Form.useForm("testFormMounted");
14
+ const setDataEvent = useEffectEvent((form2) => {
15
+ });
16
+ useEffect(() => {
17
+ setTimeout(() => {
18
+ }, 1e3);
19
+ setDataEvent(form);
20
+ axios.get("https://jsonplaceholder.typicode.com/todos/1").then((response) => {
21
+ });
22
+ }, []);
23
+ return _jsxs("div", { children: [_jsx(Form, { formName: "testFormMounted", children: _jsx(Form.Item, { name: "test", initialValue: "initial value", children: _jsx(Input, {}) }) }), _jsxs(Button, { onClick: () => {
24
+ setStateRender((prev) => prev + 1);
25
+ }, children: ["Counter: ", stateRender] })] });
26
+ }
27
+ var stdin_default = TestSetValueInEffect;
28
+ export {
29
+ stdin_default as default
30
+ };
@@ -41,7 +41,7 @@ export interface PublicFormInstance<T = any> {
41
41
  name: string;
42
42
  value: any;
43
43
  }>;
44
- getFieldErrors: () => Record<string, any>;
44
+ getFieldErrors: (names?: Array<keyof T & string>) => Record<string, any>;
45
45
  setFieldFocus: (name: keyof T & string) => void;
46
46
  }
47
47
  export interface UseFormItemProps<T = any> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-manage",
3
- "version": "1.0.8",
3
+ "version": "1.1.0-beta.1",
4
4
  "description": "Lightweight React form management with list and listener support.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,6 +42,7 @@
42
42
  "@types/react-dom": "^19.2.3",
43
43
  "@vitejs/plugin-react": "^4.3.1",
44
44
  "antd": "^5.22.5",
45
+ "axios": "^1.13.6",
45
46
  "esbuild": "^0.24.0",
46
47
  "eslint": "^9.14.0",
47
48
  "eslint-plugin-react-hooks": "^5.1.0",
package/src/App.tsx CHANGED
@@ -1,12 +1,9 @@
1
- import { Input } from "antd";
2
1
  import Form from "./providers/Form";
3
- import TestDialog from "./test/TestDialog";
4
- import TestListener from "./test/TestListener";
5
- import TestWrapperFormItem from "./test/TestWrapperFormItem";
6
2
 
7
3
  import { Form as AntdForm } from "antd";
8
- import TestWatchNormalize from "./test/TestWatchNormalize";
9
- import TestSetValueIndex from "./test/testSetValue";
4
+ import TestSetValueInEffect from "./test/testSetValue/TestSetValueInEffect";
5
+ import FormInstanceTestSuite from "./test/testFormInstance";
6
+ import TestFormInstance from "./test/testFormInstance";
10
7
 
11
8
  function TestFormWatch() {
12
9
  const watchValue = Form.useWatch("numericCode");
@@ -48,7 +45,10 @@ const App = () => {
48
45
  <TestListener />
49
46
  <TestWrapperFormItem /> */}
50
47
 
51
- <TestSetValueIndex />
48
+ {/* <TestSetValueIndex /> */}
49
+ {/* <TestSetValueInEffect /> */}
50
+
51
+ <FormInstanceTestSuite />
52
52
  </div>
53
53
  );
54
54
  };
@@ -11,9 +11,15 @@ import {
11
11
  uniqBy,
12
12
  } from "lodash";
13
13
  import { useTask } from "minh-custom-hooks-release";
14
- import type { ComponentType, FormHTMLAttributes, ReactNode } from "react";
14
+ import type {
15
+ ComponentType,
16
+ FormHTMLAttributes,
17
+ ReactElement,
18
+ ReactNode,
19
+ } from "react";
15
20
  import { createContext, useContext, useEffect, useMemo, useState } from "react";
16
21
  import { flushSync } from "react-dom";
22
+ import { v4 } from "uuid";
17
23
  import { useShallow } from "zustand/react/shallow"; // Import useShallow
18
24
  import FormItem from "../components/Form/FormItem";
19
25
  import { SUBMIT_STATE } from "../constants/form";
@@ -32,7 +38,7 @@ export type {
32
38
  FormFieldError,
33
39
  SubmitState,
34
40
  UseFormItemStateWatchReturn,
35
- ValidationRule
41
+ ValidationRule,
36
42
  } from "../types/public";
37
43
 
38
44
  export const FormContext = createContext(null);
@@ -47,7 +53,7 @@ export interface FormProps<T = any> extends Omit<
47
53
  > {
48
54
  collectHiddenFields?: boolean;
49
55
  children: ReactNode;
50
- formName: string;
56
+
51
57
  initialValues?: T;
52
58
  onFinish?: (values: T, allValues?: any) => void | Promise<void>;
53
59
  onReject?: (errorFields: any[]) => void | Promise<void>;
@@ -59,9 +65,23 @@ export interface FormProps<T = any> extends Omit<
59
65
  FormElement?: ComponentType<any>;
60
66
  }
61
67
 
62
- export default function Form<T = any>({
68
+ export interface FormPropsWithInstance<T = any> {
69
+ form: PublicFormInstance<T>;
70
+ }
71
+
72
+ export interface FormPropsWithoutInstance {
73
+ formName: string;
74
+ }
75
+
76
+ interface FormDeclearation {
77
+ <T = any>(props: FormProps<T> & FormPropsWithInstance<T>): ReactElement;
78
+ <T = any>(props: FormProps<T> & FormPropsWithoutInstance): ReactElement;
79
+ }
80
+
81
+ const InternalForm: FormDeclearation = function Form<T = any>({
63
82
  children,
64
- formName,
83
+ formName: externalFormName,
84
+ form: externalFormInstance,
65
85
  initialValues,
66
86
  onFinish,
67
87
  onReject,
@@ -69,7 +89,11 @@ export default function Form<T = any>({
69
89
  FormElement,
70
90
  collectHiddenFields: formCollectHiddenFields = true,
71
91
  ...props
72
- }: FormProps<T>) {
92
+ }: FormProps<T> & (FormPropsWithInstance<T> & FormPropsWithoutInstance)) {
93
+ const formName = useMemo(() => {
94
+ return externalFormInstance?.formName ?? externalFormName;
95
+ }, [externalFormInstance, externalFormName]);
96
+
73
97
  const {
74
98
  // appInitValue,
75
99
  getFormItemValue,
@@ -742,18 +766,22 @@ export default function Form<T = any>({
742
766
  isInitied: true,
743
767
  submitState: "idle",
744
768
  });
745
- setFormInstance({
746
- formName,
747
- resetFields,
748
- submit: runSubmit,
749
- submitAsync: runSubmitAsync,
750
- setFieldValue,
751
- setFieldValues,
752
- getFieldValue,
753
- getFieldValues,
754
- setFieldFocus,
755
- getFieldErrors,
756
- });
769
+
770
+ const formInstance = externalFormInstance ?? ({} as PublicFormInstance<T>);
771
+ if (externalFormInstance) {
772
+ externalFormInstance.setFieldValue = setFieldValue;
773
+ externalFormInstance.setFieldValues = setFieldValues;
774
+ externalFormInstance.getFieldValue = getFieldValue;
775
+ externalFormInstance.getFieldValues = getFieldValues;
776
+ externalFormInstance.setFieldFocus = setFieldFocus;
777
+ externalFormInstance.getFieldErrors = getFieldErrors;
778
+ externalFormInstance.submit = runSubmit;
779
+ externalFormInstance.submitAsync = runSubmitAsync;
780
+ externalFormInstance.resetFields = resetFields;
781
+ externalFormInstance.formName = formName;
782
+ }
783
+
784
+ setFormInstance(formInstance);
757
785
 
758
786
  return () => {
759
787
  revokeFormInstance({ formName });
@@ -800,7 +828,7 @@ export default function Form<T = any>({
800
828
  )}
801
829
  </FormContext.Provider>
802
830
  );
803
- }
831
+ };
804
832
 
805
833
  export function useFormContext() {
806
834
  const c = useContext(FormContext);
@@ -814,22 +842,133 @@ export function useForm<T = any>(
814
842
  formNameOrFormInstance?: string | PublicFormInstance<T>,
815
843
  ) {
816
844
  const formContext = useContext(FormContext);
817
- const targetFormName = isNil(formNameOrFormInstance)
818
- ? formContext?.formName
819
- : typeof formNameOrFormInstance === "object" &&
820
- formNameOrFormInstance !== null
821
- ? (formNameOrFormInstance as PublicFormInstance<T>).formName
822
- : (formNameOrFormInstance as string | undefined);
845
+ const [targetFormName] = useState(() => {
846
+ // Logic để xác định formName theo thứ tự ưu tiên:
847
+ // 1. Nếu formNameOrFormInstance undefined hoặc null → Lấy formName từ context (nếu có)
848
+ // 2. Nếu formNameOrFormInstance object (giả định là PublicFormInstance) → Lấy formName từ object đó
849
+ // 3. Nếu formNameOrFormInstance string → Sử dụng trực tiếp string đó làm formName
850
+ return isNil(formNameOrFormInstance)
851
+ ? formContext?.formName
852
+ : typeof formNameOrFormInstance === "object" &&
853
+ formNameOrFormInstance !== null
854
+ ? (formNameOrFormInstance as PublicFormInstance<T>).formName
855
+ : (formNameOrFormInstance as string | undefined);
856
+ });
823
857
 
858
+ // Nếu không có formName nào được cung cấp từ cả 3 nguồn trên, tạo một formName ngẫu nhiên
859
+ const [initFormName] = useState(() => targetFormName ?? v4());
860
+
861
+ // Lấy formInstance từ store dựa trên initFormName
824
862
  const formInstance = useFormStore((state) => {
825
- return state.formInstances.find((i) => i.formName === targetFormName);
863
+ return state.formInstances.find((i) => i.formName === initFormName);
826
864
  }) as PublicFormInstance<T> | undefined;
827
865
 
828
- return [formInstance];
866
+ const getFormInstance = useFormStore((state) => {
867
+ return state.getFormInstance;
868
+ });
869
+
870
+ // Trả về formInstance nếu tìm thấy, nếu không trả về một object mới với formName là initFormName để tránh lỗi khi gọi các method trên formInstance
871
+ return [
872
+ formInstance ??
873
+ ({
874
+ formName: initFormName,
875
+ setFieldValue(name, value, options) {
876
+ const formInstanceFind = getFormInstance(initFormName);
877
+ if (formInstanceFind) {
878
+ formInstanceFind.setFieldValue(name, value, options);
879
+ return;
880
+ }
881
+ console.warn(
882
+ `Form instance not init for formName: ${initFormName}. setFieldValue is not available.`,
883
+ );
884
+ },
885
+ setFieldValues(values, options) {
886
+ const formInstanceFind = getFormInstance(initFormName);
887
+ if (formInstanceFind) {
888
+ formInstanceFind.setFieldValues(values, options);
889
+ return;
890
+ }
891
+ console.warn(
892
+ `Form instance not init for formName: ${initFormName}. setFieldValues is not available.`,
893
+ );
894
+ },
895
+ getFieldValue(name) {
896
+ const formInstanceFind = getFormInstance(initFormName);
897
+ if (formInstanceFind) {
898
+ return formInstanceFind.getFieldValue(name);
899
+ }
900
+ console.warn(
901
+ `Form instance not init for formName: ${initFormName}. getFieldValue is not available.`,
902
+ );
903
+ return undefined;
904
+ },
905
+ getFieldValues(names) {
906
+ const formInstanceFind = getFormInstance(initFormName);
907
+ if (formInstanceFind) {
908
+ return formInstanceFind.getFieldValues(names);
909
+ }
910
+ console.warn(
911
+ `Form instance not init for formName: ${initFormName}. getFieldValues is not available.`,
912
+ );
913
+ return names.map((name) => ({ name, value: undefined }));
914
+ },
915
+ setFieldFocus(name) {
916
+ const formInstanceFind = getFormInstance(initFormName);
917
+ if (formInstanceFind) {
918
+ formInstanceFind.setFieldFocus(name);
919
+ return;
920
+ }
921
+ console.warn(
922
+ `Form instance not init for formName: ${initFormName}. setFieldFocus is not available.`,
923
+ );
924
+ },
925
+ getFieldErrors(names) {
926
+ const formInstanceFind = getFormInstance(initFormName);
927
+ if (formInstanceFind) {
928
+ return formInstanceFind.getFieldErrors(names);
929
+ }
930
+ console.warn(
931
+ `Form instance not init for formName: ${initFormName}. getFieldErrors is not available.`,
932
+ );
933
+ return names.map((name) => ({ name, errors: [] }));
934
+ },
935
+ submit() {
936
+ const formInstanceFind = getFormInstance(initFormName);
937
+ if (formInstanceFind) {
938
+ formInstanceFind.submit();
939
+ return;
940
+ }
941
+ console.warn(
942
+ `Form instance not init for formName: ${initFormName}. submit is not available.`,
943
+ );
944
+ },
945
+ submitAsync() {
946
+ const formInstanceFind = getFormInstance(initFormName);
947
+ if (formInstanceFind) {
948
+ return formInstanceFind.submitAsync();
949
+ }
950
+ console.warn(
951
+ `Form instance not init for formName: ${initFormName}. submitAsync is not available.`,
952
+ );
953
+ return Promise.resolve();
954
+ },
955
+ resetFields(resetOptions) {
956
+ const formInstanceFind = getFormInstance(initFormName);
957
+ if (formInstanceFind) {
958
+ formInstanceFind.resetFields(resetOptions);
959
+ return;
960
+ }
961
+ console.warn(
962
+ `Form instance not init for formName: ${initFormName}. resetFields is not available.`,
963
+ );
964
+ },
965
+ } as PublicFormInstance<T>),
966
+ Boolean(formInstance),
967
+ ] as const;
829
968
  }
830
969
 
831
970
  export function useWatch<T = any>(
832
- name: keyof T & string,
971
+ name: keyof T | (string & {}),
833
972
  formNameOrFormInstance?: string | PublicFormInstance<T>,
834
973
  ) {
835
974
  const [formInstance] = useForm<T>(formNameOrFormInstance as any);
@@ -933,6 +1072,83 @@ export const useFormItemStateWatch = <T = any,>(
933
1072
  };
934
1073
  };
935
1074
 
1075
+ export const useFormUtils = () => {
1076
+ const { getFormInstance } = useFormStore(
1077
+ useShallow((state) => ({
1078
+ getFormInstance: state.getFormInstance,
1079
+ })),
1080
+ );
1081
+
1082
+ const handleGetFormInstace = (
1083
+ formNameOrFormInstance?: string | PublicFormInstance,
1084
+ ) => {
1085
+ const targetFormName = isNil(formNameOrFormInstance)
1086
+ ? undefined
1087
+ : typeof formNameOrFormInstance === "object" &&
1088
+ formNameOrFormInstance !== null
1089
+ ? (formNameOrFormInstance as PublicFormInstance).formName
1090
+ : (formNameOrFormInstance as string | undefined);
1091
+
1092
+ if (!targetFormName) {
1093
+ throw new Error("Form name is required to get form instance");
1094
+ }
1095
+
1096
+ const formInstance = getFormInstance(targetFormName);
1097
+
1098
+ return formInstance;
1099
+ };
1100
+
1101
+ const setFieldValue = (
1102
+ formNameOrFormInstance: string | PublicFormInstance,
1103
+ name: string,
1104
+ value: any,
1105
+ options?: SetFieldValueOptions,
1106
+ ) => {
1107
+ const formInstance = handleGetFormInstace(formNameOrFormInstance);
1108
+ if (!formInstance) {
1109
+ console.warn(
1110
+ `Form instance not found for formName: ${formNameOrFormInstance}`,
1111
+ );
1112
+ return;
1113
+ }
1114
+ formInstance.setFieldValue(name, value, options);
1115
+ };
1116
+
1117
+ const setFieldValues = (
1118
+ formNameOrFormInstance: string | PublicFormInstance,
1119
+ values: any,
1120
+ options?: SetFieldValueOptions,
1121
+ ) => {
1122
+ const formInstance = handleGetFormInstace(formNameOrFormInstance);
1123
+ if (!formInstance) {
1124
+ console.warn(
1125
+ `Form instance not found for formName: ${formNameOrFormInstance}`,
1126
+ );
1127
+ return;
1128
+ }
1129
+ formInstance.setFieldValues(values, options);
1130
+ };
1131
+
1132
+ return {
1133
+ getFormInstance: handleGetFormInstace,
1134
+ setFieldValue,
1135
+ setFieldValues,
1136
+ };
1137
+ };
1138
+
1139
+ interface FormExposeType extends FormDeclearation {
1140
+ useForm: typeof useForm;
1141
+ Item: typeof FormItem;
1142
+ useWatch: typeof useWatch;
1143
+ useWatchNormalized: typeof useWatchNormalized;
1144
+ useSubmitDataWatch: typeof useSubmitDataWatch;
1145
+ useFormStateWatch: typeof useFormStateWatch;
1146
+ useFormItemStateWatch: typeof useFormItemStateWatch;
1147
+ useFormUtils: typeof useFormUtils;
1148
+ }
1149
+
1150
+ const Form = InternalForm as FormExposeType;
1151
+
936
1152
  Form.useForm = useForm;
937
1153
  Form.Item = FormItem;
938
1154
  Form.useWatch = useWatch;
@@ -940,3 +1156,6 @@ Form.useWatchNormalized = useWatchNormalized;
940
1156
  Form.useSubmitDataWatch = useSubmitDataWatch;
941
1157
  Form.useFormStateWatch = useFormStateWatch;
942
1158
  Form.useFormItemStateWatch = useFormItemStateWatch;
1159
+ Form.useFormUtils = useFormUtils;
1160
+
1161
+ export default Form;
@@ -279,7 +279,9 @@ const createFormStoreSlice = (storeSet: any, storeGet: any, api: any) => ({
279
279
  }),
280
280
  );
281
281
  },
282
-
282
+ getFormInstance(formName) {
283
+ return storeGet().formInstances.find((f) => f.formName === formName);
284
+ },
283
285
  setFormInstance({
284
286
  formName,
285
287
  resetFields,