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.
- package/CHANGELOG.md +45 -0
- package/dist/providers/Form.d.ts +30 -15
- package/dist/providers/Form.js +142 -25
- package/dist/stores/formStore.js +3 -0
- package/dist/test/testFormInstance/TestCase1_FormWithInstance.d.ts +14 -0
- package/dist/test/testFormInstance/TestCase1_FormWithInstance.js +21 -0
- package/dist/test/testFormInstance/TestCase2_AutoGenerateFormName.d.ts +14 -0
- package/dist/test/testFormInstance/TestCase2_AutoGenerateFormName.js +23 -0
- package/dist/test/testFormInstance/TestCase3_SafeFormMethods.d.ts +14 -0
- package/dist/test/testFormInstance/TestCase3_SafeFormMethods.js +66 -0
- package/dist/test/testFormInstance/TestCase4_UseFormStatusFlag.d.ts +15 -0
- package/dist/test/testFormInstance/TestCase4_UseFormStatusFlag.js +59 -0
- package/dist/test/testFormInstance/TestCase5_UseFormUtils.d.ts +1 -0
- package/dist/test/testFormInstance/TestCase5_UseFormUtils.js +65 -0
- package/dist/test/testFormInstance/TestCase6_FormWithoutWrapper.d.ts +15 -0
- package/dist/test/testFormInstance/TestCase6_FormWithoutWrapper.js +45 -0
- package/dist/test/testFormInstance/index.d.ts +12 -0
- package/dist/test/testFormInstance/index.js +49 -0
- package/dist/test/testSetValue/TestSetValueInEffect.d.ts +3 -0
- package/dist/test/testSetValue/TestSetValueInEffect.js +30 -0
- package/dist/types/public.d.ts +1 -1
- package/package.json +2 -1
- package/src/App.tsx +7 -7
- package/src/providers/Form.tsx +247 -28
- package/src/stores/formStore.ts +3 -1
- package/src/test/testFormInstance/TestCase1_FormWithInstance.tsx +112 -0
- package/src/test/testFormInstance/TestCase2_AutoGenerateFormName.tsx +139 -0
- package/src/test/testFormInstance/TestCase3_SafeFormMethods.tsx +203 -0
- package/src/test/testFormInstance/TestCase4_UseFormStatusFlag.tsx +252 -0
- package/src/test/testFormInstance/TestCase5_UseFormUtils.tsx +224 -0
- package/src/test/testFormInstance/TestCase6_FormWithoutWrapper.tsx +204 -0
- package/src/test/testFormInstance/index.tsx +116 -0
- package/src/test/testSetValue/TestSetValueInEffect.tsx +54 -0
- package/src/types/public.ts +1 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Badge, Button, Space, Tag, Typography } from "antd";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import Form from "../../providers/Form";
|
|
4
|
+
|
|
5
|
+
const { Title, Paragraph, Text } = Typography;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TestCase4: useForm Returns Status Flag
|
|
9
|
+
*
|
|
10
|
+
* Mô tả:
|
|
11
|
+
* - Test breaking change: useForm() trả về tuple [instance, isReady]
|
|
12
|
+
* - isReady = true khi Form đã mount và bind methods
|
|
13
|
+
* - isReady = false khi Form chưa mount hoặc đã unmount
|
|
14
|
+
*
|
|
15
|
+
* Expected Behavior:
|
|
16
|
+
* - Trước khi mount: isReady = false
|
|
17
|
+
* - Sau khi mount: isReady = true
|
|
18
|
+
* - Sau khi unmount: isReady = false
|
|
19
|
+
* - Có thể dùng isReady để conditionally render UI hoặc enable/disable buttons
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export default function TestCase4_UseFormStatusFlag() {
|
|
23
|
+
const [form, isFormReady] = Form.useForm<{ email: string; message: string }>();
|
|
24
|
+
const [formMounted, setFormMounted] = useState(false);
|
|
25
|
+
const [statusHistory, setStatusHistory] = useState<
|
|
26
|
+
{ time: string; status: boolean; event: string }[]
|
|
27
|
+
>([]);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setStatusHistory((prev) => [
|
|
31
|
+
...prev,
|
|
32
|
+
{
|
|
33
|
+
time: new Date().toLocaleTimeString(),
|
|
34
|
+
status: isFormReady,
|
|
35
|
+
event: isFormReady ? "Form Ready" : "Form Not Ready",
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
}, [isFormReady]);
|
|
39
|
+
|
|
40
|
+
const handleMountForm = () => {
|
|
41
|
+
setFormMounted(true);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleUnmountForm = () => {
|
|
45
|
+
setFormMounted(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleSetValue = () => {
|
|
49
|
+
if (isFormReady) {
|
|
50
|
+
form.setFieldValue("email", "test@example.com");
|
|
51
|
+
form.setFieldValue("message", "Hello from ready form!");
|
|
52
|
+
} else {
|
|
53
|
+
console.warn("Form is not ready yet!");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const clearHistory = () => {
|
|
58
|
+
setStatusHistory([]);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div style={{ padding: 24 }}>
|
|
63
|
+
<Title level={3}>TestCase4: useForm Returns Status Flag</Title>
|
|
64
|
+
|
|
65
|
+
<Paragraph>
|
|
66
|
+
<Text strong>Breaking Change:</Text> useForm() bây giờ trả về tuple [instance, isReady]
|
|
67
|
+
</Paragraph>
|
|
68
|
+
|
|
69
|
+
<Paragraph>
|
|
70
|
+
<Text type="secondary">
|
|
71
|
+
isReady flag cho biết Form đã mount và sẵn sàng nhận calls hay chưa.
|
|
72
|
+
Có thể dùng để conditionally enable/disable features dựa trên Form state.
|
|
73
|
+
</Text>
|
|
74
|
+
</Paragraph>
|
|
75
|
+
|
|
76
|
+
{/* Status Indicator */}
|
|
77
|
+
<div style={{ marginBottom: 24, padding: 16, background: "#fafafa", border: "1px solid #d9d9d9" }}>
|
|
78
|
+
<Space size="large">
|
|
79
|
+
<div>
|
|
80
|
+
<Text strong>Form Ready Status: </Text>
|
|
81
|
+
<Badge
|
|
82
|
+
status={isFormReady ? "success" : "error"}
|
|
83
|
+
text={
|
|
84
|
+
<Text strong style={{ color: isFormReady ? "#52c41a" : "#ff4d4f" }}>
|
|
85
|
+
{isFormReady ? "READY" : "NOT READY"}
|
|
86
|
+
</Text>
|
|
87
|
+
}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<Text strong>Form Mounted: </Text>
|
|
92
|
+
<Tag color={formMounted ? "green" : "red"}>
|
|
93
|
+
{formMounted ? "YES" : "NO"}
|
|
94
|
+
</Tag>
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<Text strong>FormName: </Text>
|
|
98
|
+
<Text code>{form.formName}</Text>
|
|
99
|
+
</div>
|
|
100
|
+
</Space>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Control Buttons */}
|
|
104
|
+
<Space style={{ marginBottom: 24 }} wrap>
|
|
105
|
+
{!formMounted ? (
|
|
106
|
+
<Button onClick={handleMountForm} type="primary" size="large">
|
|
107
|
+
Mount Form
|
|
108
|
+
</Button>
|
|
109
|
+
) : (
|
|
110
|
+
<Button onClick={handleUnmountForm} danger size="large">
|
|
111
|
+
Unmount Form
|
|
112
|
+
</Button>
|
|
113
|
+
)}
|
|
114
|
+
<Button
|
|
115
|
+
onClick={handleSetValue}
|
|
116
|
+
disabled={!isFormReady}
|
|
117
|
+
type={isFormReady ? "primary" : "default"}
|
|
118
|
+
>
|
|
119
|
+
Set Values {!isFormReady && "(Disabled - Form Not Ready)"}
|
|
120
|
+
</Button>
|
|
121
|
+
<Button onClick={clearHistory}>Clear History</Button>
|
|
122
|
+
</Space>
|
|
123
|
+
|
|
124
|
+
<div style={{ display: "flex", gap: 24 }}>
|
|
125
|
+
{/* Status History */}
|
|
126
|
+
<div style={{ flex: 1 }}>
|
|
127
|
+
<Title level={5}>Status Change History:</Title>
|
|
128
|
+
<div
|
|
129
|
+
style={{
|
|
130
|
+
border: "1px solid #d9d9d9",
|
|
131
|
+
padding: 16,
|
|
132
|
+
height: 300,
|
|
133
|
+
overflow: "auto",
|
|
134
|
+
background: "#ffffff",
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{statusHistory.length === 0 ? (
|
|
138
|
+
<Text type="secondary">No status changes yet...</Text>
|
|
139
|
+
) : (
|
|
140
|
+
<table style={{ width: "100%", borderCollapse: "collapse" }}>
|
|
141
|
+
<thead>
|
|
142
|
+
<tr style={{ borderBottom: "2px solid #d9d9d9" }}>
|
|
143
|
+
<th style={{ padding: 8, textAlign: "left" }}>Time</th>
|
|
144
|
+
<th style={{ padding: 8, textAlign: "left" }}>Status</th>
|
|
145
|
+
<th style={{ padding: 8, textAlign: "left" }}>Event</th>
|
|
146
|
+
</tr>
|
|
147
|
+
</thead>
|
|
148
|
+
<tbody>
|
|
149
|
+
{statusHistory.map((entry, index) => (
|
|
150
|
+
<tr
|
|
151
|
+
key={index}
|
|
152
|
+
style={{
|
|
153
|
+
borderBottom: "1px solid #f0f0f0",
|
|
154
|
+
background: index % 2 === 0 ? "#fafafa" : "#ffffff",
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<td style={{ padding: 8, fontFamily: "monospace", fontSize: 12 }}>
|
|
158
|
+
{entry.time}
|
|
159
|
+
</td>
|
|
160
|
+
<td style={{ padding: 8 }}>
|
|
161
|
+
<Tag color={entry.status ? "green" : "red"}>
|
|
162
|
+
{entry.status ? "TRUE" : "FALSE"}
|
|
163
|
+
</Tag>
|
|
164
|
+
</td>
|
|
165
|
+
<td style={{ padding: 8 }}>{entry.event}</td>
|
|
166
|
+
</tr>
|
|
167
|
+
))}
|
|
168
|
+
</tbody>
|
|
169
|
+
</table>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Form Panel */}
|
|
175
|
+
<div style={{ flex: 1 }}>
|
|
176
|
+
<Title level={5}>Form Panel:</Title>
|
|
177
|
+
{formMounted ? (
|
|
178
|
+
<div style={{ border: "2px solid #52c41a", padding: 16 }}>
|
|
179
|
+
<Form
|
|
180
|
+
form={form}
|
|
181
|
+
initialValues={{ email: "", message: "" }}
|
|
182
|
+
onFinish={(values) => {
|
|
183
|
+
console.log("Form submitted:", values);
|
|
184
|
+
alert(`Submitted: ${JSON.stringify(values, null, 2)}`);
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
<Form.Item name="email" label="Email">
|
|
188
|
+
<input
|
|
189
|
+
type="email"
|
|
190
|
+
placeholder="Enter email"
|
|
191
|
+
style={{ padding: 8, width: "100%" }}
|
|
192
|
+
/>
|
|
193
|
+
</Form.Item>
|
|
194
|
+
|
|
195
|
+
<Form.Item name="message" label="Message">
|
|
196
|
+
<textarea
|
|
197
|
+
placeholder="Enter message"
|
|
198
|
+
rows={4}
|
|
199
|
+
style={{ padding: 8, width: "100%" }}
|
|
200
|
+
/>
|
|
201
|
+
</Form.Item>
|
|
202
|
+
|
|
203
|
+
<Button type="primary" htmlType="submit">
|
|
204
|
+
Submit
|
|
205
|
+
</Button>
|
|
206
|
+
</Form>
|
|
207
|
+
</div>
|
|
208
|
+
) : (
|
|
209
|
+
<div
|
|
210
|
+
style={{
|
|
211
|
+
border: "2px dashed #d9d9d9",
|
|
212
|
+
padding: 16,
|
|
213
|
+
height: 300,
|
|
214
|
+
display: "flex",
|
|
215
|
+
alignItems: "center",
|
|
216
|
+
justifyContent: "center",
|
|
217
|
+
background: "#fafafa",
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<div style={{ textAlign: "center" }}>
|
|
221
|
+
<Text type="secondary" style={{ fontSize: 16 }}>
|
|
222
|
+
Form is not mounted
|
|
223
|
+
</Text>
|
|
224
|
+
<br />
|
|
225
|
+
<Button
|
|
226
|
+
type="primary"
|
|
227
|
+
onClick={handleMountForm}
|
|
228
|
+
style={{ marginTop: 16 }}
|
|
229
|
+
>
|
|
230
|
+
Mount Form
|
|
231
|
+
</Button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div style={{ marginTop: 24, padding: 16, background: "#f5f5f5" }}>
|
|
239
|
+
<Title level={5}>Expected Results:</Title>
|
|
240
|
+
<ul>
|
|
241
|
+
<li>✅ Initial state: isReady = false (logged in history)</li>
|
|
242
|
+
<li>✅ Click "Mount Form" → isReady changes to true (logged in history)</li>
|
|
243
|
+
<li>✅ "Set Values" button becomes enabled when isReady = true</li>
|
|
244
|
+
<li>✅ Click "Set Values" → Form values update successfully</li>
|
|
245
|
+
<li>✅ Click "Unmount Form" → isReady changes back to false (logged in history)</li>
|
|
246
|
+
<li>✅ "Set Values" button becomes disabled again</li>
|
|
247
|
+
<li>✅ Status badge and tag update in real-time</li>
|
|
248
|
+
</ul>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Button, Space, Typography, Card, Input, message } from "antd";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import Form from "../../providers/Form";
|
|
4
|
+
|
|
5
|
+
const { Title, Paragraph, Text } = Typography;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TestCase5: useFormUtils Hook
|
|
9
|
+
*
|
|
10
|
+
* Mô tả:
|
|
11
|
+
* - Test new hook: Form.useFormUtils()
|
|
12
|
+
* - Cho phép thao tác với form từ bên ngoài component, không cần context
|
|
13
|
+
* - Cung cấp: getFormInstance, setFieldValue, setFieldValues
|
|
14
|
+
*
|
|
15
|
+
* Expected Behavior:
|
|
16
|
+
* - Có thể get form instance từ bất kỳ đâu bằng formName
|
|
17
|
+
* - Có thể set values cho form từ bên ngoài form component
|
|
18
|
+
* - Multiple forms có thể được điều khiển từ 1 control panel
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function ControlPanel() {
|
|
22
|
+
const { getFormInstance, setFieldValue, setFieldValues } = Form.useFormUtils();
|
|
23
|
+
const [targetFormName, setTargetFormName] = useState("form1");
|
|
24
|
+
|
|
25
|
+
const handleSetSingleValue = () => {
|
|
26
|
+
try {
|
|
27
|
+
setFieldValue(targetFormName, "username", "admin");
|
|
28
|
+
message.success(`Set username='admin' for ${targetFormName}`);
|
|
29
|
+
} catch (error: any) {
|
|
30
|
+
message.error(error.message);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleSetMultipleValues = () => {
|
|
35
|
+
try {
|
|
36
|
+
setFieldValues(targetFormName, {
|
|
37
|
+
username: "john_doe",
|
|
38
|
+
email: "john@example.com",
|
|
39
|
+
});
|
|
40
|
+
message.success(`Set multiple values for ${targetFormName}`);
|
|
41
|
+
} catch (error: any) {
|
|
42
|
+
message.error(error.message);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleGetFormInstance = () => {
|
|
47
|
+
try {
|
|
48
|
+
const formInstance = getFormInstance(targetFormName);
|
|
49
|
+
console.log("Form Instance:", formInstance);
|
|
50
|
+
console.log("Current Values:", {
|
|
51
|
+
username: formInstance?.getFieldValue("username"),
|
|
52
|
+
email: formInstance?.getFieldValue("email"),
|
|
53
|
+
});
|
|
54
|
+
message.info("Check console for form instance details");
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
message.error(error.message);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleReset = () => {
|
|
61
|
+
try {
|
|
62
|
+
const formInstance = getFormInstance(targetFormName);
|
|
63
|
+
formInstance?.resetFields();
|
|
64
|
+
message.success(`Reset ${targetFormName}`);
|
|
65
|
+
} catch (error: any) {
|
|
66
|
+
message.error(error.message);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleSubmit = () => {
|
|
71
|
+
try {
|
|
72
|
+
const formInstance = getFormInstance(targetFormName);
|
|
73
|
+
formInstance?.submit();
|
|
74
|
+
message.info(`Submitting ${targetFormName}`);
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
message.error(error.message);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Card
|
|
82
|
+
title="🎮 Control Panel (useFormUtils)"
|
|
83
|
+
style={{ marginBottom: 24 }}
|
|
84
|
+
extra={
|
|
85
|
+
<Space>
|
|
86
|
+
<Text>Target Form:</Text>
|
|
87
|
+
<Input
|
|
88
|
+
value={targetFormName}
|
|
89
|
+
onChange={(e) => setTargetFormName(e.target.value)}
|
|
90
|
+
placeholder="Enter form name"
|
|
91
|
+
style={{ width: 150 }}
|
|
92
|
+
/>
|
|
93
|
+
</Space>
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
<Space wrap>
|
|
97
|
+
<Button onClick={handleSetSingleValue} type="primary">
|
|
98
|
+
Set Single Value
|
|
99
|
+
</Button>
|
|
100
|
+
<Button onClick={handleSetMultipleValues} type="primary">
|
|
101
|
+
Set Multiple Values
|
|
102
|
+
</Button>
|
|
103
|
+
<Button onClick={handleGetFormInstance}>
|
|
104
|
+
Get Form Instance
|
|
105
|
+
</Button>
|
|
106
|
+
<Button onClick={handleReset} danger>
|
|
107
|
+
Reset Form
|
|
108
|
+
</Button>
|
|
109
|
+
<Button onClick={handleSubmit} type="dashed">
|
|
110
|
+
Submit Form
|
|
111
|
+
</Button>
|
|
112
|
+
</Space>
|
|
113
|
+
<div style={{ marginTop: 16, padding: 12, background: "#f0f0f0" }}>
|
|
114
|
+
<Text type="secondary">
|
|
115
|
+
💡 Tip: Change the target form name to "form1" or "form2" to control different forms
|
|
116
|
+
</Text>
|
|
117
|
+
</div>
|
|
118
|
+
</Card>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default function TestCase5_UseFormUtils() {
|
|
123
|
+
return (
|
|
124
|
+
<div style={{ padding: 24 }}>
|
|
125
|
+
<Title level={3}>TestCase5: useFormUtils Hook</Title>
|
|
126
|
+
|
|
127
|
+
<Paragraph>
|
|
128
|
+
<Text strong>New Hook:</Text> Form.useFormUtils() cho phép thao tác với form từ bên ngoài
|
|
129
|
+
</Paragraph>
|
|
130
|
+
|
|
131
|
+
<Paragraph>
|
|
132
|
+
<Text type="secondary">
|
|
133
|
+
Hook này cung cấp các utility methods để get/set form values từ bất kỳ đâu
|
|
134
|
+
trong app, không cần phải ở trong Form context. Hữu ích cho global control panels,
|
|
135
|
+
external triggers, hoặc cross-component form manipulation.
|
|
136
|
+
</Text>
|
|
137
|
+
</Paragraph>
|
|
138
|
+
|
|
139
|
+
{/* Control Panel */}
|
|
140
|
+
<ControlPanel />
|
|
141
|
+
|
|
142
|
+
{/* Forms */}
|
|
143
|
+
<div style={{ display: "flex", gap: 24 }}>
|
|
144
|
+
{/* Form 1 */}
|
|
145
|
+
<div style={{ flex: 1 }}>
|
|
146
|
+
<Card title="Form 1 (formName='form1')">
|
|
147
|
+
<Form
|
|
148
|
+
formName="form1"
|
|
149
|
+
initialValues={{ username: "", email: "" }}
|
|
150
|
+
onFinish={(values) => {
|
|
151
|
+
console.log("Form 1 submitted:", values);
|
|
152
|
+
message.success(`Form 1 submitted: ${JSON.stringify(values)}`);
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<Form.Item name="username" label="Username">
|
|
156
|
+
<Input placeholder="Username" />
|
|
157
|
+
</Form.Item>
|
|
158
|
+
|
|
159
|
+
<Form.Item name="email" label="Email">
|
|
160
|
+
<Input type="email" placeholder="Email" />
|
|
161
|
+
</Form.Item>
|
|
162
|
+
|
|
163
|
+
<Button type="primary" htmlType="submit">
|
|
164
|
+
Submit Form 1
|
|
165
|
+
</Button>
|
|
166
|
+
</Form>
|
|
167
|
+
</Card>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Form 2 */}
|
|
171
|
+
<div style={{ flex: 1 }}>
|
|
172
|
+
<Card title="Form 2 (formName='form2')">
|
|
173
|
+
<Form
|
|
174
|
+
formName="form2"
|
|
175
|
+
initialValues={{ username: "", email: "" }}
|
|
176
|
+
onFinish={(values) => {
|
|
177
|
+
console.log("Form 2 submitted:", values);
|
|
178
|
+
message.success(`Form 2 submitted: ${JSON.stringify(values)}`);
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<Form.Item name="username" label="Username">
|
|
182
|
+
<Input placeholder="Username" />
|
|
183
|
+
</Form.Item>
|
|
184
|
+
|
|
185
|
+
<Form.Item name="email" label="Email">
|
|
186
|
+
<Input type="email" placeholder="Email" />
|
|
187
|
+
</Form.Item>
|
|
188
|
+
|
|
189
|
+
<Button type="primary" htmlType="submit">
|
|
190
|
+
Submit Form 2
|
|
191
|
+
</Button>
|
|
192
|
+
</Form>
|
|
193
|
+
</Card>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div style={{ marginTop: 24, padding: 16, background: "#f5f5f5" }}>
|
|
198
|
+
<Title level={5}>Expected Results:</Title>
|
|
199
|
+
<ul>
|
|
200
|
+
<li>✅ Control panel starts with target "form1"</li>
|
|
201
|
+
<li>✅ Click "Set Single Value" → Form 1's username field updates to "admin"</li>
|
|
202
|
+
<li>✅ Click "Set Multiple Values" → Form 1's both fields update</li>
|
|
203
|
+
<li>✅ Click "Get Form Instance" → Console logs form instance and current values</li>
|
|
204
|
+
<li>✅ Change target to "form2" in input</li>
|
|
205
|
+
<li>✅ Click "Set Single Value" → Form 2's username field updates</li>
|
|
206
|
+
<li>✅ Click "Reset Form" → Target form resets to initialValues</li>
|
|
207
|
+
<li>✅ Click "Submit Form" → Target form submits programmatically</li>
|
|
208
|
+
<li>⚠️ Enter invalid formName → Error message shows "Form instance not found"</li>
|
|
209
|
+
</ul>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div style={{ marginTop: 16, padding: 16, background: "#e6f7ff", border: "1px solid #91d5ff" }}>
|
|
213
|
+
<Title level={5}>Use Cases:</Title>
|
|
214
|
+
<ul style={{ marginBottom: 0 }}>
|
|
215
|
+
<li>Global control panels để quản lý multiple forms</li>
|
|
216
|
+
<li>External triggers (từ API response, WebSocket, events...)</li>
|
|
217
|
+
<li>Cross-component form manipulation</li>
|
|
218
|
+
<li>Testing và debugging tools</li>
|
|
219
|
+
<li>Admin dashboards với form management</li>
|
|
220
|
+
</ul>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Button, Card, Input, Space, Typography } from "antd";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import Form from "../../providers/Form";
|
|
4
|
+
|
|
5
|
+
const { Title, Paragraph, Text } = Typography;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TestCase6: Form Without Wrapper Component
|
|
9
|
+
*
|
|
10
|
+
* Mô tả:
|
|
11
|
+
* - Test use case: Sử dụng form methods mà không cần render Form component
|
|
12
|
+
* - Form instance có thể được sử dụng độc lập để quản lý state
|
|
13
|
+
* - Hữu ích cho: custom form implementations, headless forms, non-standard UI
|
|
14
|
+
*
|
|
15
|
+
* Expected Behavior:
|
|
16
|
+
* - Form instance hoạt động như một state manager
|
|
17
|
+
* - Có thể set/get values mà không cần Form component
|
|
18
|
+
* - Có thể dùng Form.useWatch để reactive update
|
|
19
|
+
* - Warning được log vì không có Form component để validate
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export default function TestCase6_FormWithoutWrapper() {
|
|
23
|
+
const [form] = Form.useForm<{
|
|
24
|
+
firstName: string;
|
|
25
|
+
lastName: string;
|
|
26
|
+
age: number;
|
|
27
|
+
}>();
|
|
28
|
+
|
|
29
|
+
// Watch individual fields
|
|
30
|
+
const firstName = Form.useWatch("firstName", form);
|
|
31
|
+
const lastName = Form.useWatch("lastName", form);
|
|
32
|
+
const age = Form.useWatch("age", form);
|
|
33
|
+
|
|
34
|
+
const [manualFirstName, setManualFirstName] = useState("");
|
|
35
|
+
const [manualLastName, setManualLastName] = useState("");
|
|
36
|
+
const [manualAge, setManualAge] = useState("");
|
|
37
|
+
|
|
38
|
+
const handleSetFromManualInputs = () => {
|
|
39
|
+
form.setFieldValue("firstName", manualFirstName);
|
|
40
|
+
form.setFieldValue("lastName", manualLastName);
|
|
41
|
+
form.setFieldValue("age", Number(manualAge) || 0);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleSetBatch = () => {
|
|
45
|
+
form.setFieldValues({
|
|
46
|
+
firstName: "John",
|
|
47
|
+
lastName: "Doe",
|
|
48
|
+
age: 30,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleGetValues = () => {
|
|
53
|
+
const values = {
|
|
54
|
+
firstName: form.getFieldValue("firstName"),
|
|
55
|
+
lastName: form.getFieldValue("lastName"),
|
|
56
|
+
age: form.getFieldValue("age"),
|
|
57
|
+
};
|
|
58
|
+
console.log("Current values:", values);
|
|
59
|
+
alert(JSON.stringify(values, null, 2));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleReset = () => {
|
|
63
|
+
form.resetFields();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleSubmit = () => {
|
|
67
|
+
// Note: Submit sẽ log warning vì không có Form component
|
|
68
|
+
form.submit();
|
|
69
|
+
console.log("Submit called (check warning in console)");
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const fullName = `${firstName || ""} ${lastName || ""}`.trim() || "(empty)";
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div style={{ padding: 24 }}>
|
|
76
|
+
<Title level={3}>TestCase6: Form Without Wrapper Component</Title>
|
|
77
|
+
|
|
78
|
+
<Paragraph>
|
|
79
|
+
<Text strong>Use Case:</Text> Sử dụng form instance như state manager mà không cần Form component
|
|
80
|
+
</Paragraph>
|
|
81
|
+
|
|
82
|
+
<Paragraph>
|
|
83
|
+
<Text type="secondary">
|
|
84
|
+
Form instance có thể hoạt động độc lập như một state manager.
|
|
85
|
+
Hữu ích cho custom form implementations, headless forms, hoặc khi bạn muốn
|
|
86
|
+
tự build UI mà không dùng Form component wrapper.
|
|
87
|
+
</Text>
|
|
88
|
+
</Paragraph>
|
|
89
|
+
|
|
90
|
+
<div style={{ display: "flex", gap: 24 }}>
|
|
91
|
+
{/* Manual Input Panel */}
|
|
92
|
+
<Card title="📝 Manual Inputs (Not Connected to Form)" style={{ flex: 1 }}>
|
|
93
|
+
<Space direction="vertical" style={{ width: "100%" }}>
|
|
94
|
+
<div>
|
|
95
|
+
<Text strong>First Name:</Text>
|
|
96
|
+
<Input
|
|
97
|
+
value={manualFirstName}
|
|
98
|
+
onChange={(e) => setManualFirstName(e.target.value)}
|
|
99
|
+
placeholder="Enter first name"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<Text strong>Last Name:</Text>
|
|
104
|
+
<Input
|
|
105
|
+
value={manualLastName}
|
|
106
|
+
onChange={(e) => setManualLastName(e.target.value)}
|
|
107
|
+
placeholder="Enter last name"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
<div>
|
|
111
|
+
<Text strong>Age:</Text>
|
|
112
|
+
<Input
|
|
113
|
+
type="number"
|
|
114
|
+
value={manualAge}
|
|
115
|
+
onChange={(e) => setManualAge(e.target.value)}
|
|
116
|
+
placeholder="Enter age"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<Button
|
|
120
|
+
type="primary"
|
|
121
|
+
onClick={handleSetFromManualInputs}
|
|
122
|
+
block
|
|
123
|
+
>
|
|
124
|
+
Set Form Values
|
|
125
|
+
</Button>
|
|
126
|
+
</Space>
|
|
127
|
+
</Card>
|
|
128
|
+
|
|
129
|
+
{/* Display Panel */}
|
|
130
|
+
<Card title="👁️ Form State (via useWatch)" style={{ flex: 1 }}>
|
|
131
|
+
<Space direction="vertical" style={{ width: "100%" }}>
|
|
132
|
+
<div>
|
|
133
|
+
<Text strong>First Name: </Text>
|
|
134
|
+
<Text code>{firstName || "(empty)"}</Text>
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<Text strong>Last Name: </Text>
|
|
138
|
+
<Text code>{lastName || "(empty)"}</Text>
|
|
139
|
+
</div>
|
|
140
|
+
<div>
|
|
141
|
+
<Text strong>Age: </Text>
|
|
142
|
+
<Text code>{age ?? "(empty)"}</Text>
|
|
143
|
+
</div>
|
|
144
|
+
<div style={{ marginTop: 16, padding: 12, background: "#f0f0f0" }}>
|
|
145
|
+
<Text strong>Computed Full Name: </Text>
|
|
146
|
+
<Text style={{ fontSize: 16 }}>{fullName}</Text>
|
|
147
|
+
</div>
|
|
148
|
+
</Space>
|
|
149
|
+
</Card>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Control Buttons */}
|
|
153
|
+
<Card title="🎮 Form Controls" style={{ marginTop: 24 }}>
|
|
154
|
+
<Space wrap>
|
|
155
|
+
<Button onClick={handleSetBatch} type="primary">
|
|
156
|
+
Set Batch Values (John Doe, 30)
|
|
157
|
+
</Button>
|
|
158
|
+
<Button onClick={handleGetValues}>
|
|
159
|
+
Get All Values (Alert)
|
|
160
|
+
</Button>
|
|
161
|
+
<Button onClick={handleReset} danger>
|
|
162
|
+
Reset Form
|
|
163
|
+
</Button>
|
|
164
|
+
<Button onClick={handleSubmit} type="dashed">
|
|
165
|
+
Submit Form (Will Log Warning)
|
|
166
|
+
</Button>
|
|
167
|
+
</Space>
|
|
168
|
+
|
|
169
|
+
<div style={{ marginTop: 16, padding: 12, background: "#fff7e6", border: "1px solid #ffd591" }}>
|
|
170
|
+
<Text strong>⚠️ Note: </Text>
|
|
171
|
+
<Text>
|
|
172
|
+
Không có Form component được render. Form instance hoạt động như pure state manager.
|
|
173
|
+
Submit sẽ log warning vì không có onFinish handler.
|
|
174
|
+
</Text>
|
|
175
|
+
</div>
|
|
176
|
+
</Card>
|
|
177
|
+
|
|
178
|
+
<div style={{ marginTop: 24, padding: 16, background: "#f5f5f5" }}>
|
|
179
|
+
<Title level={5}>Expected Results:</Title>
|
|
180
|
+
<ul>
|
|
181
|
+
<li>✅ Type in manual inputs → Values NOT auto-sync to form state</li>
|
|
182
|
+
<li>✅ Click "Set Form Values" → Form state updates, display panel shows new values</li>
|
|
183
|
+
<li>✅ Display panel updates reactively via useWatch</li>
|
|
184
|
+
<li>✅ Click "Set Batch Values" → All fields update at once</li>
|
|
185
|
+
<li>✅ Click "Get All Values" → Alert shows current form values</li>
|
|
186
|
+
<li>✅ Click "Reset Form" → All fields reset (empty)</li>
|
|
187
|
+
<li>✅ Computed Full Name updates based on firstName + lastName</li>
|
|
188
|
+
<li>⚠️ Click "Submit Form" → Console shows warning (no Form component mounted)</li>
|
|
189
|
+
</ul>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div style={{ marginTop: 16, padding: 16, background: "#e6f7ff", border: "1px solid #91d5ff" }}>
|
|
193
|
+
<Title level={5}>Use Cases:</Title>
|
|
194
|
+
<ul style={{ marginBottom: 0 }}>
|
|
195
|
+
<li><strong>Headless Forms:</strong> Build custom UI mà không bị constraint bởi Form component</li>
|
|
196
|
+
<li><strong>State Manager:</strong> Sử dụng như lightweight state manager với validation support</li>
|
|
197
|
+
<li><strong>Multi-step Forms:</strong> Quản lý form state across multiple steps/pages</li>
|
|
198
|
+
<li><strong>Form Wizards:</strong> Share form instance between different wizard steps</li>
|
|
199
|
+
<li><strong>Custom Integrations:</strong> Integrate với third-party UI libraries</li>
|
|
200
|
+
</ul>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|