@reformer/core 1.0.0 → 1.1.0-beta.2
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/README.md +57 -48
- package/dist/core/validation/core/validate-tree.d.ts +10 -4
- package/dist/core/validation/core/validate-tree.js +10 -4
- package/llms.txt +444 -685
- package/package.json +6 -5
package/llms.txt
CHANGED
|
@@ -1,847 +1,606 @@
|
|
|
1
|
-
# ReFormer
|
|
1
|
+
# ReFormer - LLM Integration Guide
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> Built on Preact Signals Core for fine-grained reactivity with full TypeScript support.
|
|
5
|
-
> Key features: type-safe schemas, declarative validation, reactive behaviors, nested forms, dynamic arrays.
|
|
3
|
+
## 1. QUICK REFERENCE
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
### Imports (CRITICALLY IMPORTANT)
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import { required, email, minLength } from 'reformer/validators';
|
|
22
|
-
import { useFormControl } from 'reformer';
|
|
23
|
-
|
|
24
|
-
// 1. Define your form type
|
|
25
|
-
type ContactForm = {
|
|
26
|
-
name: string;
|
|
27
|
-
email: string;
|
|
28
|
-
message: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// 2. Create form with schema and validation
|
|
32
|
-
const form = createForm<ContactForm>({
|
|
33
|
-
form: {
|
|
34
|
-
name: { value: '', component: Input, componentProps: { label: 'Name' } },
|
|
35
|
-
email: { value: '', component: Input, componentProps: { label: 'Email' } },
|
|
36
|
-
message: { value: '', component: Textarea, componentProps: { label: 'Message' } },
|
|
37
|
-
},
|
|
38
|
-
validation: (path) => {
|
|
39
|
-
required(path.name);
|
|
40
|
-
minLength(path.name, 2);
|
|
41
|
-
required(path.email);
|
|
42
|
-
email(path.email);
|
|
43
|
-
required(path.message);
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// 3. Use in React component
|
|
48
|
-
function ContactForm() {
|
|
49
|
-
const { value, errors, shouldShowError } = useFormControl(form.name);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<input
|
|
53
|
-
value={value}
|
|
54
|
-
onChange={(e) => form.name.setValue(e.target.value)}
|
|
55
|
-
onBlur={() => form.name.markAsTouched()}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Architecture
|
|
62
|
-
|
|
63
|
-
### Node Hierarchy
|
|
64
|
-
|
|
65
|
-
ReFormer uses a tree-based node architecture:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
GroupNode (Form)
|
|
69
|
-
├── FieldNode (single values: string, number, boolean)
|
|
70
|
-
├── GroupNode (nested objects)
|
|
71
|
-
└── ArrayNode (dynamic arrays)
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
All nodes inherit from abstract `FormNode` base class.
|
|
75
|
-
|
|
76
|
-
### Key Concepts
|
|
77
|
-
|
|
78
|
-
1. **Form Schema** - Defines structure, components, and initial values
|
|
79
|
-
2. **Validation Schema** - Declares validation rules (separate from structure)
|
|
80
|
-
3. **Behavior Schema** - Reactive logic (computed fields, conditional visibility, sync)
|
|
7
|
+
| What | Where |
|
|
8
|
+
| ------------------------------------------------------------------------------------------- | --------------------------- |
|
|
9
|
+
| `createForm`, `useFormControl`, `useFormControlValue` | `@reformer/core` |
|
|
10
|
+
| `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `GroupNodeWithControls`, `FieldNode` | `@reformer/core` |
|
|
11
|
+
| `FormSchema`, `FieldConfig`, `ArrayNode` | `@reformer/core` |
|
|
12
|
+
| `required`, `min`, `max`, `minLength`, `maxLength`, `email` | `@reformer/core/validators` |
|
|
13
|
+
| `pattern`, `url`, `phone`, `number`, `date` | `@reformer/core/validators` |
|
|
14
|
+
| `validate`, `validateAsync`, `validateTree`, `applyWhen` | `@reformer/core/validators` |
|
|
15
|
+
| `notEmpty`, `validateItems` | `@reformer/core/validators` |
|
|
16
|
+
| `computeFrom`, `enableWhen`, `disableWhen`, `watchField`, `copyFrom` | `@reformer/core/behaviors` |
|
|
17
|
+
| `resetWhen`, `revalidateWhen`, `syncFields` | `@reformer/core/behaviors` |
|
|
18
|
+
| `transformValue`, `transformers` | `@reformer/core/behaviors` |
|
|
81
19
|
|
|
82
|
-
###
|
|
20
|
+
### Type Values
|
|
83
21
|
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
22
|
+
- Optional numbers: `number | undefined` (NOT `null`)
|
|
23
|
+
- Optional strings: `string` (empty string by default)
|
|
24
|
+
- Do NOT add `[key: string]: unknown` to form interfaces
|
|
87
25
|
|
|
88
|
-
##
|
|
26
|
+
## 2. API SIGNATURES
|
|
89
27
|
|
|
90
|
-
###
|
|
28
|
+
### Validators
|
|
91
29
|
|
|
92
30
|
```typescript
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
updateOn?: 'change' | 'blur' | 'submit'; // When to validate (default: 'change')
|
|
101
|
-
debounce?: number; // Debounce validation in ms
|
|
102
|
-
}
|
|
103
|
-
```
|
|
31
|
+
// Basic validators
|
|
32
|
+
required(path, options?: { message?: string })
|
|
33
|
+
min(path, value: number, options?: { message?: string })
|
|
34
|
+
max(path, value: number, options?: { message?: string })
|
|
35
|
+
minLength(path, length: number, options?: { message?: string })
|
|
36
|
+
maxLength(path, length: number, options?: { message?: string })
|
|
37
|
+
email(path, options?: { message?: string })
|
|
104
38
|
|
|
105
|
-
|
|
39
|
+
// Additional validators
|
|
40
|
+
pattern(path, regex: RegExp, options?: { message?: string })
|
|
41
|
+
url(path, options?: { message?: string })
|
|
42
|
+
phone(path, options?: { message?: string; format?: PhoneFormat })
|
|
43
|
+
number(path, options?: { message?: string })
|
|
44
|
+
date(path, options?: { message?: string; minAge?: number; maxAge?: number; noFuture?: boolean; noPast?: boolean })
|
|
106
45
|
|
|
107
|
-
|
|
46
|
+
// Custom validators
|
|
47
|
+
validate(path, validator: (value, ctx) => ValidationError | null)
|
|
48
|
+
validateAsync(path, validator: async (value, ctx) => ValidationError | null)
|
|
49
|
+
validateTree(validator: (ctx) => ValidationError | null, options?: { targetField?: string })
|
|
108
50
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
phones: { type: string; number: string }[];
|
|
112
|
-
}
|
|
51
|
+
// Conditional validation
|
|
52
|
+
applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
|
|
113
53
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
number: { value: '', component: Input },
|
|
118
|
-
}],
|
|
119
|
-
};
|
|
54
|
+
// Array validators
|
|
55
|
+
notEmpty(path, options?: { message?: string })
|
|
56
|
+
validateItems(arrayPath, itemValidatorsFn: (itemPath) => void)
|
|
120
57
|
```
|
|
121
58
|
|
|
122
|
-
###
|
|
59
|
+
### Behaviors
|
|
123
60
|
|
|
124
61
|
```typescript
|
|
125
|
-
|
|
62
|
+
// Enable/disable fields conditionally
|
|
63
|
+
enableWhen(path, condition: (form) => boolean, options?: { resetOnDisable?: boolean })
|
|
64
|
+
disableWhen(path, condition: (form) => boolean)
|
|
126
65
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
email: string;
|
|
130
|
-
age: number;
|
|
131
|
-
address: {
|
|
132
|
-
street: string;
|
|
133
|
-
city: string;
|
|
134
|
-
};
|
|
135
|
-
phones: { type: string; number: string }[];
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const form = createForm<UserForm>({
|
|
139
|
-
form: {
|
|
140
|
-
name: { value: '', component: Input },
|
|
141
|
-
email: { value: '', component: Input },
|
|
142
|
-
age: { value: 0, component: Input, componentProps: { type: 'number' } },
|
|
143
|
-
address: {
|
|
144
|
-
street: { value: '', component: Input },
|
|
145
|
-
city: { value: '', component: Input },
|
|
146
|
-
},
|
|
147
|
-
phones: [{
|
|
148
|
-
type: { value: 'mobile', component: Select },
|
|
149
|
-
number: { value: '', component: Input },
|
|
150
|
-
}],
|
|
151
|
-
},
|
|
152
|
-
validation: (path) => { /* validators */ },
|
|
153
|
-
behavior: (path) => { /* behaviors */ },
|
|
154
|
-
});
|
|
155
|
-
```
|
|
66
|
+
// Computed fields (same nesting level)
|
|
67
|
+
computeFrom(sourcePaths[], targetPath, compute: (values) => result, options?: { debounce?: number; condition?: (form) => boolean })
|
|
156
68
|
|
|
157
|
-
|
|
69
|
+
// Watch field changes
|
|
70
|
+
watchField(path, callback: (value, ctx: BehaviorContext) => void, options?: { immediate?: boolean; debounce?: number })
|
|
158
71
|
|
|
159
|
-
|
|
72
|
+
// Copy values between fields
|
|
73
|
+
copyFrom(sourcePath, targetPath, options?: { when?: (form) => boolean; fields?: string[]; transform?: (value) => value })
|
|
160
74
|
|
|
161
|
-
|
|
75
|
+
// Reset field when condition met
|
|
76
|
+
resetWhen(path, condition: (form) => boolean, options?: { toValue?: any })
|
|
162
77
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- `valid` / `invalid` - Validation state
|
|
166
|
-
- `touched` / `untouched` - User interaction state
|
|
167
|
-
- `dirty` / `pristine` - Value changed from initial
|
|
168
|
-
- `errors` - Array of ValidationError objects
|
|
169
|
-
- `shouldShowError` - true when invalid AND (touched OR dirty)
|
|
170
|
-
- `disabled` - Is field disabled
|
|
171
|
-
- `pending` - Async validation in progress
|
|
172
|
-
- `status` - 'valid' | 'invalid' | 'pending' | 'disabled'
|
|
173
|
-
- `componentProps` - Props for component
|
|
78
|
+
// Re-validate when another field changes
|
|
79
|
+
revalidateWhen(triggerPath, targetPath)
|
|
174
80
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
- `reset()` - Reset to initial value
|
|
178
|
-
- `markAsTouched()` - Mark as touched
|
|
179
|
-
- `markAsDirty()` - Mark as dirty
|
|
180
|
-
- `disable()` / `enable()` - Toggle disabled state
|
|
181
|
-
- `validate()` - Run validation
|
|
182
|
-
- `getErrors(filter?)` - Get filtered errors
|
|
81
|
+
// Sync multiple fields
|
|
82
|
+
syncFields(paths[], options?: { bidirectional?: boolean })
|
|
183
83
|
|
|
184
|
-
|
|
84
|
+
// Transform values
|
|
85
|
+
transformValue(path, transformer: (value) => value, options?: { on?: 'change' | 'blur' })
|
|
86
|
+
transformers.trim, transformers.toUpperCase, transformers.toLowerCase, transformers.toNumber
|
|
185
87
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
**Methods:**
|
|
193
|
-
- `getFieldByPath(path: string)` - Get field by dot-notation path
|
|
194
|
-
- `patchValue(partial)` - Update subset of fields
|
|
195
|
-
- `resetAll()` - Reset all children
|
|
196
|
-
- All FormNode methods
|
|
197
|
-
|
|
198
|
-
**Proxy Access:**
|
|
199
|
-
```typescript
|
|
200
|
-
// Type-safe field access via proxy
|
|
201
|
-
form.name // FieldNode<string>
|
|
202
|
-
form.address.city // FieldNode<string>
|
|
203
|
-
form.phones // ArrayNode
|
|
88
|
+
// BehaviorContext interface:
|
|
89
|
+
interface BehaviorContext<TForm> {
|
|
90
|
+
form: GroupNodeWithControls<TForm>; // Form proxy with typed field access
|
|
91
|
+
setFieldValue: (path: string, value: any) => void;
|
|
92
|
+
getFieldValue: (path: string) => unknown;
|
|
93
|
+
}
|
|
204
94
|
```
|
|
205
95
|
|
|
206
|
-
|
|
96
|
+
## 3. COMMON PATTERNS
|
|
207
97
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
**Properties:**
|
|
211
|
-
- `controls` - Array of GroupNode items
|
|
212
|
-
- `length` - Number of items
|
|
213
|
-
|
|
214
|
-
**Methods:**
|
|
215
|
-
- `push(value)` - Add item to end
|
|
216
|
-
- `insert(index, value)` - Insert at position
|
|
217
|
-
- `removeAt(index)` - Remove at position
|
|
218
|
-
- `move(from, to)` - Move item
|
|
219
|
-
- `clear()` - Remove all items
|
|
220
|
-
- `at(index)` - Get item at index
|
|
98
|
+
### Conditional Fields with Auto-Reset
|
|
221
99
|
|
|
222
100
|
```typescript
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
form.phones.at(0).controls.number.setValue('123-456');
|
|
101
|
+
enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
|
|
102
|
+
resetOnDisable: true,
|
|
103
|
+
});
|
|
227
104
|
```
|
|
228
105
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
### ValidationSchemaFn
|
|
106
|
+
### Computed Field from Nested to Root Level
|
|
232
107
|
|
|
233
108
|
```typescript
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
validation: (path) => {
|
|
239
|
-
// Built-in validators
|
|
240
|
-
required(path.name);
|
|
241
|
-
email(path.email);
|
|
242
|
-
minLength(path.password, 8);
|
|
243
|
-
|
|
244
|
-
// Custom validator
|
|
245
|
-
validate(path.age, (value) => {
|
|
246
|
-
if (value < 18) return { code: 'tooYoung', message: 'Must be 18+' };
|
|
247
|
-
return null;
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// Async validator
|
|
251
|
-
validateAsync(path.username, async (value) => {
|
|
252
|
-
const available = await checkUsername(value);
|
|
253
|
-
if (!available) return { code: 'taken', message: 'Username taken' };
|
|
254
|
-
return null;
|
|
255
|
-
}, { debounce: 500 });
|
|
256
|
-
},
|
|
109
|
+
// DO NOT use computeFrom for cross-level computations
|
|
110
|
+
// Use watchField instead:
|
|
111
|
+
watchField(path.nested.field, (value, ctx) => {
|
|
112
|
+
ctx.setFieldValue('rootField', computedValue);
|
|
257
113
|
});
|
|
258
114
|
```
|
|
259
115
|
|
|
260
|
-
###
|
|
261
|
-
|
|
262
|
-
All imported from `reformer/validators`:
|
|
263
|
-
|
|
264
|
-
| Validator | Usage | Description |
|
|
265
|
-
|-----------|-------|-------------|
|
|
266
|
-
| `required(path)` | `required(path.name)` | Non-empty value |
|
|
267
|
-
| `email(path)` | `email(path.email)` | Valid email format |
|
|
268
|
-
| `minLength(path, n)` | `minLength(path.name, 2)` | Minimum string length |
|
|
269
|
-
| `maxLength(path, n)` | `maxLength(path.bio, 500)` | Maximum string length |
|
|
270
|
-
| `min(path, n)` | `min(path.age, 18)` | Minimum number value |
|
|
271
|
-
| `max(path, n)` | `max(path.qty, 100)` | Maximum number value |
|
|
272
|
-
| `pattern(path, regex)` | `pattern(path.code, /^[A-Z]+$/)` | Match regex |
|
|
273
|
-
| `url(path)` | `url(path.website)` | Valid URL |
|
|
274
|
-
| `phone(path)` | `phone(path.phone)` | Valid phone |
|
|
275
|
-
| `number(path)` | `number(path.amount)` | Must be number |
|
|
276
|
-
| `date(path)` | `date(path.birthDate)` | Valid date |
|
|
277
|
-
|
|
278
|
-
### Custom Validator Example
|
|
116
|
+
### Type-Safe useFormControl
|
|
279
117
|
|
|
280
118
|
```typescript
|
|
281
|
-
|
|
282
|
-
export function strongPassword() {
|
|
283
|
-
return (value: string) => {
|
|
284
|
-
if (!value) return null; // Skip empty (use required() separately)
|
|
285
|
-
|
|
286
|
-
const errors: string[] = [];
|
|
287
|
-
if (!/[A-Z]/.test(value)) errors.push('uppercase');
|
|
288
|
-
if (!/[a-z]/.test(value)) errors.push('lowercase');
|
|
289
|
-
if (!/[0-9]/.test(value)) errors.push('number');
|
|
290
|
-
if (value.length < 8) errors.push('length');
|
|
291
|
-
|
|
292
|
-
if (errors.length) {
|
|
293
|
-
return { code: 'weakPassword', message: 'Password too weak', params: { missing: errors } };
|
|
294
|
-
}
|
|
295
|
-
return null;
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Usage
|
|
300
|
-
validation: (path) => {
|
|
301
|
-
required(path.password);
|
|
302
|
-
validate(path.password, strongPassword());
|
|
303
|
-
}
|
|
119
|
+
const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
|
|
304
120
|
```
|
|
305
121
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
// Check username availability on server
|
|
310
|
-
validation: (path) => {
|
|
311
|
-
required(path.username);
|
|
122
|
+
## 4. ⚠️ COMMON MISTAKES
|
|
312
123
|
|
|
313
|
-
|
|
314
|
-
if (!value || value.length < 3) return null;
|
|
124
|
+
### Validators
|
|
315
125
|
|
|
316
|
-
|
|
317
|
-
|
|
126
|
+
```typescript
|
|
127
|
+
// ❌ WRONG
|
|
128
|
+
required(path.email, 'Email is required');
|
|
318
129
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
return null;
|
|
323
|
-
}, { debounce: 500 });
|
|
324
|
-
}
|
|
130
|
+
// ✅ CORRECT
|
|
131
|
+
required(path.email, { message: 'Email is required' });
|
|
325
132
|
```
|
|
326
133
|
|
|
327
|
-
###
|
|
134
|
+
### Types
|
|
328
135
|
|
|
329
136
|
```typescript
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
required(path.password);
|
|
334
|
-
required(path.confirmPassword);
|
|
335
|
-
|
|
336
|
-
// Cross-field validation
|
|
337
|
-
validateTree((ctx) => {
|
|
338
|
-
const password = ctx.form.password.value.value;
|
|
339
|
-
const confirm = ctx.form.confirmPassword.value.value;
|
|
340
|
-
|
|
341
|
-
if (password && confirm && password !== confirm) {
|
|
342
|
-
return {
|
|
343
|
-
code: 'passwordMismatch',
|
|
344
|
-
message: 'Passwords do not match',
|
|
345
|
-
path: 'confirmPassword',
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
return null;
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
```
|
|
137
|
+
// ❌ WRONG
|
|
138
|
+
amount: number | null;
|
|
139
|
+
[key: string]: unknown;
|
|
352
140
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
141
|
+
// ✅ CORRECT
|
|
142
|
+
amount: number | undefined;
|
|
143
|
+
// No index signature
|
|
144
|
+
```
|
|
356
145
|
|
|
357
146
|
### computeFrom
|
|
358
147
|
|
|
359
|
-
Calculate field value from other fields:
|
|
360
|
-
|
|
361
148
|
```typescript
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
behavior: (path) => {
|
|
365
|
-
// total = price * quantity
|
|
366
|
-
computeFrom(
|
|
367
|
-
[path.price, path.quantity], // Watch these fields
|
|
368
|
-
path.total, // Update this field
|
|
369
|
-
({ price, quantity }) => price * quantity // Compute function
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
```
|
|
149
|
+
// ❌ WRONG - different nesting levels
|
|
150
|
+
computeFrom([path.nested.a, path.nested.b], path.root, ...)
|
|
373
151
|
|
|
374
|
-
|
|
152
|
+
// ✅ CORRECT - use watchField
|
|
153
|
+
watchField(path.nested.a, (_, ctx) => {
|
|
154
|
+
ctx.setFieldValue('root', computed);
|
|
155
|
+
});
|
|
156
|
+
```
|
|
375
157
|
|
|
376
|
-
|
|
158
|
+
### Imports
|
|
377
159
|
|
|
378
160
|
```typescript
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
behavior: (path) => {
|
|
382
|
-
// Enable discount field only when total > 500
|
|
383
|
-
enableWhen(path.discount, (form) => form.total > 500);
|
|
161
|
+
// ❌ WRONG - types are not in submodules
|
|
162
|
+
import { ValidationSchemaFn } from '@reformer/core/validators';
|
|
384
163
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
164
|
+
// ✅ CORRECT - types from main module
|
|
165
|
+
import type { ValidationSchemaFn } from '@reformer/core';
|
|
166
|
+
import { required, email } from '@reformer/core/validators';
|
|
388
167
|
```
|
|
389
168
|
|
|
390
|
-
|
|
169
|
+
## 5. TROUBLESHOOTING
|
|
391
170
|
|
|
392
|
-
|
|
171
|
+
| Error | Cause | Solution |
|
|
172
|
+
| ------------------------------------------------------ | ------------------------------ | --------------------------------- |
|
|
173
|
+
| `'string' is not assignable to '{ message?: string }'` | Wrong validator format | Use `{ message: 'text' }` |
|
|
174
|
+
| `'null' is not assignable to 'undefined'` | Wrong optional type | Replace `null` with `undefined` |
|
|
175
|
+
| `FormFields[]` instead of concrete type | Type inference issue | Use `as FieldNode<T>` |
|
|
176
|
+
| `Type 'X' is missing properties from type 'Y'` | Cross-level computeFrom | Use watchField instead |
|
|
177
|
+
| `Module has no exported member` | Wrong import source | Types from core, functions from submodules |
|
|
178
|
+
|
|
179
|
+
## 6. COMPLETE IMPORT EXAMPLE
|
|
393
180
|
|
|
394
181
|
```typescript
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}, { debounce: 300 });
|
|
404
|
-
}
|
|
405
|
-
```
|
|
182
|
+
// Types - always from @reformer/core
|
|
183
|
+
import type {
|
|
184
|
+
ValidationSchemaFn,
|
|
185
|
+
BehaviorSchemaFn,
|
|
186
|
+
FieldPath,
|
|
187
|
+
GroupNodeWithControls,
|
|
188
|
+
FieldNode,
|
|
189
|
+
} from '@reformer/core';
|
|
406
190
|
|
|
407
|
-
|
|
191
|
+
// Core functions
|
|
192
|
+
import { createForm, useFormControl } from '@reformer/core';
|
|
408
193
|
|
|
409
|
-
|
|
194
|
+
// Validators - from /validators submodule
|
|
195
|
+
import { required, min, max, email, validate, applyWhen } from '@reformer/core/validators';
|
|
410
196
|
|
|
411
|
-
|
|
412
|
-
import { copyFrom } from 'reformer/behaviors';
|
|
413
|
-
|
|
414
|
-
behavior: (path) => {
|
|
415
|
-
// Copy billing address to shipping when checkbox is checked
|
|
416
|
-
copyFrom(path.billingAddress, path.shippingAddress, {
|
|
417
|
-
when: (form) => form.sameAsShipping === true,
|
|
418
|
-
fields: 'all', // or ['street', 'city', 'zip']
|
|
419
|
-
});
|
|
420
|
-
}
|
|
197
|
+
// Behaviors - from /behaviors submodule
|
|
198
|
+
import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
|
|
421
199
|
```
|
|
422
200
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
Two-way field synchronization:
|
|
201
|
+
## 7. FORM TYPE DEFINITION
|
|
426
202
|
|
|
427
203
|
```typescript
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
```
|
|
204
|
+
// ✅ CORRECT form type definition
|
|
205
|
+
interface MyForm {
|
|
206
|
+
// Required fields
|
|
207
|
+
name: string;
|
|
208
|
+
email: string;
|
|
434
209
|
|
|
435
|
-
|
|
210
|
+
// Optional fields - use undefined, not null
|
|
211
|
+
phone?: string;
|
|
212
|
+
age?: number;
|
|
436
213
|
|
|
437
|
-
|
|
214
|
+
// Enum/union types
|
|
215
|
+
status: 'active' | 'inactive';
|
|
438
216
|
|
|
439
|
-
|
|
440
|
-
|
|
217
|
+
// Nested objects
|
|
218
|
+
address: {
|
|
219
|
+
street: string;
|
|
220
|
+
city: string;
|
|
221
|
+
};
|
|
441
222
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
223
|
+
// Arrays - use tuple format for schema
|
|
224
|
+
items: Array<{
|
|
225
|
+
id: string;
|
|
226
|
+
name: string;
|
|
227
|
+
}>;
|
|
445
228
|
}
|
|
446
229
|
```
|
|
447
230
|
|
|
448
|
-
|
|
231
|
+
## 8. FORMSCHEMA FORMAT (CRITICALLY IMPORTANT)
|
|
449
232
|
|
|
450
|
-
|
|
233
|
+
⚠️ **Every field MUST have `value` and `component` properties!**
|
|
451
234
|
|
|
452
|
-
|
|
453
|
-
import { revalidateWhen } from 'reformer/behaviors';
|
|
235
|
+
### FieldConfig Interface
|
|
454
236
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
237
|
+
```typescript
|
|
238
|
+
interface FieldConfig<T> {
|
|
239
|
+
value: T | null; // Initial value (REQUIRED)
|
|
240
|
+
component: ComponentType; // React component (REQUIRED)
|
|
241
|
+
componentProps?: object; // Props passed to component
|
|
242
|
+
disabled?: boolean; // Disable field initially
|
|
243
|
+
validators?: ValidatorFn[]; // Sync validators
|
|
244
|
+
asyncValidators?: AsyncValidatorFn[]; // Async validators
|
|
245
|
+
updateOn?: 'change' | 'blur' | 'submit';
|
|
246
|
+
debounce?: number;
|
|
458
247
|
}
|
|
459
248
|
```
|
|
460
249
|
|
|
461
|
-
###
|
|
462
|
-
|
|
463
|
-
Create reusable custom behaviors:
|
|
250
|
+
### Primitive Fields
|
|
464
251
|
|
|
465
252
|
```typescript
|
|
466
|
-
|
|
467
|
-
import { Behavior } from 'reformer';
|
|
468
|
-
|
|
469
|
-
interface AutoSaveOptions {
|
|
470
|
-
debounce?: number;
|
|
471
|
-
onSave: (data: any) => Promise<void>;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export function autoSave<T>(options: AutoSaveOptions): Behavior<T> {
|
|
475
|
-
const { debounce = 1000, onSave } = options;
|
|
476
|
-
let timeoutId: NodeJS.Timeout;
|
|
477
|
-
|
|
478
|
-
return {
|
|
479
|
-
key: 'autoSave',
|
|
480
|
-
paths: [], // Empty = listen to all fields
|
|
481
|
-
run: (values, ctx) => {
|
|
482
|
-
clearTimeout(timeoutId);
|
|
483
|
-
timeoutId = setTimeout(async () => {
|
|
484
|
-
await onSave(ctx.form.getValue());
|
|
485
|
-
}, debounce);
|
|
486
|
-
},
|
|
487
|
-
cleanup: () => clearTimeout(timeoutId),
|
|
488
|
-
};
|
|
489
|
-
}
|
|
253
|
+
import { Input, Select, Checkbox } from '@/components/ui';
|
|
490
254
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
255
|
+
const schema: FormSchema<MyForm> = {
|
|
256
|
+
// String field
|
|
257
|
+
name: {
|
|
258
|
+
value: '', // Initial value (REQUIRED)
|
|
259
|
+
component: Input, // React component (REQUIRED)
|
|
260
|
+
componentProps: {
|
|
261
|
+
label: 'Name',
|
|
262
|
+
placeholder: 'Enter name',
|
|
497
263
|
},
|
|
498
|
-
}
|
|
499
|
-
];
|
|
500
|
-
```
|
|
264
|
+
},
|
|
501
265
|
|
|
502
|
-
|
|
266
|
+
// Number field (optional)
|
|
267
|
+
age: {
|
|
268
|
+
value: undefined, // Use undefined, NOT null
|
|
269
|
+
component: Input,
|
|
270
|
+
componentProps: { type: 'number', label: 'Age' },
|
|
271
|
+
},
|
|
503
272
|
|
|
504
|
-
|
|
273
|
+
// Boolean field
|
|
274
|
+
agree: {
|
|
275
|
+
value: false,
|
|
276
|
+
component: Checkbox,
|
|
277
|
+
componentProps: { label: 'I agree to terms' },
|
|
278
|
+
},
|
|
505
279
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
│ │ └── AddressForm.tsx
|
|
280
|
+
// Enum/Select field
|
|
281
|
+
status: {
|
|
282
|
+
value: 'active',
|
|
283
|
+
component: Select,
|
|
284
|
+
componentProps: {
|
|
285
|
+
label: 'Status',
|
|
286
|
+
options: [
|
|
287
|
+
{ value: 'active', label: 'Active' },
|
|
288
|
+
{ value: 'inactive', label: 'Inactive' },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
520
293
|
```
|
|
521
294
|
|
|
522
|
-
###
|
|
295
|
+
### Nested Objects
|
|
523
296
|
|
|
524
297
|
```typescript
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
createForm<UserProfile>({
|
|
533
|
-
form: {
|
|
534
|
-
name: { value: initial?.name ?? '', component: Input },
|
|
535
|
-
email: { value: initial?.email ?? '', component: Input },
|
|
536
|
-
// ...
|
|
537
|
-
},
|
|
538
|
-
validation,
|
|
539
|
-
behavior,
|
|
540
|
-
});
|
|
298
|
+
const schema: FormSchema<MyForm> = {
|
|
299
|
+
address: {
|
|
300
|
+
street: { value: '', component: Input, componentProps: { label: 'Street' } },
|
|
301
|
+
city: { value: '', component: Input, componentProps: { label: 'City' } },
|
|
302
|
+
zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
|
|
303
|
+
},
|
|
304
|
+
};
|
|
541
305
|
```
|
|
542
306
|
|
|
543
|
-
###
|
|
307
|
+
### Arrays (Tuple Format)
|
|
544
308
|
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
├── validators.ts # Combined + cross-step validators
|
|
551
|
-
├── behaviors.ts # Combined + cross-step behaviors
|
|
552
|
-
├── steps/
|
|
553
|
-
│ ├── shipping/
|
|
554
|
-
│ │ ├── type.ts
|
|
555
|
-
│ │ ├── schema.ts
|
|
556
|
-
│ │ ├── validators.ts
|
|
557
|
-
│ │ └── ShippingStep.tsx
|
|
558
|
-
│ ├── payment/
|
|
559
|
-
│ └── confirmation/
|
|
560
|
-
└── hooks/
|
|
561
|
-
└── useCheckoutNavigation.ts
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
## React Integration
|
|
309
|
+
```typescript
|
|
310
|
+
const itemSchema = {
|
|
311
|
+
id: { value: '', component: Input, componentProps: { label: 'ID' } },
|
|
312
|
+
name: { value: '', component: Input, componentProps: { label: 'Name' } },
|
|
313
|
+
};
|
|
565
314
|
|
|
566
|
-
|
|
315
|
+
const schema: FormSchema<MyForm> = {
|
|
316
|
+
items: [itemSchema], // Array with ONE template item
|
|
317
|
+
};
|
|
318
|
+
```
|
|
567
319
|
|
|
568
|
-
|
|
320
|
+
### ❌ WRONG - This will NOT compile
|
|
569
321
|
|
|
570
322
|
```typescript
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
valid, // Is valid
|
|
577
|
-
invalid, // Has errors
|
|
578
|
-
errors, // ValidationError[]
|
|
579
|
-
touched, // User interacted
|
|
580
|
-
disabled, // Is disabled
|
|
581
|
-
pending, // Async validation running
|
|
582
|
-
shouldShowError, // Show error (touched && invalid)
|
|
583
|
-
componentProps, // Custom props from schema
|
|
584
|
-
} = useFormControl(field);
|
|
585
|
-
|
|
586
|
-
return (
|
|
587
|
-
<div>
|
|
588
|
-
<input
|
|
589
|
-
value={value}
|
|
590
|
-
onChange={(e) => field.setValue(e.target.value)}
|
|
591
|
-
onBlur={() => field.markAsTouched()}
|
|
592
|
-
disabled={disabled}
|
|
593
|
-
/>
|
|
594
|
-
{shouldShowError && errors[0] && (
|
|
595
|
-
<span className="error">{errors[0].message}</span>
|
|
596
|
-
)}
|
|
597
|
-
</div>
|
|
598
|
-
);
|
|
599
|
-
}
|
|
323
|
+
// Missing value and component - TypeScript will error!
|
|
324
|
+
const schema = {
|
|
325
|
+
name: '', // ❌ Wrong
|
|
326
|
+
email: '', // ❌ Wrong
|
|
327
|
+
};
|
|
600
328
|
```
|
|
601
329
|
|
|
602
|
-
###
|
|
603
|
-
|
|
604
|
-
Lightweight hook - returns only value (better performance):
|
|
330
|
+
### createForm API
|
|
605
331
|
|
|
606
332
|
```typescript
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
//
|
|
611
|
-
|
|
333
|
+
// Full config with behavior and validation
|
|
334
|
+
const form = createForm<MyForm>({
|
|
335
|
+
form: formSchema, // Required: form schema with FieldConfig
|
|
336
|
+
behavior: behaviorSchema, // Optional: behavior rules
|
|
337
|
+
validation: validationSchema, // Optional: validation rules
|
|
338
|
+
});
|
|
612
339
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
340
|
+
// Access form controls
|
|
341
|
+
form.name.setValue('John');
|
|
342
|
+
form.address.city.value.value; // Get current value
|
|
343
|
+
form.items.push({ id: '1', name: 'Item' }); // Array operations
|
|
616
344
|
```
|
|
617
345
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
- Uses `useSyncExternalStore` for React 18+ integration
|
|
621
|
-
- Fine-grained updates - only affected components re-render
|
|
622
|
-
- Memoized state objects prevent unnecessary re-renders
|
|
623
|
-
- Use `useFormControlValue` when you only need the value
|
|
624
|
-
|
|
625
|
-
## API Reference
|
|
626
|
-
|
|
627
|
-
### createForm<T>(config)
|
|
628
|
-
|
|
629
|
-
Creates a new form instance with type-safe proxy access.
|
|
346
|
+
## 9. ARRAY SCHEMA FORMAT
|
|
630
347
|
|
|
631
348
|
```typescript
|
|
632
|
-
|
|
349
|
+
// ✅ CORRECT - use tuple format for arrays
|
|
350
|
+
const schema = {
|
|
351
|
+
items: [itemSchema] as [typeof itemSchema],
|
|
352
|
+
properties: [propertySchema] as [typeof propertySchema],
|
|
353
|
+
};
|
|
633
354
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
355
|
+
// ❌ WRONG - object format is NOT supported
|
|
356
|
+
const schema = {
|
|
357
|
+
items: { schema: itemSchema, initialItems: [] }, // This will NOT work
|
|
358
|
+
};
|
|
639
359
|
```
|
|
640
360
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
All nodes have these Signal properties:
|
|
644
|
-
- `value` - Current value
|
|
645
|
-
- `valid` / `invalid` - Validation state
|
|
646
|
-
- `touched` / `untouched` - Interaction state
|
|
647
|
-
- `dirty` / `pristine` - Changed state
|
|
648
|
-
- `status` - 'valid' | 'invalid' | 'pending' | 'disabled'
|
|
649
|
-
- `disabled` - Is disabled
|
|
650
|
-
- `pending` - Async validation in progress
|
|
651
|
-
|
|
652
|
-
### Node Common Methods
|
|
361
|
+
## 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)
|
|
653
362
|
|
|
654
363
|
```typescript
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
enable(): void
|
|
661
|
-
validate(): Promise<void>
|
|
662
|
-
getErrors(filter?: (error: ValidationError) => boolean): ValidationError[]
|
|
663
|
-
```
|
|
364
|
+
// ✅ CORRECT - async watchField with ALL safeguards
|
|
365
|
+
watchField(
|
|
366
|
+
path.parentField,
|
|
367
|
+
async (value, ctx) => {
|
|
368
|
+
if (!value) return; // Guard clause
|
|
664
369
|
|
|
665
|
-
|
|
370
|
+
try {
|
|
371
|
+
const { data } = await fetchData(value);
|
|
372
|
+
ctx.form.dependentField.updateComponentProps({ options: data });
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error('Failed:', error);
|
|
375
|
+
ctx.form.dependentField.updateComponentProps({ options: [] });
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
{ immediate: false, debounce: 300 } // REQUIRED options
|
|
379
|
+
);
|
|
666
380
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
381
|
+
// ❌ WRONG - missing safeguards
|
|
382
|
+
watchField(path.field, async (value, ctx) => {
|
|
383
|
+
const { data } = await fetchData(value); // Will fail silently!
|
|
384
|
+
});
|
|
672
385
|
```
|
|
673
386
|
|
|
674
|
-
###
|
|
387
|
+
### Required Options for async watchField:
|
|
388
|
+
- `immediate: false` - prevents execution during initialization
|
|
389
|
+
- `debounce: 300` - prevents excessive API calls (300-500ms recommended)
|
|
390
|
+
- Guard clause - skip if value is empty
|
|
391
|
+
- try-catch - handle errors explicitly
|
|
392
|
+
|
|
393
|
+
## 11. ARRAY CLEANUP PATTERN
|
|
675
394
|
|
|
676
395
|
```typescript
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
396
|
+
// ✅ CORRECT - cleanup array when checkbox unchecked
|
|
397
|
+
watchField(
|
|
398
|
+
path.hasItems,
|
|
399
|
+
(hasItems, ctx) => {
|
|
400
|
+
if (!hasItems && ctx.form.items) {
|
|
401
|
+
ctx.form.items.clear();
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{ immediate: false }
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// ❌ WRONG - no immediate: false, no null check
|
|
408
|
+
watchField(path.hasItems, (hasItems, ctx) => {
|
|
409
|
+
if (!hasItems) ctx.form.items.clear(); // May crash on init!
|
|
410
|
+
});
|
|
684
411
|
```
|
|
685
412
|
|
|
686
|
-
|
|
413
|
+
## 12. MULTI-STEP FORM VALIDATION
|
|
687
414
|
|
|
688
415
|
```typescript
|
|
689
|
-
|
|
690
|
-
|
|
416
|
+
// Step-specific validation schemas
|
|
417
|
+
const step1Validation: ValidationSchemaFn<Form> = (path) => {
|
|
418
|
+
required(path.loanType);
|
|
419
|
+
required(path.loanAmount);
|
|
420
|
+
};
|
|
691
421
|
|
|
692
|
-
|
|
422
|
+
const step2Validation: ValidationSchemaFn<Form> = (path) => {
|
|
423
|
+
required(path.personalData.firstName);
|
|
424
|
+
required(path.personalData.lastName);
|
|
425
|
+
};
|
|
693
426
|
|
|
694
|
-
|
|
695
|
-
|
|
427
|
+
// STEP_VALIDATIONS map for useStepForm hook
|
|
428
|
+
export const STEP_VALIDATIONS = {
|
|
429
|
+
1: step1Validation,
|
|
430
|
+
2: step2Validation,
|
|
431
|
+
};
|
|
696
432
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
433
|
+
// Full validation (combines all steps)
|
|
434
|
+
export const fullValidation: ValidationSchemaFn<Form> = (path) => {
|
|
435
|
+
step1Validation(path);
|
|
436
|
+
step2Validation(path);
|
|
437
|
+
};
|
|
701
438
|
```
|
|
702
439
|
|
|
703
|
-
##
|
|
440
|
+
## 13. ⚠️ EXTENDED COMMON MISTAKES
|
|
704
441
|
|
|
705
|
-
###
|
|
442
|
+
### Behavior Composition (Cycle Error)
|
|
706
443
|
|
|
707
444
|
```typescript
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const validateStep = async () => {
|
|
713
|
-
const stepFields = getStepFields(step);
|
|
714
|
-
stepFields.forEach(f => f.markAsTouched());
|
|
715
|
-
await form.validate();
|
|
716
|
-
return stepFields.every(f => f.valid.value);
|
|
717
|
-
};
|
|
445
|
+
// ❌ WRONG - apply() in behavior causes "Cycle detected"
|
|
446
|
+
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
447
|
+
apply(addressBehavior, path.address); // WILL FAIL!
|
|
448
|
+
};
|
|
718
449
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
};
|
|
450
|
+
// ✅ CORRECT - inline or use setup function
|
|
451
|
+
const setupAddressBehavior = (path: FieldPath<Address>) => {
|
|
452
|
+
watchField(path.region, async (region, ctx) => {
|
|
453
|
+
// ...
|
|
454
|
+
}, { immediate: false });
|
|
455
|
+
};
|
|
724
456
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
{step === 1 && <PaymentStep form={form} />}
|
|
729
|
-
{step === 2 && <ConfirmationStep form={form} />}
|
|
730
|
-
|
|
731
|
-
<button onClick={() => setStep(s => s - 1)} disabled={step === 0}>
|
|
732
|
-
Back
|
|
733
|
-
</button>
|
|
734
|
-
<button onClick={handleNext}>
|
|
735
|
-
{step === 2 ? 'Submit' : 'Next'}
|
|
736
|
-
</button>
|
|
737
|
-
</div>
|
|
738
|
-
);
|
|
739
|
-
}
|
|
457
|
+
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
458
|
+
setupAddressBehavior(path.address); // Works!
|
|
459
|
+
};
|
|
740
460
|
```
|
|
741
461
|
|
|
742
|
-
###
|
|
462
|
+
### Infinite Loop in watchField
|
|
743
463
|
|
|
744
464
|
```typescript
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
city: { value: '', component: Input, componentProps: { label: 'City' } },
|
|
749
|
-
zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
// main form
|
|
753
|
-
const form = createForm<OrderForm>({
|
|
754
|
-
form: {
|
|
755
|
-
billingAddress: addressSchema,
|
|
756
|
-
shippingAddress: addressSchema,
|
|
757
|
-
},
|
|
465
|
+
// ❌ WRONG - causes infinite loop
|
|
466
|
+
watchField(path.field, (value, ctx) => {
|
|
467
|
+
ctx.form.field.setValue(value.toUpperCase()); // Loop!
|
|
758
468
|
});
|
|
469
|
+
|
|
470
|
+
// ✅ CORRECT - write to different field OR add guard
|
|
471
|
+
watchField(path.input, (value, ctx) => {
|
|
472
|
+
const upper = value?.toUpperCase() || '';
|
|
473
|
+
if (ctx.form.display.value.value !== upper) {
|
|
474
|
+
ctx.form.display.setValue(upper);
|
|
475
|
+
}
|
|
476
|
+
}, { immediate: false });
|
|
759
477
|
```
|
|
760
478
|
|
|
761
|
-
###
|
|
479
|
+
### validateTree Typing
|
|
762
480
|
|
|
763
481
|
```typescript
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
return (
|
|
768
|
-
<div>
|
|
769
|
-
{array.controls.map((phone, index) => (
|
|
770
|
-
<div key={phone.id}>
|
|
771
|
-
<FormField field={phone.controls.type} />
|
|
772
|
-
<FormField field={phone.controls.number} />
|
|
773
|
-
<button onClick={() => array.removeAt(index)}>Remove</button>
|
|
774
|
-
</div>
|
|
775
|
-
))}
|
|
776
|
-
|
|
777
|
-
<button onClick={() => array.push({ type: 'mobile', number: '' })}>
|
|
778
|
-
Add Phone
|
|
779
|
-
</button>
|
|
780
|
-
</div>
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
```
|
|
482
|
+
// ❌ WRONG - implicit any
|
|
483
|
+
validateTree((ctx) => { ... });
|
|
784
484
|
|
|
785
|
-
|
|
485
|
+
// ✅ CORRECT - explicit typing
|
|
486
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
487
|
+
if (ctx.form.field1 > ctx.form.field2) {
|
|
488
|
+
return { code: 'error', message: 'Invalid' };
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
});
|
|
492
|
+
```
|
|
786
493
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
494
|
+
## 14. PROJECT STRUCTURE (COLOCATION)
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
src/
|
|
498
|
+
├── components/ui/ # Reusable UI components
|
|
499
|
+
│ ├── FormField.tsx
|
|
500
|
+
│ └── FormArrayManager.tsx
|
|
501
|
+
│
|
|
502
|
+
├── forms/
|
|
503
|
+
│ └── [form-name]/ # Form module
|
|
504
|
+
│ ├── type.ts # Main form type
|
|
505
|
+
│ ├── schema.ts # Main schema
|
|
506
|
+
│ ├── validators.ts # Validators
|
|
507
|
+
│ ├── behaviors.ts # Behaviors
|
|
508
|
+
│ ├── [FormName]Form.tsx # Main component
|
|
509
|
+
│ │
|
|
510
|
+
│ ├── steps/ # Multi-step wizard
|
|
511
|
+
│ │ ├── loan-info/
|
|
512
|
+
│ │ │ ├── type.ts
|
|
513
|
+
│ │ │ ├── schema.ts
|
|
514
|
+
│ │ │ ├── validators.ts
|
|
515
|
+
│ │ │ ├── behaviors.ts
|
|
516
|
+
│ │ │ └── LoanInfoForm.tsx
|
|
517
|
+
│ │ └── ...
|
|
518
|
+
│ │
|
|
519
|
+
│ └── sub-forms/ # Reusable sub-forms
|
|
520
|
+
│ ├── address/
|
|
521
|
+
│ └── personal-data/
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Key Files
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// forms/credit-application/type.ts
|
|
528
|
+
export type { LoanInfoStep } from './steps/loan-info/type';
|
|
529
|
+
export interface CreditApplicationForm {
|
|
530
|
+
loanType: LoanType;
|
|
531
|
+
loanAmount: number;
|
|
532
|
+
// ...
|
|
796
533
|
}
|
|
797
|
-
```
|
|
798
534
|
|
|
799
|
-
|
|
535
|
+
// forms/credit-application/schema.ts
|
|
536
|
+
import { loanInfoSchema } from './steps/loan-info/schema';
|
|
537
|
+
export const creditApplicationSchema = {
|
|
538
|
+
...loanInfoSchema,
|
|
539
|
+
monthlyPayment: { value: 0, disabled: true },
|
|
540
|
+
};
|
|
800
541
|
|
|
801
|
-
|
|
802
|
-
|
|
542
|
+
// forms/credit-application/validators.ts
|
|
543
|
+
import { loanValidation } from './steps/loan-info/validators';
|
|
544
|
+
export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
|
|
545
|
+
loanValidation(path);
|
|
546
|
+
// Cross-step validation...
|
|
547
|
+
};
|
|
548
|
+
```
|
|
803
549
|
|
|
804
|
-
###
|
|
805
|
-
A: Check `updateOn` option in field config. Default is 'change'. For blur-triggered validation use `updateOn: 'blur'`.
|
|
550
|
+
### Scaling
|
|
806
551
|
|
|
807
|
-
|
|
808
|
-
|
|
552
|
+
| Complexity | Structure |
|
|
553
|
+
|------------|-----------|
|
|
554
|
+
| Simple | Single file: `ContactForm.tsx` |
|
|
555
|
+
| Medium | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
|
|
556
|
+
| Complex | Full colocation with `steps/` and `sub-forms/` |
|
|
809
557
|
|
|
810
|
-
|
|
811
|
-
A: Ensure your type interface matches the schema structure exactly. Use `createForm<YourType>()` for proper type inference.
|
|
558
|
+
## 15. NON-EXISTENT API (DO NOT USE)
|
|
812
559
|
|
|
813
|
-
|
|
814
|
-
A: Call `form.reset()` for single field or `form.resetAll()` for GroupNode to reset all children.
|
|
560
|
+
⚠️ **The following APIs do NOT exist in @reformer/core:**
|
|
815
561
|
|
|
816
|
-
|
|
817
|
-
|
|
562
|
+
| ❌ Wrong | ✅ Correct | Notes |
|
|
563
|
+
|----------|-----------|-------|
|
|
564
|
+
| `useForm` | `createForm` | There is no useForm hook |
|
|
565
|
+
| `FieldSchema` | `FieldConfig<T>` | Type for individual field config |
|
|
566
|
+
| `when()` | `applyWhen()` | Conditional validation function |
|
|
567
|
+
| `FormFields` | `FieldNode<T>` | Type for field nodes |
|
|
818
568
|
|
|
819
|
-
###
|
|
820
|
-
A: Use `form.patchValue({ field1: 'value1', field2: 'value2' })` to update multiple fields at once.
|
|
569
|
+
### Common Import Errors
|
|
821
570
|
|
|
822
|
-
### Q: Form instance recreated on every render?
|
|
823
|
-
A: Wrap `createForm()` in `useMemo()`:
|
|
824
571
|
```typescript
|
|
825
|
-
|
|
572
|
+
// ❌ WRONG - These do NOT exist
|
|
573
|
+
import { useForm } from '@reformer/core'; // NO!
|
|
574
|
+
import { when } from '@reformer/core/validators'; // NO!
|
|
575
|
+
import type { FieldSchema } from '@reformer/core'; // NO!
|
|
576
|
+
import type { FormFields } from '@reformer/core'; // NO!
|
|
577
|
+
|
|
578
|
+
// ✅ CORRECT
|
|
579
|
+
import { createForm, useFormControl } from '@reformer/core';
|
|
580
|
+
import { applyWhen } from '@reformer/core/validators';
|
|
581
|
+
import type { FieldConfig, FieldNode } from '@reformer/core';
|
|
826
582
|
```
|
|
827
583
|
|
|
828
|
-
###
|
|
829
|
-
|
|
584
|
+
### FormSchema Common Mistakes
|
|
585
|
+
|
|
830
586
|
```typescript
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (form.valid.value) {
|
|
837
|
-
const data = form.value.value;
|
|
838
|
-
await submitToServer(data);
|
|
839
|
-
}
|
|
587
|
+
// ❌ WRONG - Simple values don't work
|
|
588
|
+
const schema = {
|
|
589
|
+
name: '', // Missing { value, component }
|
|
590
|
+
email: '', // Missing { value, component }
|
|
840
591
|
};
|
|
841
|
-
```
|
|
842
592
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
593
|
+
// ✅ CORRECT - Every field needs value and component
|
|
594
|
+
const schema: FormSchema<MyForm> = {
|
|
595
|
+
name: {
|
|
596
|
+
value: '',
|
|
597
|
+
component: Input,
|
|
598
|
+
componentProps: { label: 'Name' },
|
|
599
|
+
},
|
|
600
|
+
email: {
|
|
601
|
+
value: '',
|
|
602
|
+
component: Input,
|
|
603
|
+
componentProps: { label: 'Email', type: 'email' },
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
```
|