@delightui/components 0.1.162-alpha.5 → 0.1.163
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/dist/cjs/components/atoms/Checkbox/Checkbox.d.ts +1 -1
- package/dist/cjs/components/atoms/Checkbox/Checkbox.presenter.d.ts +1 -1
- package/dist/cjs/components/atoms/CheckboxItem/CheckboxItem.d.ts +1 -1
- package/dist/cjs/components/atoms/CheckboxItem/CheckboxItem.presenter.d.ts +2 -2
- package/dist/cjs/components/atoms/CustomToggle/CustomToggle.d.ts +1 -1
- package/dist/cjs/components/atoms/Input/Input.d.ts +1 -1
- package/dist/cjs/components/atoms/Input/Input.presenter.d.ts +2 -2
- package/dist/cjs/components/atoms/Password/Password.presenter.d.ts +2 -2
- package/dist/cjs/components/atoms/RadioButton/RadioButton.d.ts +1 -1
- package/dist/cjs/components/atoms/RadioButtonItem/RadioButtonItem.d.ts +1 -1
- package/dist/cjs/components/atoms/RadioButtonItem/RadioButtonItem.presenter.d.ts +2 -2
- package/dist/cjs/components/atoms/TextArea/TextArea.d.ts +1 -1
- package/dist/cjs/components/atoms/TextArea/TextArea.presenter.d.ts +1 -1
- package/dist/cjs/components/atoms/Toggle/Toggle.d.ts +1 -1
- package/dist/cjs/components/atoms/Toggle/Toggle.presenter.d.ts +2 -2
- package/dist/cjs/components/atoms/ToggleButton/ToggleButton.d.ts +1 -1
- package/dist/cjs/components/molecules/FormField/FormField.d.ts +2 -28
- package/dist/cjs/components/molecules/FormField/FormField.presenter.d.ts +8 -17
- package/dist/cjs/components/molecules/FormField/FormField.types.d.ts +52 -115
- package/dist/cjs/components/molecules/FormField/FormField.utils.d.ts +11 -8
- package/dist/cjs/components/molecules/FormField/index.d.ts +2 -1
- package/dist/cjs/components/molecules/Search/Search.d.ts +1 -1
- package/dist/cjs/components/molecules/Search/Search.presenter.d.ts +2 -2
- package/dist/cjs/components/molecules/Select/Select.presenter.d.ts +4 -4
- package/dist/cjs/components/organisms/Form/DropzoneFormExample.d.ts +6 -0
- package/dist/cjs/components/organisms/Form/Form.d.ts +1 -22
- package/dist/cjs/components/organisms/Form/Form.types.d.ts +93 -64
- package/dist/cjs/components/organisms/Form/Form.utils.d.ts +2 -0
- package/dist/cjs/components/organisms/Form/FormContext.d.ts +5 -0
- package/dist/cjs/components/organisms/Form/UpdatedFormExample.d.ts +23 -0
- package/dist/cjs/components/organisms/Form/{examples/UseFormExample.d.ts → UseFormExample.d.ts} +3 -0
- package/dist/cjs/components/organisms/Form/index.d.ts +3 -7
- package/dist/cjs/components/organisms/Form/useForm.d.ts +40 -43
- package/dist/cjs/library.js +3 -3
- package/dist/cjs/library.js.map +1 -1
- package/dist/esm/components/atoms/Checkbox/Checkbox.d.ts +1 -1
- package/dist/esm/components/atoms/Checkbox/Checkbox.presenter.d.ts +1 -1
- package/dist/esm/components/atoms/CheckboxItem/CheckboxItem.d.ts +1 -1
- package/dist/esm/components/atoms/CheckboxItem/CheckboxItem.presenter.d.ts +2 -2
- package/dist/esm/components/atoms/CustomToggle/CustomToggle.d.ts +1 -1
- package/dist/esm/components/atoms/Input/Input.d.ts +1 -1
- package/dist/esm/components/atoms/Input/Input.presenter.d.ts +2 -2
- package/dist/esm/components/atoms/Password/Password.presenter.d.ts +2 -2
- package/dist/esm/components/atoms/RadioButton/RadioButton.d.ts +1 -1
- package/dist/esm/components/atoms/RadioButtonItem/RadioButtonItem.d.ts +1 -1
- package/dist/esm/components/atoms/RadioButtonItem/RadioButtonItem.presenter.d.ts +2 -2
- package/dist/esm/components/atoms/TextArea/TextArea.d.ts +1 -1
- package/dist/esm/components/atoms/TextArea/TextArea.presenter.d.ts +1 -1
- package/dist/esm/components/atoms/Toggle/Toggle.d.ts +1 -1
- package/dist/esm/components/atoms/Toggle/Toggle.presenter.d.ts +2 -2
- package/dist/esm/components/atoms/ToggleButton/ToggleButton.d.ts +1 -1
- package/dist/esm/components/molecules/FormField/FormField.d.ts +2 -28
- package/dist/esm/components/molecules/FormField/FormField.presenter.d.ts +8 -17
- package/dist/esm/components/molecules/FormField/FormField.types.d.ts +52 -115
- package/dist/esm/components/molecules/FormField/FormField.utils.d.ts +11 -8
- package/dist/esm/components/molecules/FormField/index.d.ts +2 -1
- package/dist/esm/components/molecules/Search/Search.d.ts +1 -1
- package/dist/esm/components/molecules/Search/Search.presenter.d.ts +2 -2
- package/dist/esm/components/molecules/Select/Select.presenter.d.ts +4 -4
- package/dist/esm/components/organisms/Form/DropzoneFormExample.d.ts +6 -0
- package/dist/esm/components/organisms/Form/Form.d.ts +1 -22
- package/dist/esm/components/organisms/Form/Form.types.d.ts +93 -64
- package/dist/esm/components/organisms/Form/Form.utils.d.ts +2 -0
- package/dist/esm/components/organisms/Form/FormContext.d.ts +5 -0
- package/dist/esm/components/organisms/Form/UpdatedFormExample.d.ts +23 -0
- package/dist/esm/components/organisms/Form/{examples/UseFormExample.d.ts → UseFormExample.d.ts} +3 -0
- package/dist/esm/components/organisms/Form/index.d.ts +3 -7
- package/dist/esm/components/organisms/Form/useForm.d.ts +40 -43
- package/dist/esm/library.js +3 -3
- package/dist/esm/library.js.map +1 -1
- package/dist/index.d.ts +228 -319
- package/docs/components/molecules/FormField.md +34 -129
- package/docs/components/organisms/Form.md +162 -858
- package/package.json +1 -4
- package/dist/cjs/components/molecules/FormField/useSafeController.d.ts +0 -14
- package/dist/cjs/components/molecules/FormField/useSafeFormContext.d.ts +0 -8
- package/dist/cjs/components/organisms/Form/Form.presenter.d.ts +0 -290
- package/dist/cjs/components/organisms/Form/examples/DropzoneFormExample.d.ts +0 -6
- package/dist/cjs/components/organisms/Form/examples/UpdatedFormExample.d.ts +0 -2
- package/dist/cjs/components/organisms/Form/useAutosave.d.ts +0 -10
- package/dist/esm/components/molecules/FormField/useSafeController.d.ts +0 -14
- package/dist/esm/components/molecules/FormField/useSafeFormContext.d.ts +0 -8
- package/dist/esm/components/organisms/Form/Form.presenter.d.ts +0 -290
- package/dist/esm/components/organisms/Form/examples/DropzoneFormExample.d.ts +0 -6
- package/dist/esm/components/organisms/Form/examples/UpdatedFormExample.d.ts +0 -2
- package/dist/esm/components/organisms/Form/useAutosave.d.ts +0 -10
- package/docs/FORM_MIGRATION_GUIDE.md +0 -631
- /package/dist/cjs/components/organisms/Form/{examples/AutosaveFormExample.d.ts → AutosaveFormExample.d.ts} +0 -0
- /package/dist/esm/components/organisms/Form/{examples/AutosaveFormExample.d.ts → AutosaveFormExample.d.ts} +0 -0
|
@@ -1,84 +1,68 @@
|
|
|
1
1
|
# Form
|
|
2
2
|
|
|
3
|
-
> **⚠️ BREAKING CHANGES:** The Form component has been rewritten using React Hook Form. See the [Migration Guide](../../../FORM_MIGRATION_GUIDE.md) for detailed migration instructions.
|
|
4
|
-
|
|
5
3
|
## Description
|
|
6
4
|
|
|
7
|
-
A comprehensive form management component
|
|
5
|
+
A comprehensive form management component that provides state management, validation, error handling, and submission workflows. Built with TypeScript for type safety and supports both controlled and uncontrolled form patterns with extensive validation capabilities.
|
|
8
6
|
|
|
9
7
|
## Aliases
|
|
10
8
|
|
|
11
9
|
- Form
|
|
10
|
+
- FormProvider
|
|
11
|
+
- FormContainer
|
|
12
|
+
- FormWrapper
|
|
12
13
|
|
|
13
14
|
## Props Breakdown
|
|
14
15
|
|
|
16
|
+
**Extends:** `FormHTMLAttributes<HTMLFormElement>` (excluding `onSubmit`)
|
|
17
|
+
|
|
15
18
|
| Prop | Type | Default | Required | Description |
|
|
16
19
|
|------|------|---------|----------|-------------|
|
|
17
|
-
| `children` | `ReactNode` | - | Yes | Form fields and content |
|
|
18
|
-
| `defaultValues` | `object` | - | No | Initial form values (uncontrolled mode) |
|
|
19
|
-
| `onSubmit` | `(values: TFormValues) => void \| Promise<void>` | - | No | Form submission handler (no setError callback) |
|
|
20
|
-
| `autosave` | `boolean` | `false` | No | Enable automatic form submission on value changes |
|
|
21
|
-
| `autosaveDelayMs` | `number` | `1000` | No | Debounce delay in milliseconds before autosave triggers |
|
|
22
|
-
| `mode` | `'onSubmit' \| 'onChange' \| 'onBlur' \| 'onTouched' \| 'all'` | `'onSubmit'` | No | React Hook Form validation mode |
|
|
23
|
-
| `formOptions` | `UseFormProps` | - | No | Additional React Hook Form options |
|
|
24
20
|
| `formRef` | `Ref<HTMLFormElement>` | - | No | Reference to the form element |
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
|
31
|
-
|
|
32
|
-
| `
|
|
33
|
-
| `onFormStateChange` | Use RHF's `watch()` or `useWatch()` hooks |
|
|
34
|
-
| `formValidator` | Use field-level validation or RHF resolver |
|
|
35
|
-
| `validateOnChange` | Use `mode="onChange"` instead |
|
|
36
|
-
|
|
37
|
-
### Key Differences from Old API
|
|
21
|
+
| `children` | `ReactNode` | - | No | Form fields and content |
|
|
22
|
+
| `formState` | `FormState` | - | No | Initial form state values |
|
|
23
|
+
| `onFormStateChange` | `FormStateChangeHandler` | - | No | Callback when form state changes |
|
|
24
|
+
| `formValidator` | `FormValidator` | - | No | Function to validate entire form |
|
|
25
|
+
| `onSubmit` | `FormSubmitHandler` | - | No | Form submission handler |
|
|
26
|
+
| `validateOnChange` | `boolean` | - | No | Whether to validate on field changes |
|
|
27
|
+
| `autosave` | `boolean` | `false` | No | Enable automatic form submission on value changes |
|
|
28
|
+
| `autosaveDelayMs` | `number` | - | No | Delay in milliseconds before autosave triggers |
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
- ✅ **`defaultValues`** replaces `formState` for initial values
|
|
41
|
-
- ✅ **`mode`** prop provides flexible validation timing
|
|
42
|
-
- ✅ Full React Hook Form ecosystem access via hooks
|
|
43
|
-
- ✅ Better TypeScript type safety
|
|
44
|
-
- ✅ Improved performance and smaller bundle size
|
|
30
|
+
Plus all standard HTML form attributes (method, action, encType, target, etc.) except `onSubmit`.
|
|
45
31
|
|
|
46
32
|
## Examples
|
|
47
33
|
|
|
48
34
|
### Basic Form
|
|
49
35
|
```tsx
|
|
50
|
-
import { Form, FormField, Input, Button
|
|
36
|
+
import { Form, FormField, Input, Button } from '@delightui/components';
|
|
51
37
|
|
|
52
38
|
function BasicFormExample() {
|
|
53
|
-
const handleSubmit = (values
|
|
39
|
+
const handleSubmit = (values, setError) => {
|
|
54
40
|
console.log('Form submitted:', values);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return 'Please enter a valid email address';
|
|
41
|
+
|
|
42
|
+
// Example validation
|
|
43
|
+
if (!values.email || !values.email.includes('@')) {
|
|
44
|
+
setError('email', 'Please enter a valid email address');
|
|
45
|
+
return;
|
|
61
46
|
}
|
|
62
|
-
|
|
47
|
+
|
|
48
|
+
// Process form submission
|
|
49
|
+
alert('Form submitted successfully!');
|
|
63
50
|
};
|
|
64
51
|
|
|
65
52
|
return (
|
|
66
|
-
<Form
|
|
67
|
-
defaultValues={{ name: '', email: '', message: '' }}
|
|
68
|
-
onSubmit={handleSubmit}
|
|
69
|
-
>
|
|
53
|
+
<Form onSubmit={handleSubmit}>
|
|
70
54
|
<FormField name="name" label="Full Name" required>
|
|
71
55
|
<Input placeholder="Enter your name" />
|
|
72
56
|
</FormField>
|
|
73
|
-
|
|
74
|
-
<FormField name="email" label="Email Address" required
|
|
57
|
+
|
|
58
|
+
<FormField name="email" label="Email Address" required>
|
|
75
59
|
<Input inputType="Email" placeholder="Enter your email" />
|
|
76
60
|
</FormField>
|
|
77
|
-
|
|
61
|
+
|
|
78
62
|
<FormField name="message" label="Message">
|
|
79
63
|
<TextArea placeholder="Your message..." rows={4} />
|
|
80
64
|
</FormField>
|
|
81
|
-
|
|
65
|
+
|
|
82
66
|
<Button actionType="submit">Submit Form</Button>
|
|
83
67
|
</Form>
|
|
84
68
|
);
|
|
@@ -87,127 +71,115 @@ function BasicFormExample() {
|
|
|
87
71
|
|
|
88
72
|
### Form with Validation
|
|
89
73
|
```tsx
|
|
90
|
-
import { useWatch } from 'react-hook-form';
|
|
91
|
-
|
|
92
|
-
function ConfirmPasswordField() {
|
|
93
|
-
const password = useWatch({ name: 'password' });
|
|
94
|
-
|
|
95
|
-
const validateConfirm = (value: string) => {
|
|
96
|
-
if (value !== password) {
|
|
97
|
-
return 'Passwords do not match';
|
|
98
|
-
}
|
|
99
|
-
return undefined;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<FormField
|
|
104
|
-
name="confirmPassword"
|
|
105
|
-
label="Confirm Password"
|
|
106
|
-
required
|
|
107
|
-
validate={validateConfirm}
|
|
108
|
-
>
|
|
109
|
-
<Input inputType="Password" placeholder="Confirm password" />
|
|
110
|
-
</FormField>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
74
|
function ValidationFormExample() {
|
|
115
|
-
const handleSubmit = (values
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
75
|
+
const handleSubmit = (values, setError) => {
|
|
76
|
+
// Custom validation logic
|
|
77
|
+
let hasErrors = false;
|
|
78
|
+
|
|
79
|
+
if (!values.password || values.password.length < 8) {
|
|
80
|
+
setError('password', 'Password must be at least 8 characters');
|
|
81
|
+
hasErrors = true;
|
|
122
82
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
83
|
+
|
|
84
|
+
if (values.password !== values.confirmPassword) {
|
|
85
|
+
setError('confirmPassword', 'Passwords do not match');
|
|
86
|
+
hasErrors = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!hasErrors) {
|
|
90
|
+
console.log('Creating account:', values);
|
|
129
91
|
}
|
|
130
|
-
return undefined;
|
|
131
92
|
};
|
|
132
93
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
94
|
+
const formValidator = (values, setError) => {
|
|
95
|
+
let isValid = true;
|
|
96
|
+
|
|
97
|
+
// Username validation
|
|
98
|
+
if (!values.username || values.username.length < 3) {
|
|
99
|
+
setError('username', 'Username must be at least 3 characters');
|
|
100
|
+
isValid = false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Email validation
|
|
104
|
+
if (!values.email || !/\S+@\S+\.\S+/.test(values.email)) {
|
|
105
|
+
setError('email', 'Please enter a valid email');
|
|
106
|
+
isValid = false;
|
|
136
107
|
}
|
|
137
|
-
|
|
108
|
+
|
|
109
|
+
return isValid;
|
|
138
110
|
};
|
|
139
111
|
|
|
140
112
|
return (
|
|
141
|
-
<Form
|
|
142
|
-
defaultValues={{ username: '', email: '', password: '', confirmPassword: '' }}
|
|
113
|
+
<Form
|
|
143
114
|
onSubmit={handleSubmit}
|
|
144
|
-
|
|
115
|
+
formValidator={formValidator}
|
|
116
|
+
validateOnChange={true}
|
|
145
117
|
>
|
|
146
|
-
<FormField name="username" label="Username" required
|
|
118
|
+
<FormField name="username" label="Username" required>
|
|
147
119
|
<Input placeholder="Choose a username" />
|
|
148
120
|
</FormField>
|
|
149
|
-
|
|
150
|
-
<FormField name="email" label="Email" required
|
|
121
|
+
|
|
122
|
+
<FormField name="email" label="Email" required>
|
|
151
123
|
<Input inputType="Email" placeholder="your@email.com" />
|
|
152
124
|
</FormField>
|
|
153
|
-
|
|
154
|
-
<FormField name="password" label="Password" required
|
|
125
|
+
|
|
126
|
+
<FormField name="password" label="Password" required>
|
|
155
127
|
<Input inputType="Password" placeholder="Create password" />
|
|
156
128
|
</FormField>
|
|
157
|
-
|
|
158
|
-
<
|
|
159
|
-
|
|
129
|
+
|
|
130
|
+
<FormField name="confirmPassword" label="Confirm Password" required>
|
|
131
|
+
<Input inputType="Password" placeholder="Confirm password" />
|
|
132
|
+
</FormField>
|
|
133
|
+
|
|
160
134
|
<div className="form-actions">
|
|
161
135
|
<Button actionType="submit">Create Account</Button>
|
|
136
|
+
<Button type="Outlined" actionType="reset">Clear Form</Button>
|
|
162
137
|
</div>
|
|
163
138
|
</Form>
|
|
164
139
|
);
|
|
165
140
|
}
|
|
166
141
|
```
|
|
167
142
|
|
|
168
|
-
### Form with Initial
|
|
143
|
+
### Form with Initial State
|
|
169
144
|
```tsx
|
|
170
145
|
function InitialStateFormExample() {
|
|
171
|
-
const
|
|
172
|
-
firstName:
|
|
173
|
-
lastName:
|
|
174
|
-
email:
|
|
175
|
-
role:
|
|
176
|
-
notifications:
|
|
177
|
-
}
|
|
146
|
+
const initialFormState = {
|
|
147
|
+
firstName: 'John',
|
|
148
|
+
lastName: 'Doe',
|
|
149
|
+
email: 'john.doe@example.com',
|
|
150
|
+
role: 'developer',
|
|
151
|
+
notifications: true
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleSubmit = (values, setError) => {
|
|
178
155
|
console.log('Updated profile:', values);
|
|
179
156
|
};
|
|
180
157
|
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
158
|
+
const handleStateChange = (state, setError) => {
|
|
159
|
+
// Validate on state change
|
|
160
|
+
if (state.email && !state.email.includes('@')) {
|
|
161
|
+
setError('email', 'Invalid email format');
|
|
184
162
|
}
|
|
185
|
-
return undefined;
|
|
186
163
|
};
|
|
187
164
|
|
|
188
165
|
return (
|
|
189
|
-
<Form
|
|
190
|
-
|
|
191
|
-
firstName: 'John',
|
|
192
|
-
lastName: 'Doe',
|
|
193
|
-
email: 'john.doe@example.com',
|
|
194
|
-
role: 'developer',
|
|
195
|
-
notifications: true
|
|
196
|
-
}}
|
|
166
|
+
<Form
|
|
167
|
+
formState={initialFormState}
|
|
197
168
|
onSubmit={handleSubmit}
|
|
169
|
+
onFormStateChange={handleStateChange}
|
|
198
170
|
>
|
|
199
171
|
<FormField name="firstName" label="First Name" required>
|
|
200
172
|
<Input />
|
|
201
173
|
</FormField>
|
|
202
|
-
|
|
174
|
+
|
|
203
175
|
<FormField name="lastName" label="Last Name" required>
|
|
204
176
|
<Input />
|
|
205
177
|
</FormField>
|
|
206
|
-
|
|
207
|
-
<FormField name="email" label="Email" required
|
|
178
|
+
|
|
179
|
+
<FormField name="email" label="Email" required>
|
|
208
180
|
<Input inputType="Email" />
|
|
209
181
|
</FormField>
|
|
210
|
-
|
|
182
|
+
|
|
211
183
|
<FormField name="role" label="Role">
|
|
212
184
|
<Select>
|
|
213
185
|
<Option value="developer">Developer</Option>
|
|
@@ -215,11 +187,11 @@ function InitialStateFormExample() {
|
|
|
215
187
|
<Option value="manager">Manager</Option>
|
|
216
188
|
</Select>
|
|
217
189
|
</FormField>
|
|
218
|
-
|
|
190
|
+
|
|
219
191
|
<FormField name="notifications" label="Email Notifications">
|
|
220
192
|
<Checkbox>Send me email notifications</Checkbox>
|
|
221
193
|
</FormField>
|
|
222
|
-
|
|
194
|
+
|
|
223
195
|
<Button actionType="submit">Update Profile</Button>
|
|
224
196
|
</Form>
|
|
225
197
|
);
|
|
@@ -564,24 +536,24 @@ function AsyncFormExample() {
|
|
|
564
536
|
|
|
565
537
|
### Autosave Form
|
|
566
538
|
```tsx
|
|
567
|
-
import { useState } from 'react';
|
|
568
|
-
|
|
569
539
|
function AutosaveFormExample() {
|
|
570
540
|
const [saveCount, setSaveCount] = useState(0);
|
|
571
|
-
const [lastSaved, setLastSaved] = useState
|
|
541
|
+
const [lastSaved, setLastSaved] = useState(null);
|
|
572
542
|
|
|
573
|
-
const
|
|
543
|
+
const handleSubmit = async (values, setError) => {
|
|
574
544
|
try {
|
|
575
545
|
// Simulate API call to save draft
|
|
576
|
-
await fetch('/api/drafts', {
|
|
546
|
+
const response = await fetch('/api/drafts', {
|
|
577
547
|
method: 'POST',
|
|
578
548
|
headers: { 'Content-Type': 'application/json' },
|
|
579
549
|
body: JSON.stringify(values)
|
|
580
550
|
});
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
551
|
+
|
|
552
|
+
if (response.ok) {
|
|
553
|
+
setSaveCount(prev => prev + 1);
|
|
554
|
+
setLastSaved(new Date().toLocaleTimeString());
|
|
555
|
+
console.log('Draft auto-saved:', values);
|
|
556
|
+
}
|
|
585
557
|
} catch (error) {
|
|
586
558
|
console.error('Failed to save draft:', error);
|
|
587
559
|
}
|
|
@@ -589,23 +561,29 @@ function AutosaveFormExample() {
|
|
|
589
561
|
|
|
590
562
|
return (
|
|
591
563
|
<div>
|
|
592
|
-
<Form
|
|
593
|
-
|
|
594
|
-
onSubmit={handleAutosave}
|
|
564
|
+
<Form
|
|
565
|
+
onSubmit={handleSubmit}
|
|
595
566
|
autosave={true}
|
|
596
567
|
autosaveDelayMs={2000} // Wait 2 seconds after user stops typing
|
|
597
568
|
>
|
|
598
569
|
<FormField name="title" label="Title">
|
|
599
570
|
<Input placeholder="Enter title" />
|
|
600
571
|
</FormField>
|
|
601
|
-
|
|
572
|
+
|
|
602
573
|
<FormField name="content" label="Content">
|
|
603
|
-
<TextArea
|
|
604
|
-
placeholder="Start writing..."
|
|
605
|
-
rows={10}
|
|
574
|
+
<TextArea
|
|
575
|
+
placeholder="Start writing..."
|
|
576
|
+
rows={10}
|
|
606
577
|
/>
|
|
607
578
|
</FormField>
|
|
608
|
-
|
|
579
|
+
|
|
580
|
+
<FormField name="tags" label="Tags">
|
|
581
|
+
<ChipInput
|
|
582
|
+
placeholder="Add tags"
|
|
583
|
+
options={['react', 'typescript', 'design', 'tutorial']}
|
|
584
|
+
/>
|
|
585
|
+
</FormField>
|
|
586
|
+
|
|
609
587
|
{lastSaved && (
|
|
610
588
|
<Text type="BodySmall">
|
|
611
589
|
Auto-saved {saveCount} times. Last saved at {lastSaved}
|
|
@@ -617,740 +595,66 @@ function AutosaveFormExample() {
|
|
|
617
595
|
}
|
|
618
596
|
```
|
|
619
597
|
|
|
620
|
-
###
|
|
598
|
+
### Autosave with Manual Submit
|
|
621
599
|
```tsx
|
|
622
|
-
function
|
|
623
|
-
const
|
|
624
|
-
if (!value) return 'Username is required';
|
|
600
|
+
function AutosaveWithManualSubmitExample() {
|
|
601
|
+
const [isDraft, setIsDraft] = useState(true);
|
|
625
602
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
return 'Username is already taken';
|
|
603
|
+
const handleAutosave = (values, setError) => {
|
|
604
|
+
if (isDraft) {
|
|
605
|
+
console.log('Draft auto-saved:', values);
|
|
606
|
+
// Save to local storage or draft API
|
|
607
|
+
localStorage.setItem('formDraft', JSON.stringify(values));
|
|
632
608
|
}
|
|
633
|
-
return undefined;
|
|
634
609
|
};
|
|
635
610
|
|
|
636
|
-
const
|
|
637
|
-
|
|
611
|
+
const handlePublish = () => {
|
|
612
|
+
setIsDraft(false);
|
|
613
|
+
// Form will now submit normally
|
|
638
614
|
};
|
|
639
615
|
|
|
640
616
|
return (
|
|
641
|
-
<Form
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
required
|
|
649
|
-
asyncValidate={validateUsername}
|
|
650
|
-
>
|
|
651
|
-
<Input placeholder="Choose a username" />
|
|
652
|
-
</FormField>
|
|
653
|
-
|
|
654
|
-
<FormField name="email" label="Email" required>
|
|
655
|
-
<Input inputType="Email" placeholder="your@email.com" />
|
|
656
|
-
</FormField>
|
|
657
|
-
|
|
658
|
-
<Button actionType="submit">Create Account</Button>
|
|
659
|
-
</Form>
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
### Using React Hook Form Hooks
|
|
665
|
-
```tsx
|
|
666
|
-
import { useFormContext, useWatch } from 'react-hook-form';
|
|
667
|
-
|
|
668
|
-
function FormDebugger() {
|
|
669
|
-
const { formState } = useFormContext();
|
|
670
|
-
const watchedValues = useWatch();
|
|
671
|
-
|
|
672
|
-
return (
|
|
673
|
-
<div style={{ padding: '10px', background: '#f0f0f0', marginTop: '10px' }}>
|
|
674
|
-
<Text type="BodySmall">
|
|
675
|
-
Form is {formState.isDirty ? 'modified' : 'pristine'}
|
|
676
|
-
</Text>
|
|
677
|
-
<Text type="BodySmall">
|
|
678
|
-
Form is {formState.isValid ? 'valid' : 'invalid'}
|
|
679
|
-
</Text>
|
|
680
|
-
<pre>{JSON.stringify(watchedValues, null, 2)}</pre>
|
|
681
|
-
</div>
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function FormWithHooksExample() {
|
|
686
|
-
const handleSubmit = (values: { email: string; password: string }) => {
|
|
687
|
-
console.log('Submitted:', values);
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
return (
|
|
691
|
-
<Form
|
|
692
|
-
defaultValues={{ email: '', password: '' }}
|
|
693
|
-
onSubmit={handleSubmit}
|
|
694
|
-
mode="onChange"
|
|
695
|
-
>
|
|
696
|
-
<FormField name="email" label="Email" required>
|
|
697
|
-
<Input inputType="Email" />
|
|
698
|
-
</FormField>
|
|
699
|
-
|
|
700
|
-
<FormField name="password" label="Password" required>
|
|
701
|
-
<Input inputType="Password" />
|
|
702
|
-
</FormField>
|
|
703
|
-
|
|
704
|
-
<Button actionType="submit">Sign In</Button>
|
|
705
|
-
|
|
706
|
-
<FormDebugger />
|
|
707
|
-
</Form>
|
|
708
|
-
);
|
|
709
|
-
}
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
### Tracking Dirty Fields
|
|
713
|
-
```tsx
|
|
714
|
-
import { useFormContext } from 'react-hook-form';
|
|
715
|
-
|
|
716
|
-
function DirtyFieldsExample() {
|
|
717
|
-
const handleSubmit = (values: { name: string; email: string; phone: string }) => {
|
|
718
|
-
console.log('Submitted:', values);
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
return (
|
|
722
|
-
<Form
|
|
723
|
-
defaultValues={{ name: 'John Doe', email: 'john@example.com', phone: '' }}
|
|
724
|
-
onSubmit={handleSubmit}
|
|
725
|
-
>
|
|
726
|
-
<FormField name="name" label="Name" required>
|
|
727
|
-
<Input />
|
|
728
|
-
</FormField>
|
|
729
|
-
|
|
730
|
-
<FormField name="email" label="Email" required>
|
|
731
|
-
<Input inputType="Email" />
|
|
732
|
-
</FormField>
|
|
733
|
-
|
|
734
|
-
<FormField name="phone" label="Phone">
|
|
735
|
-
<Input inputType="Tel" />
|
|
736
|
-
</FormField>
|
|
737
|
-
|
|
738
|
-
<DirtyFieldsIndicator />
|
|
739
|
-
|
|
740
|
-
<Button actionType="submit">Save Changes</Button>
|
|
741
|
-
</Form>
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function DirtyFieldsIndicator() {
|
|
746
|
-
const { formState } = useFormContext();
|
|
747
|
-
const { dirtyFields, isDirty } = formState;
|
|
748
|
-
|
|
749
|
-
if (!isDirty) {
|
|
750
|
-
return (
|
|
751
|
-
<div style={{ padding: '10px', background: '#e8f5e9', marginTop: '10px' }}>
|
|
752
|
-
<Text type="BodySmall">No changes made</Text>
|
|
753
|
-
</div>
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const changedFields = Object.keys(dirtyFields).filter(key => dirtyFields[key]);
|
|
758
|
-
|
|
759
|
-
return (
|
|
760
|
-
<div style={{ padding: '10px', background: '#fff3e0', marginTop: '10px' }}>
|
|
761
|
-
<Text type="BodySmall" weight="SemiBold">
|
|
762
|
-
Modified fields: {changedFields.join(', ')}
|
|
763
|
-
</Text>
|
|
764
|
-
<Text type="BodySmall">
|
|
765
|
-
You have unsaved changes in {changedFields.length} field(s)
|
|
766
|
-
</Text>
|
|
767
|
-
</div>
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
### Server-Side Error Handling
|
|
773
|
-
```tsx
|
|
774
|
-
import { useFormContext } from 'react-hook-form';
|
|
775
|
-
|
|
776
|
-
function ServerValidationExample() {
|
|
777
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
778
|
-
|
|
779
|
-
const handleSubmit = async (values: { email: string; username: string }) => {
|
|
780
|
-
setIsSubmitting(true);
|
|
781
|
-
|
|
782
|
-
try {
|
|
783
|
-
const response = await fetch('/api/register', {
|
|
784
|
-
method: 'POST',
|
|
785
|
-
headers: { 'Content-Type': 'application/json' },
|
|
786
|
-
body: JSON.stringify(values)
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
const data = await response.json();
|
|
790
|
-
|
|
791
|
-
if (!response.ok) {
|
|
792
|
-
// Server returned validation errors
|
|
793
|
-
throw data;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
alert('Registration successful!');
|
|
797
|
-
} catch (error: any) {
|
|
798
|
-
// Handle server-side validation errors
|
|
799
|
-
// Error format: { field: 'email', message: 'Email already exists' }
|
|
800
|
-
console.error('Server validation error:', error);
|
|
801
|
-
} finally {
|
|
802
|
-
setIsSubmitting(false);
|
|
803
|
-
}
|
|
804
|
-
};
|
|
805
|
-
|
|
806
|
-
return (
|
|
807
|
-
<Form
|
|
808
|
-
defaultValues={{ email: '', username: '' }}
|
|
809
|
-
onSubmit={handleSubmit}
|
|
810
|
-
>
|
|
811
|
-
<FormField name="email" label="Email" required>
|
|
812
|
-
<Input inputType="Email" />
|
|
813
|
-
</FormField>
|
|
814
|
-
|
|
815
|
-
<FormField name="username" label="Username" required>
|
|
816
|
-
<Input />
|
|
817
|
-
</FormField>
|
|
818
|
-
|
|
819
|
-
<ServerErrorHandler />
|
|
820
|
-
|
|
821
|
-
<Button actionType="submit" loading={isSubmitting}>
|
|
822
|
-
Register
|
|
823
|
-
</Button>
|
|
824
|
-
</Form>
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function ServerErrorHandler() {
|
|
829
|
-
const { setError, clearErrors } = useFormContext();
|
|
830
|
-
|
|
831
|
-
// Example: You could expose a method to set errors from parent
|
|
832
|
-
// or handle them via a context/state management solution
|
|
833
|
-
|
|
834
|
-
const handleServerError = (field: string, message: string) => {
|
|
835
|
-
setError(field, {
|
|
836
|
-
type: 'server',
|
|
837
|
-
message: message
|
|
838
|
-
});
|
|
839
|
-
};
|
|
840
|
-
|
|
841
|
-
const handleClearErrors = () => {
|
|
842
|
-
clearErrors(); // Clear all errors
|
|
843
|
-
// or clearErrors('email'); // Clear specific field
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
return null; // This is a utility component
|
|
847
|
-
}
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
### Dynamic Form Arrays (useFieldArray)
|
|
851
|
-
```tsx
|
|
852
|
-
import { useFieldArray, useFormContext } from 'react-hook-form';
|
|
853
|
-
|
|
854
|
-
interface FormValues {
|
|
855
|
-
name: string;
|
|
856
|
-
emails: { value: string }[];
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
function DynamicFieldArrayExample() {
|
|
860
|
-
const handleSubmit = (values: FormValues) => {
|
|
861
|
-
console.log('Submitted:', values);
|
|
862
|
-
alert(`Name: ${values.name}\nEmails: ${values.emails.map(e => e.value).join(', ')}`);
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
return (
|
|
866
|
-
<Form<FormValues>
|
|
867
|
-
defaultValues={{
|
|
868
|
-
name: '',
|
|
869
|
-
emails: [{ value: '' }] // Start with one email field
|
|
617
|
+
<Form
|
|
618
|
+
onSubmit={handleAutosave}
|
|
619
|
+
autosave={isDraft}
|
|
620
|
+
autosaveDelayMs={1000}
|
|
621
|
+
formState={{
|
|
622
|
+
title: localStorage.getItem('formDraft')?.title || '',
|
|
623
|
+
content: localStorage.getItem('formDraft')?.content || ''
|
|
870
624
|
}}
|
|
871
|
-
onSubmit={handleSubmit}
|
|
872
|
-
>
|
|
873
|
-
<FormField name="name" label="Name" required>
|
|
874
|
-
<Input placeholder="Enter your name" />
|
|
875
|
-
</FormField>
|
|
876
|
-
|
|
877
|
-
<EmailFieldArray />
|
|
878
|
-
|
|
879
|
-
<Button actionType="submit">Submit</Button>
|
|
880
|
-
</Form>
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function EmailFieldArray() {
|
|
885
|
-
const { control } = useFormContext();
|
|
886
|
-
const { fields, append, remove } = useFieldArray({
|
|
887
|
-
control,
|
|
888
|
-
name: 'emails'
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
return (
|
|
892
|
-
<div>
|
|
893
|
-
<Text type="BodyMedium" weight="SemiBold">Email Addresses</Text>
|
|
894
|
-
|
|
895
|
-
{fields.map((field, index) => (
|
|
896
|
-
<div key={field.id} style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
|
|
897
|
-
<FormField
|
|
898
|
-
name={`emails.${index}.value` as any}
|
|
899
|
-
label={`Email ${index + 1}`}
|
|
900
|
-
required
|
|
901
|
-
>
|
|
902
|
-
<Input inputType="Email" placeholder="email@example.com" />
|
|
903
|
-
</FormField>
|
|
904
|
-
|
|
905
|
-
{fields.length > 1 && (
|
|
906
|
-
<Button
|
|
907
|
-
type="Ghost"
|
|
908
|
-
style="Destructive"
|
|
909
|
-
size="Small"
|
|
910
|
-
onClick={() => remove(index)}
|
|
911
|
-
>
|
|
912
|
-
Remove
|
|
913
|
-
</Button>
|
|
914
|
-
)}
|
|
915
|
-
</div>
|
|
916
|
-
))}
|
|
917
|
-
|
|
918
|
-
<Button
|
|
919
|
-
type="Ghost"
|
|
920
|
-
size="Small"
|
|
921
|
-
onClick={() => append({ value: '' })}
|
|
922
|
-
style={{ marginTop: '8px' }}
|
|
923
|
-
>
|
|
924
|
-
Add Email
|
|
925
|
-
</Button>
|
|
926
|
-
</div>
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
### Conditional Validation
|
|
932
|
-
```tsx
|
|
933
|
-
import { useWatch } from 'react-hook-form';
|
|
934
|
-
|
|
935
|
-
function ConditionalValidationExample() {
|
|
936
|
-
const handleSubmit = (values: { contactMethod: string; email?: string; phone?: string }) => {
|
|
937
|
-
console.log('Submitted:', values);
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
return (
|
|
941
|
-
<Form
|
|
942
|
-
defaultValues={{ contactMethod: 'email', email: '', phone: '' }}
|
|
943
|
-
onSubmit={handleSubmit}
|
|
944
|
-
>
|
|
945
|
-
<FormField name="contactMethod" label="Preferred Contact Method">
|
|
946
|
-
<Select>
|
|
947
|
-
<Option value="email">Email</Option>
|
|
948
|
-
<Option value="phone">Phone</Option>
|
|
949
|
-
<Option value="both">Both</Option>
|
|
950
|
-
</Select>
|
|
951
|
-
</FormField>
|
|
952
|
-
|
|
953
|
-
<ConditionalFields />
|
|
954
|
-
|
|
955
|
-
<Button actionType="submit">Submit</Button>
|
|
956
|
-
</Form>
|
|
957
|
-
);
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
function ConditionalFields() {
|
|
961
|
-
const contactMethod = useWatch({ name: 'contactMethod' });
|
|
962
|
-
|
|
963
|
-
const validateEmail = (value: string) => {
|
|
964
|
-
if ((contactMethod === 'email' || contactMethod === 'both') && !value) {
|
|
965
|
-
return 'Email is required for your selected contact method';
|
|
966
|
-
}
|
|
967
|
-
if (value && !value.includes('@')) {
|
|
968
|
-
return 'Invalid email format';
|
|
969
|
-
}
|
|
970
|
-
return undefined;
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
const validatePhone = (value: string) => {
|
|
974
|
-
if ((contactMethod === 'phone' || contactMethod === 'both') && !value) {
|
|
975
|
-
return 'Phone is required for your selected contact method';
|
|
976
|
-
}
|
|
977
|
-
return undefined;
|
|
978
|
-
};
|
|
979
|
-
|
|
980
|
-
return (
|
|
981
|
-
<>
|
|
982
|
-
{(contactMethod === 'email' || contactMethod === 'both') && (
|
|
983
|
-
<FormField
|
|
984
|
-
name="email"
|
|
985
|
-
label="Email"
|
|
986
|
-
required={contactMethod === 'email' || contactMethod === 'both'}
|
|
987
|
-
validate={validateEmail}
|
|
988
|
-
>
|
|
989
|
-
<Input inputType="Email" placeholder="your@email.com" />
|
|
990
|
-
</FormField>
|
|
991
|
-
)}
|
|
992
|
-
|
|
993
|
-
{(contactMethod === 'phone' || contactMethod === 'both') && (
|
|
994
|
-
<FormField
|
|
995
|
-
name="phone"
|
|
996
|
-
label="Phone"
|
|
997
|
-
required={contactMethod === 'phone' || contactMethod === 'both'}
|
|
998
|
-
validate={validatePhone}
|
|
999
|
-
>
|
|
1000
|
-
<Input inputType="Tel" placeholder="(555) 123-4567" />
|
|
1001
|
-
</FormField>
|
|
1002
|
-
)}
|
|
1003
|
-
</>
|
|
1004
|
-
);
|
|
1005
|
-
}
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
### Reset with Specific Values
|
|
1009
|
-
```tsx
|
|
1010
|
-
import { useFormContext } from 'react-hook-form';
|
|
1011
|
-
|
|
1012
|
-
function ResetWithValuesExample() {
|
|
1013
|
-
const savedData = {
|
|
1014
|
-
name: 'John Doe',
|
|
1015
|
-
email: 'john@example.com',
|
|
1016
|
-
bio: 'Software developer'
|
|
1017
|
-
};
|
|
1018
|
-
|
|
1019
|
-
const handleSubmit = (values: typeof savedData) => {
|
|
1020
|
-
console.log('Saved:', values);
|
|
1021
|
-
alert('Profile updated!');
|
|
1022
|
-
};
|
|
1023
|
-
|
|
1024
|
-
return (
|
|
1025
|
-
<Form
|
|
1026
|
-
defaultValues={savedData}
|
|
1027
|
-
onSubmit={handleSubmit}
|
|
1028
625
|
>
|
|
1029
|
-
<FormField name="
|
|
1030
|
-
<Input />
|
|
1031
|
-
</FormField>
|
|
1032
|
-
|
|
1033
|
-
<FormField name="email" label="Email" required>
|
|
1034
|
-
<Input inputType="Email" />
|
|
1035
|
-
</FormField>
|
|
1036
|
-
|
|
1037
|
-
<FormField name="bio" label="Bio">
|
|
1038
|
-
<TextArea rows={4} />
|
|
626
|
+
<FormField name="title" label="Article Title" required>
|
|
627
|
+
<Input placeholder="Enter title" />
|
|
1039
628
|
</FormField>
|
|
1040
|
-
|
|
1041
|
-
<
|
|
1042
|
-
|
|
1043
|
-
<Button actionType="submit">Save Changes</Button>
|
|
1044
|
-
</Form>
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function ResetButtons({ savedData }: { savedData: any }) {
|
|
1049
|
-
const { reset, formState } = useFormContext();
|
|
1050
|
-
|
|
1051
|
-
return (
|
|
1052
|
-
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
|
|
1053
|
-
<Button
|
|
1054
|
-
type="Outlined"
|
|
1055
|
-
onClick={() => reset(savedData)}
|
|
1056
|
-
disabled={!formState.isDirty}
|
|
1057
|
-
>
|
|
1058
|
-
Discard Changes
|
|
1059
|
-
</Button>
|
|
1060
|
-
|
|
1061
|
-
<Button
|
|
1062
|
-
type="Ghost"
|
|
1063
|
-
onClick={() => reset({ name: '', email: '', bio: '' })}
|
|
1064
|
-
>
|
|
1065
|
-
Clear All
|
|
1066
|
-
</Button>
|
|
1067
|
-
</div>
|
|
1068
|
-
);
|
|
1069
|
-
}
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
### Watching Specific Fields
|
|
1073
|
-
```tsx
|
|
1074
|
-
import { useWatch } from 'react-hook-form';
|
|
1075
|
-
|
|
1076
|
-
function WatchSpecificFieldsExample() {
|
|
1077
|
-
const handleSubmit = (values: { country: string; state: string; city: string }) => {
|
|
1078
|
-
console.log('Submitted:', values);
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
|
-
return (
|
|
1082
|
-
<Form
|
|
1083
|
-
defaultValues={{ country: '', state: '', city: '' }}
|
|
1084
|
-
onSubmit={handleSubmit}
|
|
1085
|
-
>
|
|
1086
|
-
<FormField name="country" label="Country" required>
|
|
1087
|
-
<Select>
|
|
1088
|
-
<Option value="">Select a country</Option>
|
|
1089
|
-
<Option value="us">United States</Option>
|
|
1090
|
-
<Option value="canada">Canada</Option>
|
|
1091
|
-
<Option value="uk">United Kingdom</Option>
|
|
1092
|
-
</Select>
|
|
629
|
+
|
|
630
|
+
<FormField name="content" label="Article Content" required>
|
|
631
|
+
<TextArea placeholder="Write your article..." rows={15} />
|
|
1093
632
|
</FormField>
|
|
1094
|
-
|
|
1095
|
-
<
|
|
1096
|
-
|
|
1097
|
-
<Button actionType="submit">Submit</Button>
|
|
1098
|
-
</Form>
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function DependentFields() {
|
|
1103
|
-
// Watch only the country field
|
|
1104
|
-
const country = useWatch({ name: 'country' });
|
|
1105
|
-
const { setValue } = useFormContext();
|
|
1106
|
-
|
|
1107
|
-
// Reset dependent fields when country changes
|
|
1108
|
-
useEffect(() => {
|
|
1109
|
-
setValue('state', '');
|
|
1110
|
-
setValue('city', '');
|
|
1111
|
-
}, [country, setValue]);
|
|
1112
|
-
|
|
1113
|
-
if (!country) return null;
|
|
1114
|
-
|
|
1115
|
-
return (
|
|
1116
|
-
<>
|
|
1117
|
-
<FormField name="state" label="State/Province" required>
|
|
633
|
+
|
|
634
|
+
<FormField name="category" label="Category">
|
|
1118
635
|
<Select>
|
|
1119
|
-
<Option value="">
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
<Option value="ca">California</Option>
|
|
1123
|
-
<Option value="ny">New York</Option>
|
|
1124
|
-
<Option value="tx">Texas</Option>
|
|
1125
|
-
</>
|
|
1126
|
-
)}
|
|
1127
|
-
{country === 'canada' && (
|
|
1128
|
-
<>
|
|
1129
|
-
<Option value="on">Ontario</Option>
|
|
1130
|
-
<Option value="qc">Quebec</Option>
|
|
1131
|
-
<Option value="bc">British Columbia</Option>
|
|
1132
|
-
</>
|
|
1133
|
-
)}
|
|
636
|
+
<Option value="tech">Technology</Option>
|
|
637
|
+
<Option value="design">Design</Option>
|
|
638
|
+
<Option value="business">Business</Option>
|
|
1134
639
|
</Select>
|
|
1135
640
|
</FormField>
|
|
1136
|
-
|
|
1137
|
-
<
|
|
1138
|
-
<
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
}
|
|
1143
|
-
```
|
|
1144
|
-
|
|
1145
|
-
### Manual Validation Triggering
|
|
1146
|
-
```tsx
|
|
1147
|
-
import { useFormContext } from 'react-hook-form';
|
|
1148
|
-
|
|
1149
|
-
function ManualValidationExample() {
|
|
1150
|
-
const [isChecking, setIsChecking] = useState(false);
|
|
1151
|
-
const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
|
|
1152
|
-
|
|
1153
|
-
const handleSubmit = (values: { username: string; email: string }) => {
|
|
1154
|
-
console.log('Submitted:', values);
|
|
1155
|
-
};
|
|
1156
|
-
|
|
1157
|
-
return (
|
|
1158
|
-
<Form
|
|
1159
|
-
defaultValues={{ username: '', email: '' }}
|
|
1160
|
-
onSubmit={handleSubmit}
|
|
1161
|
-
>
|
|
1162
|
-
<UsernameFieldWithCheck
|
|
1163
|
-
isChecking={isChecking}
|
|
1164
|
-
setIsChecking={setIsChecking}
|
|
1165
|
-
isAvailable={isAvailable}
|
|
1166
|
-
setIsAvailable={setIsAvailable}
|
|
1167
|
-
/>
|
|
1168
|
-
|
|
1169
|
-
<FormField name="email" label="Email" required>
|
|
1170
|
-
<Input inputType="Email" />
|
|
1171
|
-
</FormField>
|
|
1172
|
-
|
|
1173
|
-
<Button actionType="submit">Register</Button>
|
|
1174
|
-
</Form>
|
|
1175
|
-
);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function UsernameFieldWithCheck({
|
|
1179
|
-
isChecking,
|
|
1180
|
-
setIsChecking,
|
|
1181
|
-
isAvailable,
|
|
1182
|
-
setIsAvailable
|
|
1183
|
-
}: {
|
|
1184
|
-
isChecking: boolean;
|
|
1185
|
-
setIsChecking: (value: boolean) => void;
|
|
1186
|
-
isAvailable: boolean | null;
|
|
1187
|
-
setIsAvailable: (value: boolean | null) => void;
|
|
1188
|
-
}) {
|
|
1189
|
-
const { trigger, getValues, setError, clearErrors } = useFormContext();
|
|
1190
|
-
|
|
1191
|
-
const checkAvailability = async () => {
|
|
1192
|
-
// First, validate the username field
|
|
1193
|
-
const isValid = await trigger('username');
|
|
1194
|
-
|
|
1195
|
-
if (!isValid) {
|
|
1196
|
-
return; // Don't check if validation fails
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
setIsChecking(true);
|
|
1200
|
-
setIsAvailable(null);
|
|
1201
|
-
|
|
1202
|
-
try {
|
|
1203
|
-
const username = getValues('username');
|
|
1204
|
-
const response = await fetch(`/api/check-username?username=${username}`);
|
|
1205
|
-
const { available } = await response.json();
|
|
1206
|
-
|
|
1207
|
-
setIsAvailable(available);
|
|
1208
|
-
|
|
1209
|
-
if (!available) {
|
|
1210
|
-
setError('username', {
|
|
1211
|
-
type: 'manual',
|
|
1212
|
-
message: 'Username is already taken'
|
|
1213
|
-
});
|
|
1214
|
-
} else {
|
|
1215
|
-
clearErrors('username');
|
|
1216
|
-
}
|
|
1217
|
-
} catch (error) {
|
|
1218
|
-
console.error('Failed to check username:', error);
|
|
1219
|
-
} finally {
|
|
1220
|
-
setIsChecking(false);
|
|
1221
|
-
}
|
|
1222
|
-
};
|
|
1223
|
-
|
|
1224
|
-
return (
|
|
1225
|
-
<div>
|
|
1226
|
-
<FormField
|
|
1227
|
-
name="username"
|
|
1228
|
-
label="Username"
|
|
1229
|
-
required
|
|
1230
|
-
validate={(value) => {
|
|
1231
|
-
if (value.length < 3) {
|
|
1232
|
-
return 'Username must be at least 3 characters';
|
|
1233
|
-
}
|
|
1234
|
-
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
|
|
1235
|
-
return 'Username can only contain letters, numbers, and underscores';
|
|
1236
|
-
}
|
|
1237
|
-
return undefined;
|
|
1238
|
-
}}
|
|
1239
|
-
>
|
|
1240
|
-
<Input placeholder="Choose a username" />
|
|
1241
|
-
</FormField>
|
|
1242
|
-
|
|
1243
|
-
<div style={{ marginTop: '4px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1244
|
-
<Button
|
|
1245
|
-
type="Ghost"
|
|
1246
|
-
size="Small"
|
|
1247
|
-
onClick={checkAvailability}
|
|
1248
|
-
loading={isChecking}
|
|
641
|
+
|
|
642
|
+
<div className="form-actions">
|
|
643
|
+
<Button type="Outlined">Save as Draft</Button>
|
|
644
|
+
<Button
|
|
645
|
+
actionType="submit"
|
|
646
|
+
onClick={handlePublish}
|
|
1249
647
|
>
|
|
1250
|
-
|
|
648
|
+
Publish Article
|
|
1251
649
|
</Button>
|
|
1252
|
-
|
|
1253
|
-
{isAvailable === true && (
|
|
1254
|
-
<Text type="BodySmall" style={{ color: 'green' }}>
|
|
1255
|
-
✓ Username is available
|
|
1256
|
-
</Text>
|
|
1257
|
-
)}
|
|
1258
|
-
{isAvailable === false && (
|
|
1259
|
-
<Text type="BodySmall" style={{ color: 'red' }}>
|
|
1260
|
-
✗ Username is taken
|
|
1261
|
-
</Text>
|
|
1262
|
-
)}
|
|
1263
650
|
</div>
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
```tsx
|
|
1271
|
-
import { useFormContext } from 'react-hook-form';
|
|
1272
|
-
|
|
1273
|
-
function FormStateReferenceExample() {
|
|
1274
|
-
const handleSubmit = (values: { name: string; email: string }) => {
|
|
1275
|
-
console.log('Submitted:', values);
|
|
1276
|
-
};
|
|
1277
|
-
|
|
1278
|
-
return (
|
|
1279
|
-
<Form
|
|
1280
|
-
defaultValues={{ name: '', email: '' }}
|
|
1281
|
-
onSubmit={handleSubmit}
|
|
1282
|
-
mode="onChange"
|
|
1283
|
-
>
|
|
1284
|
-
<FormField name="name" label="Name" required>
|
|
1285
|
-
<Input />
|
|
1286
|
-
</FormField>
|
|
1287
|
-
|
|
1288
|
-
<FormField name="email" label="Email" required>
|
|
1289
|
-
<Input inputType="Email" />
|
|
1290
|
-
</FormField>
|
|
1291
|
-
|
|
1292
|
-
<FormStateDisplay />
|
|
1293
|
-
|
|
1294
|
-
<Button actionType="submit">Submit</Button>
|
|
651
|
+
|
|
652
|
+
{isDraft && (
|
|
653
|
+
<Text type="BodySmall" style={{ marginTop: '8px' }}>
|
|
654
|
+
Your changes are automatically saved as you type
|
|
655
|
+
</Text>
|
|
656
|
+
)}
|
|
1295
657
|
</Form>
|
|
1296
658
|
);
|
|
1297
659
|
}
|
|
1298
|
-
|
|
1299
|
-
function FormStateDisplay() {
|
|
1300
|
-
const { formState } = useFormContext();
|
|
1301
|
-
const {
|
|
1302
|
-
isDirty, // true if any field has been modified
|
|
1303
|
-
isValid, // true if no validation errors
|
|
1304
|
-
isSubmitting, // true during async submission
|
|
1305
|
-
isSubmitted, // true if form has been submitted
|
|
1306
|
-
isSubmitSuccessful, // true if submission was successful
|
|
1307
|
-
submitCount, // number of times form has been submitted
|
|
1308
|
-
touchedFields, // fields that have been focused and blurred
|
|
1309
|
-
dirtyFields, // fields that have been modified
|
|
1310
|
-
errors // current validation errors
|
|
1311
|
-
} = formState;
|
|
1312
|
-
|
|
1313
|
-
return (
|
|
1314
|
-
<div style={{
|
|
1315
|
-
padding: '12px',
|
|
1316
|
-
background: '#f5f5f5',
|
|
1317
|
-
borderRadius: '4px',
|
|
1318
|
-
marginTop: '16px',
|
|
1319
|
-
fontSize: '13px'
|
|
1320
|
-
}}>
|
|
1321
|
-
<Text type="BodySmall" weight="SemiBold">Form State Properties:</Text>
|
|
1322
|
-
|
|
1323
|
-
<div style={{ marginTop: '8px', display: 'grid', gap: '4px' }}>
|
|
1324
|
-
<div><strong>isDirty:</strong> {String(isDirty)}</div>
|
|
1325
|
-
<div><strong>isValid:</strong> {String(isValid)}</div>
|
|
1326
|
-
<div><strong>isSubmitting:</strong> {String(isSubmitting)}</div>
|
|
1327
|
-
<div><strong>isSubmitted:</strong> {String(isSubmitted)}</div>
|
|
1328
|
-
<div><strong>isSubmitSuccessful:</strong> {String(isSubmitSuccessful)}</div>
|
|
1329
|
-
<div><strong>submitCount:</strong> {submitCount}</div>
|
|
1330
|
-
<div><strong>touchedFields:</strong> {JSON.stringify(touchedFields)}</div>
|
|
1331
|
-
<div><strong>dirtyFields:</strong> {JSON.stringify(dirtyFields)}</div>
|
|
1332
|
-
<div><strong>errors:</strong> {JSON.stringify(errors)}</div>
|
|
1333
|
-
</div>
|
|
1334
|
-
|
|
1335
|
-
<div style={{ marginTop: '12px', padding: '8px', background: '#e3f2fd' }}>
|
|
1336
|
-
<Text type="BodySmall" weight="SemiBold">Common Patterns:</Text>
|
|
1337
|
-
<ul style={{ margin: '8px 0', paddingLeft: '20px', fontSize: '12px' }}>
|
|
1338
|
-
<li>Disable submit button: <code>disabled={`{!isDirty || !isValid}`}</code></li>
|
|
1339
|
-
<li>Show loading: <code>loading={`{isSubmitting}`}</code></li>
|
|
1340
|
-
<li>Show success message: <code>{`{isSubmitSuccessful && '✓ Saved!'}`}</code></li>
|
|
1341
|
-
<li>Warn on navigation: <code>{`{isDirty && 'Unsaved changes'}`}</code></li>
|
|
1342
|
-
</ul>
|
|
1343
|
-
</div>
|
|
1344
|
-
</div>
|
|
1345
|
-
);
|
|
1346
|
-
}
|
|
1347
|
-
```
|
|
1348
|
-
|
|
1349
|
-
---
|
|
1350
|
-
|
|
1351
|
-
## Additional Resources
|
|
1352
|
-
|
|
1353
|
-
- [React Hook Form Documentation](https://react-hook-form.com/)
|
|
1354
|
-
- [Migration Guide](../../../FORM_MIGRATION_GUIDE.md)
|
|
1355
|
-
- [FormField Documentation](../../molecules/FormField.md)
|
|
1356
|
-
- [Example Files](../../../src/components/organisms/Form/examples/)
|
|
660
|
+
```
|