@delightui/components 0.1.161 → 0.1.162-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +16 -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 +290 -0
- package/dist/cjs/components/organisms/Form/Form.types.d.ts +24 -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 +16 -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 +290 -0
- package/dist/esm/components/organisms/Form/Form.types.d.ts +24 -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 +162 -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
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
# Form & FormField Migration Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Form and FormField components have been rewritten on top of **React Hook Form** (RHF) to provide better performance, TypeScript support, and a more modern API. This guide will help you migrate from the old custom form implementation to the new RHF-based implementation.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Reference: Props Comparison
|
|
10
|
+
|
|
11
|
+
### Form Component
|
|
12
|
+
|
|
13
|
+
| OLD Prop | NEW Prop | Status | Migration Notes |
|
|
14
|
+
|----------|----------|--------|-----------------|
|
|
15
|
+
| `formState?: T` | `defaultValues?: object` | 🔴 **BREAKING** | Renamed. Use for initial uncontrolled form values |
|
|
16
|
+
| `onFormStateChange?: (state, setError) => void` | ❌ Removed | 🔴 **BREAKING** | Use RHF's `watch()` or `useWatch()` hooks instead |
|
|
17
|
+
| `formValidator?: (state, setError) => boolean` | ❌ Removed | 🔴 **BREAKING** | Use field-level validation or RHF's `resolver` |
|
|
18
|
+
| `onSubmit?: (values, setError) => void` | `onSubmit?: (values) => void \| Promise<void>` | 🔴 **BREAKING** | No `setError` callback - use RHF hooks instead |
|
|
19
|
+
| `validateOnChange?: boolean` | `mode?: 'onSubmit' \| 'onChange' \| 'onBlur' \| ...` | 🔴 **BREAKING** | More flexible validation modes |
|
|
20
|
+
| `formRef?: Ref<HTMLFormElement>` | `formRef?: Ref<HTMLFormElement>` | ✅ **SAME** | Unchanged |
|
|
21
|
+
| `children?: ReactNode` | `children: ReactNode` | ✅ **SAME** | Now required |
|
|
22
|
+
| ❌ N/A | `autosave?: boolean` | 🟢 **NEW** | Enable autosave functionality |
|
|
23
|
+
| ❌ N/A | `autosaveDelayMs?: number` | 🟢 **NEW** | Autosave debounce delay |
|
|
24
|
+
| ❌ N/A | `formOptions?: UseFormProps` | 🟢 **NEW** | Pass additional RHF options |
|
|
25
|
+
| ❌ N/A | `className?: string` | 🟢 **NEW** | Form CSS class |
|
|
26
|
+
| ❌ N/A | `style?: CSSProperties` | 🟢 **NEW** | Form inline styles |
|
|
27
|
+
|
|
28
|
+
### FormField Component
|
|
29
|
+
|
|
30
|
+
| OLD Prop | NEW Prop | Status | Migration Notes |
|
|
31
|
+
|----------|----------|--------|-----------------|
|
|
32
|
+
| `name: string` | `name: TName` (type-safe) | 🟡 **ENHANCED** | Now type-checked against form values |
|
|
33
|
+
| `label?: string` | `label?: string` | ✅ **SAME** | Unchanged |
|
|
34
|
+
| `children: ReactNode` | `children: ReactNode` | ✅ **SAME** | Unchanged |
|
|
35
|
+
| `message?: string` | `message?: string` | ✅ **SAME** | Info message |
|
|
36
|
+
| `hasMessage?: boolean` | ❌ Removed | 🔴 **BREAKING** | No longer needed |
|
|
37
|
+
| `infoIcon?: ReactNode` | `infoIcon?: ReactNode` | ✅ **SAME** | Unchanged |
|
|
38
|
+
| `required?: boolean` | `required?: boolean` | ✅ **SAME** | Unchanged |
|
|
39
|
+
| `validate?: (setError, value?) => boolean` | `validate?: (value) => string \| undefined` | 🔴 **BREAKING** | Return error message instead of calling setError |
|
|
40
|
+
| ❌ N/A | `asyncValidate?: (value) => Promise<string \| undefined>` | 🟢 **NEW** | Async validation support |
|
|
41
|
+
| `disabled?: boolean` | `disabled?: boolean` | ✅ **SAME** | Unchanged |
|
|
42
|
+
| `invalid?: boolean` | `invalid?: boolean` | ✅ **SAME** | External invalid state |
|
|
43
|
+
| `id?: string` | `id?: string` | ✅ **SAME** | Unchanged |
|
|
44
|
+
| ❌ N/A | `displayName?: string` | 🟢 **NEW** | Custom name for error messages |
|
|
45
|
+
| ❌ N/A | `defaultValue?: TValue` | 🟢 **NEW** | Field-level default value |
|
|
46
|
+
| ❌ N/A | `value?: TValue` | 🟢 **NEW** | Standalone mode: controlled value |
|
|
47
|
+
| ❌ N/A | `onChange?: (value) => void` | 🟢 **NEW** | Standalone mode: onChange handler |
|
|
48
|
+
| ❌ N/A | `error?: string` | 🟢 **NEW** | Standalone mode: external error |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Breaking Changes
|
|
53
|
+
|
|
54
|
+
### 1. FormProvider Component Removed
|
|
55
|
+
|
|
56
|
+
**OLD:**
|
|
57
|
+
```tsx
|
|
58
|
+
<FormProvider formState={formState} onFormStateChange={handleChange} onSubmit={handleSubmit}>
|
|
59
|
+
<Form>
|
|
60
|
+
<FormField name="email" required>
|
|
61
|
+
<Input />
|
|
62
|
+
</FormField>
|
|
63
|
+
</Form>
|
|
64
|
+
</FormProvider>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**NEW:**
|
|
68
|
+
```tsx
|
|
69
|
+
<Form defaultValues={formState} onSubmit={handleSubmit}>
|
|
70
|
+
<FormField name="email" required>
|
|
71
|
+
<Input />
|
|
72
|
+
</FormField>
|
|
73
|
+
</Form>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Props Renamed: `formState` → `defaultValues`
|
|
77
|
+
|
|
78
|
+
**OLD:**
|
|
79
|
+
```tsx
|
|
80
|
+
<FormProvider formState={{ email: '', name: '' }}>
|
|
81
|
+
<Form>{/* ... */}</Form>
|
|
82
|
+
</FormProvider>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**NEW:**
|
|
86
|
+
```tsx
|
|
87
|
+
<Form defaultValues={{ email: '', name: '' }}>
|
|
88
|
+
{/* ... */}
|
|
89
|
+
</Form>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. `onSubmit` Signature Changed
|
|
93
|
+
|
|
94
|
+
**OLD:**
|
|
95
|
+
```tsx
|
|
96
|
+
const handleSubmit = (values, setError) => {
|
|
97
|
+
if (!validateEmail(values.email)) {
|
|
98
|
+
setError('email', 'Invalid email');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Submit logic
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**NEW:**
|
|
106
|
+
```tsx
|
|
107
|
+
import { useFormContext } from 'react-hook-form';
|
|
108
|
+
|
|
109
|
+
const handleSubmit = async (values) => {
|
|
110
|
+
// For setting errors, use RHF hooks in a component
|
|
111
|
+
// or handle validation in field-level validators
|
|
112
|
+
try {
|
|
113
|
+
await submitToAPI(values);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Use setError from useFormContext if needed
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Setting Errors in NEW Version:**
|
|
121
|
+
```tsx
|
|
122
|
+
// Create a component that can access form context
|
|
123
|
+
function MyFormContent() {
|
|
124
|
+
const { setError } = useFormContext();
|
|
125
|
+
|
|
126
|
+
const handleAPIError = (fieldName, message) => {
|
|
127
|
+
setError(fieldName, { message });
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<FormField name="email" required>
|
|
132
|
+
<Input />
|
|
133
|
+
</FormField>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function MyForm() {
|
|
138
|
+
const handleSubmit = async (values) => {
|
|
139
|
+
// Handle submission
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Form defaultValues={{ email: '' }} onSubmit={handleSubmit}>
|
|
144
|
+
<MyFormContent />
|
|
145
|
+
</Form>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 4. Validation Function Signature Changed
|
|
151
|
+
|
|
152
|
+
**OLD:**
|
|
153
|
+
```tsx
|
|
154
|
+
const validatePassword = (setError, value) => {
|
|
155
|
+
if (!value) {
|
|
156
|
+
setError('Password is required');
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (value.length < 8) {
|
|
160
|
+
setError('Password must be at least 8 characters');
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
<FormField name="password" validate={validatePassword}>
|
|
167
|
+
<Input inputType="Password" />
|
|
168
|
+
</FormField>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**NEW:**
|
|
172
|
+
```tsx
|
|
173
|
+
const validatePassword = (value: string) => {
|
|
174
|
+
if (!value) {
|
|
175
|
+
return 'Password is required';
|
|
176
|
+
}
|
|
177
|
+
if (value.length < 8) {
|
|
178
|
+
return 'Password must be at least 8 characters';
|
|
179
|
+
}
|
|
180
|
+
return undefined; // Valid
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
<FormField name="password" validate={validatePassword}>
|
|
184
|
+
<Input inputType="Password" />
|
|
185
|
+
</FormField>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 5. `onFormStateChange` Removed
|
|
189
|
+
|
|
190
|
+
**OLD:**
|
|
191
|
+
```tsx
|
|
192
|
+
const handleStateChange = (state, setError) => {
|
|
193
|
+
console.log('Form changed:', state);
|
|
194
|
+
// Validate on change
|
|
195
|
+
if (state.email && !validateEmail(state.email)) {
|
|
196
|
+
setError('email', 'Invalid email');
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
<FormProvider
|
|
201
|
+
formState={formState}
|
|
202
|
+
onFormStateChange={handleStateChange}
|
|
203
|
+
>
|
|
204
|
+
<Form>{/* ... */}</Form>
|
|
205
|
+
</FormProvider>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**NEW:**
|
|
209
|
+
```tsx
|
|
210
|
+
import { useWatch } from 'react-hook-form';
|
|
211
|
+
|
|
212
|
+
function FormContent() {
|
|
213
|
+
const formValues = useWatch(); // Watch all fields
|
|
214
|
+
// Or watch specific field: const email = useWatch({ name: 'email' });
|
|
215
|
+
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
console.log('Form changed:', formValues);
|
|
218
|
+
}, [formValues]);
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
<FormField name="email" required>
|
|
223
|
+
<Input />
|
|
224
|
+
</FormField>
|
|
225
|
+
{/* ... */}
|
|
226
|
+
</>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function MyForm() {
|
|
231
|
+
return (
|
|
232
|
+
<Form defaultValues={{ email: '' }}>
|
|
233
|
+
<FormContent />
|
|
234
|
+
</Form>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 6. `validateOnChange` → `mode`
|
|
240
|
+
|
|
241
|
+
**OLD:**
|
|
242
|
+
```tsx
|
|
243
|
+
<FormProvider validateOnChange={true}>
|
|
244
|
+
<Form>{/* ... */}</Form>
|
|
245
|
+
</FormProvider>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**NEW:**
|
|
249
|
+
```tsx
|
|
250
|
+
<Form mode="onChange">
|
|
251
|
+
{/* ... */}
|
|
252
|
+
</Form>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Available modes:**
|
|
256
|
+
- `"onSubmit"` (default) - Validate on submit
|
|
257
|
+
- `"onChange"` - Validate on every change
|
|
258
|
+
- `"onBlur"` - Validate on blur
|
|
259
|
+
- `"onTouched"` - Validate after first blur, then on change
|
|
260
|
+
- `"all"` - Validate on change, blur, and submit
|
|
261
|
+
|
|
262
|
+
### 7. `formValidator` Removed
|
|
263
|
+
|
|
264
|
+
**OLD:**
|
|
265
|
+
```tsx
|
|
266
|
+
const formValidator = (state, setError) => {
|
|
267
|
+
let isValid = true;
|
|
268
|
+
if (state.password !== state.confirmPassword) {
|
|
269
|
+
setError('confirmPassword', 'Passwords do not match');
|
|
270
|
+
isValid = false;
|
|
271
|
+
}
|
|
272
|
+
return isValid;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
<FormProvider formValidator={formValidator}>
|
|
276
|
+
<Form>{/* ... */}</Form>
|
|
277
|
+
</FormProvider>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**NEW (Option 1 - Field-level validation):**
|
|
281
|
+
```tsx
|
|
282
|
+
<FormField
|
|
283
|
+
name="confirmPassword"
|
|
284
|
+
validate={(value) => {
|
|
285
|
+
const password = /* get password value using useWatch */;
|
|
286
|
+
if (value !== password) {
|
|
287
|
+
return 'Passwords do not match';
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
<Input inputType="Password" />
|
|
293
|
+
</FormField>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**NEW (Option 2 - RHF Resolver):**
|
|
297
|
+
```tsx
|
|
298
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
299
|
+
import * as z from 'zod';
|
|
300
|
+
|
|
301
|
+
const schema = z.object({
|
|
302
|
+
password: z.string().min(8),
|
|
303
|
+
confirmPassword: z.string()
|
|
304
|
+
}).refine(data => data.password === data.confirmPassword, {
|
|
305
|
+
message: "Passwords do not match",
|
|
306
|
+
path: ["confirmPassword"]
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
<Form
|
|
310
|
+
formOptions={{ resolver: zodResolver(schema) }}
|
|
311
|
+
defaultValues={{ password: '', confirmPassword: '' }}
|
|
312
|
+
>
|
|
313
|
+
{/* ... */}
|
|
314
|
+
</Form>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### 8. `hasMessage` Prop Removed
|
|
318
|
+
|
|
319
|
+
**OLD:**
|
|
320
|
+
```tsx
|
|
321
|
+
<FormField name="email" hasMessage={true} message="Invalid email">
|
|
322
|
+
<Input />
|
|
323
|
+
</FormField>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**NEW:**
|
|
327
|
+
```tsx
|
|
328
|
+
<FormField name="email" message="Invalid email">
|
|
329
|
+
<Input />
|
|
330
|
+
</FormField>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## New Features
|
|
336
|
+
|
|
337
|
+
### 1. Autosave Support
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
<Form
|
|
341
|
+
defaultValues={{ title: '', content: '' }}
|
|
342
|
+
onSubmit={handleAutosave}
|
|
343
|
+
autosave={true}
|
|
344
|
+
autosaveDelayMs={2000} // Wait 2s after user stops typing
|
|
345
|
+
>
|
|
346
|
+
<FormField name="title" label="Title">
|
|
347
|
+
<Input />
|
|
348
|
+
</FormField>
|
|
349
|
+
<FormField name="content" label="Content">
|
|
350
|
+
<TextArea rows={10} />
|
|
351
|
+
</FormField>
|
|
352
|
+
</Form>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### 2. Async Validation
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
const validateUsername = async (value: string) => {
|
|
359
|
+
const response = await fetch(`/api/check-username?username=${value}`);
|
|
360
|
+
const { available } = await response.json();
|
|
361
|
+
if (!available) {
|
|
362
|
+
return 'Username is already taken';
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
<FormField
|
|
368
|
+
name="username"
|
|
369
|
+
label="Username"
|
|
370
|
+
asyncValidate={validateUsername}
|
|
371
|
+
>
|
|
372
|
+
<Input />
|
|
373
|
+
</FormField>
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 3. Standalone FormField (No Form Required)
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
function StandaloneExample() {
|
|
380
|
+
const [email, setEmail] = useState('');
|
|
381
|
+
const [error, setError] = useState('');
|
|
382
|
+
|
|
383
|
+
const validateEmail = (value: string) => {
|
|
384
|
+
if (!value.includes('@')) {
|
|
385
|
+
return 'Invalid email';
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<FormField
|
|
392
|
+
name="email"
|
|
393
|
+
label="Email"
|
|
394
|
+
value={email}
|
|
395
|
+
onChange={setEmail}
|
|
396
|
+
error={error}
|
|
397
|
+
validate={(value) => {
|
|
398
|
+
const error = validateEmail(value);
|
|
399
|
+
setError(error || '');
|
|
400
|
+
return error;
|
|
401
|
+
}}
|
|
402
|
+
>
|
|
403
|
+
<Input />
|
|
404
|
+
</FormField>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### 4. Full TypeScript Type Safety
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
interface MyFormValues {
|
|
413
|
+
email: string;
|
|
414
|
+
age: number;
|
|
415
|
+
newsletter: boolean;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function TypeSafeForm() {
|
|
419
|
+
const handleSubmit = (values: MyFormValues) => {
|
|
420
|
+
// values is fully typed!
|
|
421
|
+
console.log(values.email); // ✅ TypeScript knows this is a string
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<Form<MyFormValues>
|
|
426
|
+
defaultValues={{ email: '', age: 0, newsletter: false }}
|
|
427
|
+
onSubmit={handleSubmit}
|
|
428
|
+
>
|
|
429
|
+
{/* TypeScript will validate field names */}
|
|
430
|
+
<FormField<MyFormValues> name="email" label="Email">
|
|
431
|
+
<Input />
|
|
432
|
+
</FormField>
|
|
433
|
+
|
|
434
|
+
{/* This would cause a TypeScript error: */}
|
|
435
|
+
{/* <FormField name="invalidField"> */}
|
|
436
|
+
</Form>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### 5. Access to React Hook Form Methods
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
import { useFormContext } from 'react-hook-form';
|
|
445
|
+
|
|
446
|
+
function FormActions() {
|
|
447
|
+
const { reset, setValue, getValues, formState } = useFormContext();
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<div>
|
|
451
|
+
<Button onClick={() => reset()}>Reset Form</Button>
|
|
452
|
+
<Button onClick={() => setValue('email', 'test@example.com')}>
|
|
453
|
+
Prefill Email
|
|
454
|
+
</Button>
|
|
455
|
+
<Text>Form is {formState.isDirty ? 'modified' : 'pristine'}</Text>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function MyForm() {
|
|
461
|
+
return (
|
|
462
|
+
<Form defaultValues={{ email: '' }}>
|
|
463
|
+
<FormField name="email" label="Email">
|
|
464
|
+
<Input />
|
|
465
|
+
</FormField>
|
|
466
|
+
<FormActions />
|
|
467
|
+
</Form>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Migration Checklist
|
|
475
|
+
|
|
476
|
+
- [ ] Replace `FormProvider` wrapper with direct `<Form>` usage
|
|
477
|
+
- [ ] Rename `formState` prop to `defaultValues`
|
|
478
|
+
- [ ] Update `onSubmit` handlers to remove `setError` parameter
|
|
479
|
+
- [ ] Convert validation functions to return error strings instead of calling `setError`
|
|
480
|
+
- [ ] Replace `onFormStateChange` with `useWatch()` or `useFormContext()`
|
|
481
|
+
- [ ] Change `validateOnChange` to `mode="onChange"`
|
|
482
|
+
- [ ] Remove `formValidator` and use field-level validation or RHF resolver
|
|
483
|
+
- [ ] Remove `hasMessage` prop from FormField components
|
|
484
|
+
- [ ] Update TypeScript types to use new form value types
|
|
485
|
+
- [ ] Test all form validation flows
|
|
486
|
+
- [ ] Test form submission and error handling
|
|
487
|
+
- [ ] Consider using new features: autosave, async validation, standalone mode
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Common Migration Patterns
|
|
492
|
+
|
|
493
|
+
### Pattern 1: Simple Contact Form
|
|
494
|
+
|
|
495
|
+
**OLD:**
|
|
496
|
+
```tsx
|
|
497
|
+
function ContactForm() {
|
|
498
|
+
const [formState, setFormState] = useState({ name: '', email: '', message: '' });
|
|
499
|
+
|
|
500
|
+
const handleSubmit = (values, setError) => {
|
|
501
|
+
if (!values.email.includes('@')) {
|
|
502
|
+
setError('email', 'Invalid email');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
console.log('Submitted:', values);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<FormProvider formState={formState} onSubmit={handleSubmit}>
|
|
510
|
+
<Form>
|
|
511
|
+
<FormField name="name" label="Name" required><Input /></FormField>
|
|
512
|
+
<FormField name="email" label="Email" required><Input /></FormField>
|
|
513
|
+
<FormField name="message" label="Message"><TextArea /></FormField>
|
|
514
|
+
<Button actionType="submit">Send</Button>
|
|
515
|
+
</Form>
|
|
516
|
+
</FormProvider>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**NEW:**
|
|
522
|
+
```tsx
|
|
523
|
+
function ContactForm() {
|
|
524
|
+
const handleSubmit = (values: { name: string; email: string; message: string }) => {
|
|
525
|
+
console.log('Submitted:', values);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const validateEmail = (value: string) => {
|
|
529
|
+
if (!value.includes('@')) return 'Invalid email';
|
|
530
|
+
return undefined;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<Form
|
|
535
|
+
defaultValues={{ name: '', email: '', message: '' }}
|
|
536
|
+
onSubmit={handleSubmit}
|
|
537
|
+
>
|
|
538
|
+
<FormField name="name" label="Name" required><Input /></FormField>
|
|
539
|
+
<FormField name="email" label="Email" required validate={validateEmail}>
|
|
540
|
+
<Input />
|
|
541
|
+
</FormField>
|
|
542
|
+
<FormField name="message" label="Message"><TextArea /></FormField>
|
|
543
|
+
<Button actionType="submit">Send</Button>
|
|
544
|
+
</Form>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Pattern 2: Form with Dynamic Validation
|
|
550
|
+
|
|
551
|
+
**OLD:**
|
|
552
|
+
```tsx
|
|
553
|
+
function SignupForm() {
|
|
554
|
+
const formValidator = (state, setError) => {
|
|
555
|
+
if (state.password !== state.confirmPassword) {
|
|
556
|
+
setError('confirmPassword', 'Passwords must match');
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
<FormProvider formValidator={formValidator}>
|
|
564
|
+
<Form>
|
|
565
|
+
<FormField name="password" label="Password" required>
|
|
566
|
+
<Input inputType="Password" />
|
|
567
|
+
</FormField>
|
|
568
|
+
<FormField name="confirmPassword" label="Confirm" required>
|
|
569
|
+
<Input inputType="Password" />
|
|
570
|
+
</FormField>
|
|
571
|
+
<Button actionType="submit">Sign Up</Button>
|
|
572
|
+
</Form>
|
|
573
|
+
</FormProvider>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**NEW:**
|
|
579
|
+
```tsx
|
|
580
|
+
import { useWatch, useFormContext } from 'react-hook-form';
|
|
581
|
+
|
|
582
|
+
function ConfirmPasswordField() {
|
|
583
|
+
const password = useWatch({ name: 'password' });
|
|
584
|
+
|
|
585
|
+
const validateConfirm = (value: string) => {
|
|
586
|
+
if (value !== password) return 'Passwords must match';
|
|
587
|
+
return undefined;
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
<FormField
|
|
592
|
+
name="confirmPassword"
|
|
593
|
+
label="Confirm"
|
|
594
|
+
required
|
|
595
|
+
validate={validateConfirm}
|
|
596
|
+
>
|
|
597
|
+
<Input inputType="Password" />
|
|
598
|
+
</FormField>
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function SignupForm() {
|
|
603
|
+
return (
|
|
604
|
+
<Form defaultValues={{ password: '', confirmPassword: '' }}>
|
|
605
|
+
<FormField name="password" label="Password" required>
|
|
606
|
+
<Input inputType="Password" />
|
|
607
|
+
</FormField>
|
|
608
|
+
<ConfirmPasswordField />
|
|
609
|
+
<Button actionType="submit">Sign Up</Button>
|
|
610
|
+
</Form>
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Resources
|
|
618
|
+
|
|
619
|
+
- [React Hook Form Documentation](https://react-hook-form.com/)
|
|
620
|
+
- [React Hook Form API Reference](https://react-hook-form.com/api)
|
|
621
|
+
- [Migration Examples in Codebase](./components/organisms/Form/examples/)
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Need Help?
|
|
626
|
+
|
|
627
|
+
If you encounter issues during migration:
|
|
628
|
+
|
|
629
|
+
1. Check the [Form examples](../src/components/organisms/Form/examples/) in the codebase
|
|
630
|
+
2. Review the [React Hook Form docs](https://react-hook-form.com/)
|
|
631
|
+
3. Reach out to the team for assistance
|