@delightui/components 0.1.161 → 0.1.162-alpha.0
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 +28 -2
- package/dist/cjs/components/molecules/FormField/FormField.presenter.d.ts +14 -8
- package/dist/cjs/components/molecules/FormField/FormField.types.d.ts +85 -52
- package/dist/cjs/components/molecules/FormField/FormField.utils.d.ts +8 -11
- package/dist/cjs/components/molecules/FormField/index.d.ts +1 -2
- package/dist/cjs/components/molecules/FormField/useSafeController.d.ts +14 -0
- package/dist/cjs/components/molecules/FormField/useSafeFormContext.d.ts +8 -0
- 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/Form.d.ts +22 -1
- package/dist/cjs/components/organisms/Form/Form.presenter.d.ts +21 -0
- package/dist/cjs/components/organisms/Form/Form.types.d.ts +27 -131
- package/dist/cjs/components/organisms/Form/examples/DropzoneFormExample.d.ts +6 -0
- package/dist/cjs/components/organisms/Form/examples/UpdatedFormExample.d.ts +2 -0
- package/dist/cjs/components/organisms/Form/{UseFormExample.d.ts → examples/UseFormExample.d.ts} +0 -3
- package/dist/cjs/components/organisms/Form/index.d.ts +4 -4
- package/dist/cjs/components/organisms/Form/useAutosave.d.ts +10 -0
- 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 +28 -2
- package/dist/esm/components/molecules/FormField/FormField.presenter.d.ts +14 -8
- package/dist/esm/components/molecules/FormField/FormField.types.d.ts +85 -52
- package/dist/esm/components/molecules/FormField/FormField.utils.d.ts +8 -11
- package/dist/esm/components/molecules/FormField/index.d.ts +1 -2
- package/dist/esm/components/molecules/FormField/useSafeController.d.ts +14 -0
- package/dist/esm/components/molecules/FormField/useSafeFormContext.d.ts +8 -0
- 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/Form.d.ts +22 -1
- package/dist/esm/components/organisms/Form/Form.presenter.d.ts +21 -0
- package/dist/esm/components/organisms/Form/Form.types.d.ts +27 -131
- package/dist/esm/components/organisms/Form/examples/DropzoneFormExample.d.ts +6 -0
- package/dist/esm/components/organisms/Form/examples/UpdatedFormExample.d.ts +2 -0
- package/dist/esm/components/organisms/Form/{UseFormExample.d.ts → examples/UseFormExample.d.ts} +0 -3
- package/dist/esm/components/organisms/Form/index.d.ts +4 -4
- package/dist/esm/components/organisms/Form/useAutosave.d.ts +10 -0
- package/dist/esm/library.js +3 -3
- package/dist/esm/library.js.map +1 -1
- package/dist/index.d.ts +165 -232
- package/docs/FORM_MIGRATION_GUIDE.md +631 -0
- package/docs/components/molecules/FormField.md +129 -34
- package/docs/components/organisms/Form.md +858 -162
- package/package.json +4 -1
- package/dist/cjs/components/organisms/Form/DropzoneFormExample.d.ts +0 -6
- package/dist/cjs/components/organisms/Form/Form.utils.d.ts +0 -2
- package/dist/cjs/components/organisms/Form/FormContext.d.ts +0 -5
- package/dist/cjs/components/organisms/Form/UpdatedFormExample.d.ts +0 -23
- package/dist/cjs/components/organisms/Form/useForm.d.ts +0 -50
- package/dist/esm/components/organisms/Form/DropzoneFormExample.d.ts +0 -6
- package/dist/esm/components/organisms/Form/Form.utils.d.ts +0 -2
- package/dist/esm/components/organisms/Form/FormContext.d.ts +0 -5
- package/dist/esm/components/organisms/Form/UpdatedFormExample.d.ts +0 -23
- package/dist/esm/components/organisms/Form/useForm.d.ts +0 -50
- /package/dist/cjs/components/organisms/Form/{AutosaveFormExample.d.ts → examples/AutosaveFormExample.d.ts} +0 -0
- /package/dist/esm/components/organisms/Form/{AutosaveFormExample.d.ts → examples/AutosaveFormExample.d.ts} +0 -0
|
@@ -1,68 +1,84 @@
|
|
|
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
|
+
|
|
3
5
|
## Description
|
|
4
6
|
|
|
5
|
-
A comprehensive form management component that provides state management, validation, error handling, and submission workflows.
|
|
7
|
+
A comprehensive form management component built on **React Hook Form** that provides state management, validation, error handling, and submission workflows. Features TypeScript support, autosave functionality, async validation, and seamless integration with the FormField component.
|
|
6
8
|
|
|
7
9
|
## Aliases
|
|
8
10
|
|
|
9
11
|
- Form
|
|
10
|
-
- FormProvider
|
|
11
|
-
- FormContainer
|
|
12
|
-
- FormWrapper
|
|
13
12
|
|
|
14
13
|
## Props Breakdown
|
|
15
14
|
|
|
16
|
-
**Extends:** `FormHTMLAttributes<HTMLFormElement>` (excluding `onSubmit`)
|
|
17
|
-
|
|
18
15
|
| Prop | Type | Default | Required | Description |
|
|
19
16
|
|------|------|---------|----------|-------------|
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
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 |
|
|
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) |
|
|
27
20
|
| `autosave` | `boolean` | `false` | No | Enable automatic form submission on value changes |
|
|
28
|
-
| `autosaveDelayMs` | `number` |
|
|
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
|
+
| `formRef` | `Ref<HTMLFormElement>` | - | No | Reference to the form element |
|
|
25
|
+
| `className` | `string` | - | No | CSS class for the form element |
|
|
26
|
+
| `style` | `CSSProperties` | - | No | Inline styles for the form element |
|
|
27
|
+
|
|
28
|
+
### Removed Props (Breaking Changes)
|
|
29
|
+
|
|
30
|
+
| OLD Prop | Migration Path |
|
|
31
|
+
|----------|----------------|
|
|
32
|
+
| `formState` | Use `defaultValues` instead |
|
|
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
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
- ✅ **`onSubmit`** now receives only `values` (no `setError` callback)
|
|
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
|
|
31
45
|
|
|
32
46
|
## Examples
|
|
33
47
|
|
|
34
48
|
### Basic Form
|
|
35
49
|
```tsx
|
|
36
|
-
import { Form, FormField, Input, Button } from '@delightui/components';
|
|
50
|
+
import { Form, FormField, Input, Button, TextArea } from '@delightui/components';
|
|
37
51
|
|
|
38
52
|
function BasicFormExample() {
|
|
39
|
-
const handleSubmit = (values
|
|
53
|
+
const handleSubmit = (values: { name: string; email: string; message: string }) => {
|
|
40
54
|
console.log('Form submitted:', values);
|
|
41
|
-
|
|
42
|
-
// Example validation
|
|
43
|
-
if (!values.email || !values.email.includes('@')) {
|
|
44
|
-
setError('email', 'Please enter a valid email address');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Process form submission
|
|
49
55
|
alert('Form submitted successfully!');
|
|
50
56
|
};
|
|
51
57
|
|
|
58
|
+
const validateEmail = (value: string) => {
|
|
59
|
+
if (!value.includes('@')) {
|
|
60
|
+
return 'Please enter a valid email address';
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
52
65
|
return (
|
|
53
|
-
<Form
|
|
66
|
+
<Form
|
|
67
|
+
defaultValues={{ name: '', email: '', message: '' }}
|
|
68
|
+
onSubmit={handleSubmit}
|
|
69
|
+
>
|
|
54
70
|
<FormField name="name" label="Full Name" required>
|
|
55
71
|
<Input placeholder="Enter your name" />
|
|
56
72
|
</FormField>
|
|
57
|
-
|
|
58
|
-
<FormField name="email" label="Email Address" required>
|
|
73
|
+
|
|
74
|
+
<FormField name="email" label="Email Address" required validate={validateEmail}>
|
|
59
75
|
<Input inputType="Email" placeholder="Enter your email" />
|
|
60
76
|
</FormField>
|
|
61
|
-
|
|
77
|
+
|
|
62
78
|
<FormField name="message" label="Message">
|
|
63
79
|
<TextArea placeholder="Your message..." rows={4} />
|
|
64
80
|
</FormField>
|
|
65
|
-
|
|
81
|
+
|
|
66
82
|
<Button actionType="submit">Submit Form</Button>
|
|
67
83
|
</Form>
|
|
68
84
|
);
|
|
@@ -71,115 +87,127 @@ function BasicFormExample() {
|
|
|
71
87
|
|
|
72
88
|
### Form with Validation
|
|
73
89
|
```tsx
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (values.password !== values.confirmPassword) {
|
|
85
|
-
setError('confirmPassword', 'Passwords do not match');
|
|
86
|
-
hasErrors = true;
|
|
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';
|
|
87
98
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
function ValidationFormExample() {
|
|
115
|
+
const handleSubmit = (values: { username: string; email: string; password: string; confirmPassword: string }) => {
|
|
116
|
+
console.log('Creating account:', values);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const validateUsername = (value: string) => {
|
|
120
|
+
if (value.length < 3) {
|
|
121
|
+
return 'Username must be at least 3 characters';
|
|
91
122
|
}
|
|
123
|
+
return undefined;
|
|
92
124
|
};
|
|
93
125
|
|
|
94
|
-
const
|
|
95
|
-
|
|
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;
|
|
126
|
+
const validateEmail = (value: string) => {
|
|
127
|
+
if (!/\S+@\S+\.\S+/.test(value)) {
|
|
128
|
+
return 'Please enter a valid email';
|
|
101
129
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
return undefined;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const validatePassword = (value: string) => {
|
|
134
|
+
if (value.length < 8) {
|
|
135
|
+
return 'Password must be at least 8 characters';
|
|
107
136
|
}
|
|
108
|
-
|
|
109
|
-
return isValid;
|
|
137
|
+
return undefined;
|
|
110
138
|
};
|
|
111
139
|
|
|
112
140
|
return (
|
|
113
|
-
<Form
|
|
141
|
+
<Form
|
|
142
|
+
defaultValues={{ username: '', email: '', password: '', confirmPassword: '' }}
|
|
114
143
|
onSubmit={handleSubmit}
|
|
115
|
-
|
|
116
|
-
validateOnChange={true}
|
|
144
|
+
mode="onChange" // Validate on every change
|
|
117
145
|
>
|
|
118
|
-
<FormField name="username" label="Username" required>
|
|
146
|
+
<FormField name="username" label="Username" required validate={validateUsername}>
|
|
119
147
|
<Input placeholder="Choose a username" />
|
|
120
148
|
</FormField>
|
|
121
|
-
|
|
122
|
-
<FormField name="email" label="Email" required>
|
|
149
|
+
|
|
150
|
+
<FormField name="email" label="Email" required validate={validateEmail}>
|
|
123
151
|
<Input inputType="Email" placeholder="your@email.com" />
|
|
124
152
|
</FormField>
|
|
125
|
-
|
|
126
|
-
<FormField name="password" label="Password" required>
|
|
153
|
+
|
|
154
|
+
<FormField name="password" label="Password" required validate={validatePassword}>
|
|
127
155
|
<Input inputType="Password" placeholder="Create password" />
|
|
128
156
|
</FormField>
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
</FormField>
|
|
133
|
-
|
|
157
|
+
|
|
158
|
+
<ConfirmPasswordField />
|
|
159
|
+
|
|
134
160
|
<div className="form-actions">
|
|
135
161
|
<Button actionType="submit">Create Account</Button>
|
|
136
|
-
<Button type="Outlined" actionType="reset">Clear Form</Button>
|
|
137
162
|
</div>
|
|
138
163
|
</Form>
|
|
139
164
|
);
|
|
140
165
|
}
|
|
141
166
|
```
|
|
142
167
|
|
|
143
|
-
### Form with Initial
|
|
168
|
+
### Form with Initial Values
|
|
144
169
|
```tsx
|
|
145
170
|
function InitialStateFormExample() {
|
|
146
|
-
const
|
|
147
|
-
firstName:
|
|
148
|
-
lastName:
|
|
149
|
-
email:
|
|
150
|
-
role:
|
|
151
|
-
notifications:
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const handleSubmit = (values, setError) => {
|
|
171
|
+
const handleSubmit = (values: {
|
|
172
|
+
firstName: string;
|
|
173
|
+
lastName: string;
|
|
174
|
+
email: string;
|
|
175
|
+
role: string;
|
|
176
|
+
notifications: boolean;
|
|
177
|
+
}) => {
|
|
155
178
|
console.log('Updated profile:', values);
|
|
156
179
|
};
|
|
157
180
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
setError('email', 'Invalid email format');
|
|
181
|
+
const validateEmail = (value: string) => {
|
|
182
|
+
if (!value.includes('@')) {
|
|
183
|
+
return 'Invalid email format';
|
|
162
184
|
}
|
|
185
|
+
return undefined;
|
|
163
186
|
};
|
|
164
187
|
|
|
165
188
|
return (
|
|
166
|
-
<Form
|
|
167
|
-
|
|
189
|
+
<Form
|
|
190
|
+
defaultValues={{
|
|
191
|
+
firstName: 'John',
|
|
192
|
+
lastName: 'Doe',
|
|
193
|
+
email: 'john.doe@example.com',
|
|
194
|
+
role: 'developer',
|
|
195
|
+
notifications: true
|
|
196
|
+
}}
|
|
168
197
|
onSubmit={handleSubmit}
|
|
169
|
-
onFormStateChange={handleStateChange}
|
|
170
198
|
>
|
|
171
199
|
<FormField name="firstName" label="First Name" required>
|
|
172
200
|
<Input />
|
|
173
201
|
</FormField>
|
|
174
|
-
|
|
202
|
+
|
|
175
203
|
<FormField name="lastName" label="Last Name" required>
|
|
176
204
|
<Input />
|
|
177
205
|
</FormField>
|
|
178
|
-
|
|
179
|
-
<FormField name="email" label="Email" required>
|
|
206
|
+
|
|
207
|
+
<FormField name="email" label="Email" required validate={validateEmail}>
|
|
180
208
|
<Input inputType="Email" />
|
|
181
209
|
</FormField>
|
|
182
|
-
|
|
210
|
+
|
|
183
211
|
<FormField name="role" label="Role">
|
|
184
212
|
<Select>
|
|
185
213
|
<Option value="developer">Developer</Option>
|
|
@@ -187,11 +215,11 @@ function InitialStateFormExample() {
|
|
|
187
215
|
<Option value="manager">Manager</Option>
|
|
188
216
|
</Select>
|
|
189
217
|
</FormField>
|
|
190
|
-
|
|
218
|
+
|
|
191
219
|
<FormField name="notifications" label="Email Notifications">
|
|
192
220
|
<Checkbox>Send me email notifications</Checkbox>
|
|
193
221
|
</FormField>
|
|
194
|
-
|
|
222
|
+
|
|
195
223
|
<Button actionType="submit">Update Profile</Button>
|
|
196
224
|
</Form>
|
|
197
225
|
);
|
|
@@ -536,24 +564,24 @@ function AsyncFormExample() {
|
|
|
536
564
|
|
|
537
565
|
### Autosave Form
|
|
538
566
|
```tsx
|
|
567
|
+
import { useState } from 'react';
|
|
568
|
+
|
|
539
569
|
function AutosaveFormExample() {
|
|
540
570
|
const [saveCount, setSaveCount] = useState(0);
|
|
541
|
-
const [lastSaved, setLastSaved] = useState(null);
|
|
571
|
+
const [lastSaved, setLastSaved] = useState<string | null>(null);
|
|
542
572
|
|
|
543
|
-
const
|
|
573
|
+
const handleAutosave = async (values: { title: string; content: string }) => {
|
|
544
574
|
try {
|
|
545
575
|
// Simulate API call to save draft
|
|
546
|
-
|
|
576
|
+
await fetch('/api/drafts', {
|
|
547
577
|
method: 'POST',
|
|
548
578
|
headers: { 'Content-Type': 'application/json' },
|
|
549
579
|
body: JSON.stringify(values)
|
|
550
580
|
});
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
console.log('Draft auto-saved:', values);
|
|
556
|
-
}
|
|
581
|
+
|
|
582
|
+
setSaveCount(prev => prev + 1);
|
|
583
|
+
setLastSaved(new Date().toLocaleTimeString());
|
|
584
|
+
console.log('Draft auto-saved:', values);
|
|
557
585
|
} catch (error) {
|
|
558
586
|
console.error('Failed to save draft:', error);
|
|
559
587
|
}
|
|
@@ -561,29 +589,23 @@ function AutosaveFormExample() {
|
|
|
561
589
|
|
|
562
590
|
return (
|
|
563
591
|
<div>
|
|
564
|
-
<Form
|
|
565
|
-
|
|
592
|
+
<Form
|
|
593
|
+
defaultValues={{ title: '', content: '' }}
|
|
594
|
+
onSubmit={handleAutosave}
|
|
566
595
|
autosave={true}
|
|
567
596
|
autosaveDelayMs={2000} // Wait 2 seconds after user stops typing
|
|
568
597
|
>
|
|
569
598
|
<FormField name="title" label="Title">
|
|
570
599
|
<Input placeholder="Enter title" />
|
|
571
600
|
</FormField>
|
|
572
|
-
|
|
601
|
+
|
|
573
602
|
<FormField name="content" label="Content">
|
|
574
|
-
<TextArea
|
|
575
|
-
placeholder="Start writing..."
|
|
576
|
-
rows={10}
|
|
577
|
-
/>
|
|
578
|
-
</FormField>
|
|
579
|
-
|
|
580
|
-
<FormField name="tags" label="Tags">
|
|
581
|
-
<ChipInput
|
|
582
|
-
placeholder="Add tags"
|
|
583
|
-
options={['react', 'typescript', 'design', 'tutorial']}
|
|
603
|
+
<TextArea
|
|
604
|
+
placeholder="Start writing..."
|
|
605
|
+
rows={10}
|
|
584
606
|
/>
|
|
585
607
|
</FormField>
|
|
586
|
-
|
|
608
|
+
|
|
587
609
|
{lastSaved && (
|
|
588
610
|
<Text type="BodySmall">
|
|
589
611
|
Auto-saved {saveCount} times. Last saved at {lastSaved}
|
|
@@ -595,66 +617,740 @@ function AutosaveFormExample() {
|
|
|
595
617
|
}
|
|
596
618
|
```
|
|
597
619
|
|
|
598
|
-
###
|
|
620
|
+
### Async Validation
|
|
599
621
|
```tsx
|
|
600
|
-
function
|
|
601
|
-
const
|
|
622
|
+
function AsyncValidationExample() {
|
|
623
|
+
const validateUsername = async (value: string) => {
|
|
624
|
+
if (!value) return 'Username is required';
|
|
602
625
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
626
|
+
// Simulate API call
|
|
627
|
+
const response = await fetch(`/api/check-username?username=${value}`);
|
|
628
|
+
const { available } = await response.json();
|
|
629
|
+
|
|
630
|
+
if (!available) {
|
|
631
|
+
return 'Username is already taken';
|
|
608
632
|
}
|
|
633
|
+
return undefined;
|
|
609
634
|
};
|
|
610
635
|
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
// Form will now submit normally
|
|
636
|
+
const handleSubmit = (values: { username: string; email: string }) => {
|
|
637
|
+
console.log('Account created:', values);
|
|
614
638
|
};
|
|
615
639
|
|
|
616
640
|
return (
|
|
617
|
-
<Form
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
641
|
+
<Form
|
|
642
|
+
defaultValues={{ username: '', email: '' }}
|
|
643
|
+
onSubmit={handleSubmit}
|
|
644
|
+
>
|
|
645
|
+
<FormField
|
|
646
|
+
name="username"
|
|
647
|
+
label="Username"
|
|
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
|
|
624
870
|
}}
|
|
871
|
+
onSubmit={handleSubmit}
|
|
625
872
|
>
|
|
626
|
-
<FormField name="
|
|
627
|
-
<Input placeholder="Enter
|
|
873
|
+
<FormField name="name" label="Name" required>
|
|
874
|
+
<Input placeholder="Enter your name" />
|
|
628
875
|
</FormField>
|
|
629
|
-
|
|
630
|
-
<
|
|
631
|
-
|
|
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>
|
|
632
951
|
</FormField>
|
|
633
|
-
|
|
634
|
-
<
|
|
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
|
+
>
|
|
1029
|
+
<FormField name="name" label="Name" required>
|
|
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} />
|
|
1039
|
+
</FormField>
|
|
1040
|
+
|
|
1041
|
+
<ResetButtons savedData={savedData} />
|
|
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>
|
|
635
1087
|
<Select>
|
|
636
|
-
<Option value="
|
|
637
|
-
<Option value="
|
|
638
|
-
<Option value="
|
|
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>
|
|
639
1092
|
</Select>
|
|
640
1093
|
</FormField>
|
|
641
|
-
|
|
642
|
-
<
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1094
|
+
|
|
1095
|
+
<DependentFields />
|
|
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>
|
|
1118
|
+
<Select>
|
|
1119
|
+
<Option value="">Select a state</Option>
|
|
1120
|
+
{country === 'us' && (
|
|
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
|
+
)}
|
|
1134
|
+
</Select>
|
|
1135
|
+
</FormField>
|
|
1136
|
+
|
|
1137
|
+
<FormField name="city" label="City" required>
|
|
1138
|
+
<Input placeholder="Enter your city" />
|
|
1139
|
+
</FormField>
|
|
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}
|
|
647
1249
|
>
|
|
648
|
-
|
|
1250
|
+
Check Availability
|
|
649
1251
|
</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
|
+
)}
|
|
650
1263
|
</div>
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1264
|
+
</div>
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
### FormState Properties Reference
|
|
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>
|
|
657
1295
|
</Form>
|
|
658
1296
|
);
|
|
659
1297
|
}
|
|
660
|
-
|
|
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/)
|