@reformer/core 1.0.0 → 1.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +301 -725
- package/package.json +6 -5
package/llms.txt
CHANGED
|
@@ -1,847 +1,423 @@
|
|
|
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
|
-
- React 18+ or React 19+
|
|
15
|
-
- @preact/signals-core ^1.8.0
|
|
16
|
-
|
|
17
|
-
## Quick Start
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
import { createForm } from 'reformer';
|
|
21
|
-
import { required, email, minLength } from 'reformer/validators';
|
|
22
|
-
import { useFormControl } from 'reformer';
|
|
7
|
+
| What | Where |
|
|
8
|
+
| ------------------------------------------------------------------------------------------- | --------------------------- |
|
|
9
|
+
| `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `GroupNodeWithControls`, `FieldNode` | `@reformer/core` |
|
|
10
|
+
| `required`, `min`, `max`, `minLength`, `email`, `validate`, `validateTree` | `@reformer/core/validators` |
|
|
11
|
+
| `computeFrom`, `enableWhen`, `disableWhen`, `copyFrom`, `watchField` | `@reformer/core/behaviors` |
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
type ContactForm = {
|
|
26
|
-
name: string;
|
|
27
|
-
email: string;
|
|
28
|
-
message: string;
|
|
29
|
-
};
|
|
13
|
+
### Type Values
|
|
30
14
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
```
|
|
15
|
+
- Optional numbers: `number | undefined` (NOT `null`)
|
|
16
|
+
- Optional strings: `string` (empty string by default)
|
|
17
|
+
- Do NOT add `[key: string]: unknown` to form interfaces
|
|
60
18
|
|
|
61
|
-
##
|
|
19
|
+
## 2. API SIGNATURES
|
|
62
20
|
|
|
63
|
-
###
|
|
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)
|
|
81
|
-
|
|
82
|
-
### Signals-based Reactivity
|
|
83
|
-
|
|
84
|
-
- Uses @preact/signals-core for fine-grained reactivity
|
|
85
|
-
- Only affected components re-render when values change
|
|
86
|
-
- React integration via useSyncExternalStore
|
|
87
|
-
|
|
88
|
-
## Form Schema
|
|
89
|
-
|
|
90
|
-
### FieldConfig<T>
|
|
21
|
+
### Validators
|
|
91
22
|
|
|
92
23
|
```typescript
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
24
|
+
required(path, options?: { message?: string })
|
|
25
|
+
min(path, value: number, options?: { message?: string })
|
|
26
|
+
max(path, value: number, options?: { message?: string })
|
|
27
|
+
minLength(path, length: number, options?: { message?: string })
|
|
28
|
+
maxLength(path, length: number, options?: { message?: string })
|
|
29
|
+
email(path, options?: { message?: string })
|
|
30
|
+
validate(path, validator: (value) => ValidationError | null)
|
|
31
|
+
validateTree(validator: (ctx) => ValidationError | null)
|
|
32
|
+
applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
|
|
103
33
|
```
|
|
104
34
|
|
|
105
|
-
###
|
|
106
|
-
|
|
107
|
-
Arrays use single-element tuple syntax in schema:
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
interface FormType {
|
|
111
|
-
phones: { type: string; number: string }[];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const schema: FormSchema<FormType> = {
|
|
115
|
-
phones: [{
|
|
116
|
-
type: { value: 'mobile', component: Select },
|
|
117
|
-
number: { value: '', component: Input },
|
|
118
|
-
}],
|
|
119
|
-
};
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Complete Schema Example
|
|
35
|
+
### Behaviors
|
|
123
36
|
|
|
124
37
|
```typescript
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
age: number;
|
|
131
|
-
address: {
|
|
132
|
-
street: string;
|
|
133
|
-
city: string;
|
|
134
|
-
};
|
|
135
|
-
phones: { type: string; number: string }[];
|
|
136
|
-
};
|
|
38
|
+
enableWhen(path, condition: (form) => boolean, options?: { resetOnDisable?: boolean })
|
|
39
|
+
disableWhen(path, condition: (form) => boolean)
|
|
40
|
+
computeFrom(sourcePaths[], targetPath, compute: (values) => result)
|
|
41
|
+
watchField(path, callback: (value, ctx: BehaviorContext) => void)
|
|
42
|
+
copyFrom(sourcePath, targetPath, options?: { when?, fields?, transform? })
|
|
137
43
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
});
|
|
44
|
+
// BehaviorContext interface:
|
|
45
|
+
interface BehaviorContext<TForm> {
|
|
46
|
+
form: TForm; // Current form state
|
|
47
|
+
setFieldValue: (path, value) => void; // Set field value
|
|
48
|
+
getFieldValue: (path) => unknown; // Get field value
|
|
49
|
+
}
|
|
155
50
|
```
|
|
156
51
|
|
|
157
|
-
##
|
|
158
|
-
|
|
159
|
-
### FieldNode<T>
|
|
160
|
-
|
|
161
|
-
Represents a single form field value.
|
|
162
|
-
|
|
163
|
-
**Properties (all are Signals):**
|
|
164
|
-
- `value` - Current value
|
|
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
|
|
174
|
-
|
|
175
|
-
**Methods:**
|
|
176
|
-
- `setValue(value, options?)` - Set new value
|
|
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
|
|
183
|
-
|
|
184
|
-
### GroupNode<T>
|
|
52
|
+
## 3. COMMON PATTERNS
|
|
185
53
|
|
|
186
|
-
|
|
54
|
+
### Conditional Fields with Auto-Reset
|
|
187
55
|
|
|
188
|
-
**Properties:**
|
|
189
|
-
- `controls` - Dictionary of child nodes
|
|
190
|
-
- All FormNode properties (computed from children)
|
|
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
56
|
```typescript
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
form.phones // ArrayNode
|
|
57
|
+
enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
|
|
58
|
+
resetOnDisable: true,
|
|
59
|
+
});
|
|
204
60
|
```
|
|
205
61
|
|
|
206
|
-
###
|
|
207
|
-
|
|
208
|
-
Manages dynamic arrays.
|
|
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
|
|
62
|
+
### Computed Field from Nested to Root Level
|
|
221
63
|
|
|
222
64
|
```typescript
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
65
|
+
// DO NOT use computeFrom for cross-level computations
|
|
66
|
+
// Use watchField instead:
|
|
67
|
+
watchField(path.nested.field, (value, ctx) => {
|
|
68
|
+
ctx.setFieldValue('rootField', computedValue);
|
|
69
|
+
});
|
|
227
70
|
```
|
|
228
71
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
### ValidationSchemaFn
|
|
72
|
+
### Type-Safe useFormControl
|
|
232
73
|
|
|
233
74
|
```typescript
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const form = createForm<FormType>({
|
|
237
|
-
form: { /* schema */ },
|
|
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
|
-
},
|
|
257
|
-
});
|
|
75
|
+
const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
|
|
258
76
|
```
|
|
259
77
|
|
|
260
|
-
|
|
78
|
+
## 4. ⚠️ COMMON MISTAKES
|
|
261
79
|
|
|
262
|
-
|
|
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
|
|
80
|
+
### Validators
|
|
279
81
|
|
|
280
82
|
```typescript
|
|
281
|
-
//
|
|
282
|
-
|
|
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
|
-
}
|
|
83
|
+
// ❌ WRONG
|
|
84
|
+
required(path.email, 'Email is required');
|
|
298
85
|
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
required(path.password);
|
|
302
|
-
validate(path.password, strongPassword());
|
|
303
|
-
}
|
|
86
|
+
// ✅ CORRECT
|
|
87
|
+
required(path.email, { message: 'Email is required' });
|
|
304
88
|
```
|
|
305
89
|
|
|
306
|
-
###
|
|
90
|
+
### Types
|
|
307
91
|
|
|
308
92
|
```typescript
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
93
|
+
// ❌ WRONG
|
|
94
|
+
amount: number | null;
|
|
95
|
+
[key: string]: unknown;
|
|
312
96
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const response = await fetch(`/api/check-username?u=${value}`);
|
|
317
|
-
const { available } = await response.json();
|
|
318
|
-
|
|
319
|
-
if (!available) {
|
|
320
|
-
return { code: 'usernameTaken', message: 'Username is already taken' };
|
|
321
|
-
}
|
|
322
|
-
return null;
|
|
323
|
-
}, { debounce: 500 });
|
|
324
|
-
}
|
|
97
|
+
// ✅ CORRECT
|
|
98
|
+
amount: number | undefined;
|
|
99
|
+
// No index signature
|
|
325
100
|
```
|
|
326
101
|
|
|
327
|
-
### Cross-field Validation
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
import { validateTree } from 'reformer/validators';
|
|
331
|
-
|
|
332
|
-
validation: (path) => {
|
|
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
|
-
```
|
|
352
|
-
|
|
353
|
-
## Behaviors
|
|
354
|
-
|
|
355
|
-
Behaviors add reactive logic to forms. All imported from `reformer/behaviors`.
|
|
356
|
-
|
|
357
102
|
### computeFrom
|
|
358
103
|
|
|
359
|
-
Calculate field value from other fields:
|
|
360
|
-
|
|
361
104
|
```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
|
-
```
|
|
105
|
+
// ❌ WRONG - different nesting levels
|
|
106
|
+
computeFrom([path.nested.a, path.nested.b], path.root, ...)
|
|
373
107
|
|
|
374
|
-
|
|
108
|
+
// ✅ CORRECT - use watchField
|
|
109
|
+
watchField(path.nested.a, (_, ctx) => {
|
|
110
|
+
ctx.setFieldValue('root', computed);
|
|
111
|
+
});
|
|
112
|
+
```
|
|
375
113
|
|
|
376
|
-
|
|
114
|
+
### Imports
|
|
377
115
|
|
|
378
116
|
```typescript
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
behavior: (path) => {
|
|
382
|
-
// Enable discount field only when total > 500
|
|
383
|
-
enableWhen(path.discount, (form) => form.total > 500);
|
|
117
|
+
// ❌ WRONG - types are not in submodules
|
|
118
|
+
import { ValidationSchemaFn } from '@reformer/core/validators';
|
|
384
119
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
120
|
+
// ✅ CORRECT - types from main module
|
|
121
|
+
import type { ValidationSchemaFn } from '@reformer/core';
|
|
122
|
+
import { required, email } from '@reformer/core/validators';
|
|
388
123
|
```
|
|
389
124
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
React to field changes with custom logic:
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
import { watchField } from 'reformer/behaviors';
|
|
396
|
-
|
|
397
|
-
behavior: (path) => {
|
|
398
|
-
// Load cities when country changes
|
|
399
|
-
watchField(path.country, async (value, ctx) => {
|
|
400
|
-
const cities = await fetchCities(value);
|
|
401
|
-
ctx.form.city.updateComponentProps({ options: cities });
|
|
402
|
-
ctx.form.city.setValue(''); // Reset city selection
|
|
403
|
-
}, { debounce: 300 });
|
|
404
|
-
}
|
|
405
|
-
```
|
|
125
|
+
## 5. TROUBLESHOOTING
|
|
406
126
|
|
|
407
|
-
|
|
127
|
+
| Error | Cause | Solution |
|
|
128
|
+
| ------------------------------------------------------ | ------------------------------ | --------------------------------- |
|
|
129
|
+
| `'string' is not assignable to '{ message?: string }'` | Wrong validator format | Use `{ message: 'text' }` |
|
|
130
|
+
| `'null' is not assignable to 'undefined'` | Wrong optional type | Replace `null` with `undefined` |
|
|
131
|
+
| `FormFields[]` instead of concrete type | Type inference issue | Use `as FieldNode<T>` |
|
|
132
|
+
| `Type 'X' is missing properties from type 'Y'` | Cross-level computeFrom | Use watchField instead |
|
|
133
|
+
| `Module has no exported member` | Wrong import source | Types from core, functions from submodules |
|
|
408
134
|
|
|
409
|
-
|
|
135
|
+
## 6. COMPLETE IMPORT EXAMPLE
|
|
410
136
|
|
|
411
137
|
```typescript
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### syncFields
|
|
138
|
+
// Types - always from @reformer/core
|
|
139
|
+
import type {
|
|
140
|
+
ValidationSchemaFn,
|
|
141
|
+
BehaviorSchemaFn,
|
|
142
|
+
FieldPath,
|
|
143
|
+
GroupNodeWithControls,
|
|
144
|
+
FieldNode,
|
|
145
|
+
} from '@reformer/core';
|
|
424
146
|
|
|
425
|
-
|
|
147
|
+
// Core functions
|
|
148
|
+
import { createForm, useFormControl } from '@reformer/core';
|
|
426
149
|
|
|
427
|
-
|
|
428
|
-
import {
|
|
150
|
+
// Validators - from /validators submodule
|
|
151
|
+
import { required, min, max, email, validate, applyWhen } from '@reformer/core/validators';
|
|
429
152
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
153
|
+
// Behaviors - from /behaviors submodule
|
|
154
|
+
import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
|
|
433
155
|
```
|
|
434
156
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
Reset field when condition is met:
|
|
157
|
+
## 7. FORM TYPE DEFINITION
|
|
438
158
|
|
|
439
159
|
```typescript
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
```
|
|
160
|
+
// ✅ CORRECT form type definition
|
|
161
|
+
interface MyForm {
|
|
162
|
+
// Required fields
|
|
163
|
+
name: string;
|
|
164
|
+
email: string;
|
|
447
165
|
|
|
448
|
-
|
|
166
|
+
// Optional fields - use undefined, not null
|
|
167
|
+
phone?: string;
|
|
168
|
+
age?: number;
|
|
449
169
|
|
|
450
|
-
|
|
170
|
+
// Enum/union types
|
|
171
|
+
status: 'active' | 'inactive';
|
|
451
172
|
|
|
452
|
-
|
|
453
|
-
|
|
173
|
+
// Nested objects
|
|
174
|
+
address: {
|
|
175
|
+
street: string;
|
|
176
|
+
city: string;
|
|
177
|
+
};
|
|
454
178
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
179
|
+
// Arrays - use tuple format for schema
|
|
180
|
+
items: Array<{
|
|
181
|
+
id: string;
|
|
182
|
+
name: string;
|
|
183
|
+
}>;
|
|
458
184
|
}
|
|
459
185
|
```
|
|
460
186
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
Create reusable custom behaviors:
|
|
187
|
+
## 8. CREATEFORM API
|
|
464
188
|
|
|
465
189
|
```typescript
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
}
|
|
490
|
-
|
|
491
|
-
// Usage
|
|
492
|
-
behaviors: (path, { use }) => [
|
|
493
|
-
use(autoSave({
|
|
494
|
-
debounce: 2000,
|
|
495
|
-
onSave: async (data) => {
|
|
496
|
-
await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
|
|
497
|
-
},
|
|
498
|
-
})),
|
|
499
|
-
];
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
## Recommended Project Structure
|
|
190
|
+
// Full config with behavior and validation
|
|
191
|
+
const form = createForm<MyForm>({
|
|
192
|
+
form: formSchema, // Required: form schema
|
|
193
|
+
behavior: behaviorSchema, // Optional: behavior rules
|
|
194
|
+
validation: validationSchema, // Optional: validation rules
|
|
195
|
+
});
|
|
503
196
|
|
|
504
|
-
|
|
197
|
+
// Legacy format (schema only)
|
|
198
|
+
const form = createForm<MyForm>(formSchema);
|
|
505
199
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
│ │ ├── schema.ts
|
|
518
|
-
│ │ ├── validators.ts
|
|
519
|
-
│ │ └── AddressForm.tsx
|
|
200
|
+
// Form schema example
|
|
201
|
+
const formSchema: FormSchema<MyForm> = {
|
|
202
|
+
name: '',
|
|
203
|
+
email: '',
|
|
204
|
+
address: {
|
|
205
|
+
street: '',
|
|
206
|
+
city: '',
|
|
207
|
+
},
|
|
208
|
+
// Arrays use tuple format
|
|
209
|
+
items: [{ id: '', name: '' }] as [{ id: string; name: string }],
|
|
210
|
+
};
|
|
520
211
|
```
|
|
521
212
|
|
|
522
|
-
|
|
213
|
+
## 9. ARRAY SCHEMA FORMAT
|
|
523
214
|
|
|
524
215
|
```typescript
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
export const createUserProfileForm = (initial?: Partial<UserProfile>) =>
|
|
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
|
-
});
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Multi-step Form Structure
|
|
544
|
-
|
|
545
|
-
```
|
|
546
|
-
src/forms/checkout/
|
|
547
|
-
├── CheckoutForm.tsx # Main form component
|
|
548
|
-
├── type.ts # Combined type
|
|
549
|
-
├── schema.ts # Combined schema
|
|
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
|
|
565
|
-
|
|
566
|
-
### useFormControl<T>
|
|
567
|
-
|
|
568
|
-
Subscribe to all field state changes:
|
|
216
|
+
// ✅ CORRECT - use tuple format for arrays
|
|
217
|
+
const schema = {
|
|
218
|
+
items: [itemSchema] as [typeof itemSchema],
|
|
219
|
+
properties: [propertySchema] as [typeof propertySchema],
|
|
220
|
+
};
|
|
569
221
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const {
|
|
575
|
-
value, // Current value
|
|
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
|
-
}
|
|
222
|
+
// ❌ WRONG - object format is NOT supported
|
|
223
|
+
const schema = {
|
|
224
|
+
items: { schema: itemSchema, initialItems: [] }, // This will NOT work
|
|
225
|
+
};
|
|
600
226
|
```
|
|
601
227
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
Lightweight hook - returns only value (better performance):
|
|
228
|
+
## 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)
|
|
605
229
|
|
|
606
230
|
```typescript
|
|
607
|
-
|
|
231
|
+
// ✅ CORRECT - async watchField with ALL safeguards
|
|
232
|
+
watchField(
|
|
233
|
+
path.parentField,
|
|
234
|
+
async (value, ctx) => {
|
|
235
|
+
if (!value) return; // Guard clause
|
|
608
236
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
237
|
+
try {
|
|
238
|
+
const { data } = await fetchData(value);
|
|
239
|
+
ctx.form.dependentField.updateComponentProps({ options: data });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('Failed:', error);
|
|
242
|
+
ctx.form.dependentField.updateComponentProps({ options: [] });
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{ immediate: false, debounce: 300 } // REQUIRED options
|
|
246
|
+
);
|
|
612
247
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
248
|
+
// ❌ WRONG - missing safeguards
|
|
249
|
+
watchField(path.field, async (value, ctx) => {
|
|
250
|
+
const { data } = await fetchData(value); // Will fail silently!
|
|
251
|
+
});
|
|
616
252
|
```
|
|
617
253
|
|
|
618
|
-
###
|
|
619
|
-
|
|
620
|
-
-
|
|
621
|
-
-
|
|
622
|
-
-
|
|
623
|
-
- Use `useFormControlValue` when you only need the value
|
|
624
|
-
|
|
625
|
-
## API Reference
|
|
254
|
+
### Required Options for async watchField:
|
|
255
|
+
- `immediate: false` - prevents execution during initialization
|
|
256
|
+
- `debounce: 300` - prevents excessive API calls (300-500ms recommended)
|
|
257
|
+
- Guard clause - skip if value is empty
|
|
258
|
+
- try-catch - handle errors explicitly
|
|
626
259
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
Creates a new form instance with type-safe proxy access.
|
|
260
|
+
## 11. ARRAY CLEANUP PATTERN
|
|
630
261
|
|
|
631
262
|
```typescript
|
|
632
|
-
|
|
263
|
+
// ✅ CORRECT - cleanup array when checkbox unchecked
|
|
264
|
+
watchField(
|
|
265
|
+
path.hasItems,
|
|
266
|
+
(hasItems, ctx) => {
|
|
267
|
+
if (!hasItems && ctx.form.items) {
|
|
268
|
+
ctx.form.items.clear();
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{ immediate: false }
|
|
272
|
+
);
|
|
633
273
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
274
|
+
// ❌ WRONG - no immediate: false, no null check
|
|
275
|
+
watchField(path.hasItems, (hasItems, ctx) => {
|
|
276
|
+
if (!hasItems) ctx.form.items.clear(); // May crash on init!
|
|
277
|
+
});
|
|
639
278
|
```
|
|
640
279
|
|
|
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
|
|
280
|
+
## 12. MULTI-STEP FORM VALIDATION
|
|
653
281
|
|
|
654
282
|
```typescript
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
enable(): void
|
|
661
|
-
validate(): Promise<void>
|
|
662
|
-
getErrors(filter?: (error: ValidationError) => boolean): ValidationError[]
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### SetValueOptions
|
|
283
|
+
// Step-specific validation schemas
|
|
284
|
+
const step1Validation: ValidationSchemaFn<Form> = (path) => {
|
|
285
|
+
required(path.loanType);
|
|
286
|
+
required(path.loanAmount);
|
|
287
|
+
};
|
|
666
288
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
```
|
|
289
|
+
const step2Validation: ValidationSchemaFn<Form> = (path) => {
|
|
290
|
+
required(path.personalData.firstName);
|
|
291
|
+
required(path.personalData.lastName);
|
|
292
|
+
};
|
|
673
293
|
|
|
674
|
-
|
|
294
|
+
// STEP_VALIDATIONS map for useStepForm hook
|
|
295
|
+
export const STEP_VALIDATIONS = {
|
|
296
|
+
1: step1Validation,
|
|
297
|
+
2: step2Validation,
|
|
298
|
+
};
|
|
675
299
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
severity?: 'error' | 'warning'; // Severity level
|
|
682
|
-
path?: string; // Field path (for cross-field)
|
|
683
|
-
}
|
|
300
|
+
// Full validation (combines all steps)
|
|
301
|
+
export const fullValidation: ValidationSchemaFn<Form> = (path) => {
|
|
302
|
+
step1Validation(path);
|
|
303
|
+
step2Validation(path);
|
|
304
|
+
};
|
|
684
305
|
```
|
|
685
306
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
```typescript
|
|
689
|
-
type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
|
|
690
|
-
```
|
|
307
|
+
## 13. ⚠️ EXTENDED COMMON MISTAKES
|
|
691
308
|
|
|
692
|
-
###
|
|
309
|
+
### Behavior Composition (Cycle Error)
|
|
693
310
|
|
|
694
311
|
```typescript
|
|
695
|
-
|
|
312
|
+
// ❌ WRONG - apply() in behavior causes "Cycle detected"
|
|
313
|
+
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
314
|
+
apply(addressBehavior, path.address); // WILL FAIL!
|
|
315
|
+
};
|
|
696
316
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
317
|
+
// ✅ CORRECT - inline or use setup function
|
|
318
|
+
const setupAddressBehavior = (path: FieldPath<Address>) => {
|
|
319
|
+
watchField(path.region, async (region, ctx) => {
|
|
320
|
+
// ...
|
|
321
|
+
}, { immediate: false });
|
|
322
|
+
};
|
|
702
323
|
|
|
703
|
-
|
|
324
|
+
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
325
|
+
setupAddressBehavior(path.address); // Works!
|
|
326
|
+
};
|
|
327
|
+
```
|
|
704
328
|
|
|
705
|
-
###
|
|
329
|
+
### Infinite Loop in watchField
|
|
706
330
|
|
|
707
331
|
```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
|
-
};
|
|
718
|
-
|
|
719
|
-
const handleNext = async () => {
|
|
720
|
-
if (await validateStep()) {
|
|
721
|
-
setStep(s => s + 1);
|
|
722
|
-
}
|
|
723
|
-
};
|
|
332
|
+
// ❌ WRONG - causes infinite loop
|
|
333
|
+
watchField(path.field, (value, ctx) => {
|
|
334
|
+
ctx.form.field.setValue(value.toUpperCase()); // Loop!
|
|
335
|
+
});
|
|
724
336
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
Back
|
|
733
|
-
</button>
|
|
734
|
-
<button onClick={handleNext}>
|
|
735
|
-
{step === 2 ? 'Submit' : 'Next'}
|
|
736
|
-
</button>
|
|
737
|
-
</div>
|
|
738
|
-
);
|
|
739
|
-
}
|
|
337
|
+
// ✅ CORRECT - write to different field OR add guard
|
|
338
|
+
watchField(path.input, (value, ctx) => {
|
|
339
|
+
const upper = value?.toUpperCase() || '';
|
|
340
|
+
if (ctx.form.display.value.value !== upper) {
|
|
341
|
+
ctx.form.display.setValue(upper);
|
|
342
|
+
}
|
|
343
|
+
}, { immediate: false });
|
|
740
344
|
```
|
|
741
345
|
|
|
742
|
-
###
|
|
346
|
+
### validateTree Typing
|
|
743
347
|
|
|
744
348
|
```typescript
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
street: { value: '', component: Input, componentProps: { label: 'Street' } },
|
|
748
|
-
city: { value: '', component: Input, componentProps: { label: 'City' } },
|
|
749
|
-
zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
|
|
750
|
-
};
|
|
349
|
+
// ❌ WRONG - implicit any
|
|
350
|
+
validateTree((ctx) => { ... });
|
|
751
351
|
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
form
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
352
|
+
// ✅ CORRECT - explicit typing
|
|
353
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
354
|
+
if (ctx.form.field1 > ctx.form.field2) {
|
|
355
|
+
return { code: 'error', message: 'Invalid' };
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
758
358
|
});
|
|
759
359
|
```
|
|
760
360
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
361
|
+
## 14. PROJECT STRUCTURE (COLOCATION)
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
src/
|
|
365
|
+
├── components/ui/ # Reusable UI components
|
|
366
|
+
│ ├── FormField.tsx
|
|
367
|
+
│ └── FormArrayManager.tsx
|
|
368
|
+
│
|
|
369
|
+
├── forms/
|
|
370
|
+
│ └── [form-name]/ # Form module
|
|
371
|
+
│ ├── type.ts # Main form type
|
|
372
|
+
│ ├── schema.ts # Main schema
|
|
373
|
+
│ ├── validators.ts # Validators
|
|
374
|
+
│ ├── behaviors.ts # Behaviors
|
|
375
|
+
│ ├── [FormName]Form.tsx # Main component
|
|
376
|
+
│ │
|
|
377
|
+
│ ├── steps/ # Multi-step wizard
|
|
378
|
+
│ │ ├── loan-info/
|
|
379
|
+
│ │ │ ├── type.ts
|
|
380
|
+
│ │ │ ├── schema.ts
|
|
381
|
+
│ │ │ ├── validators.ts
|
|
382
|
+
│ │ │ ├── behaviors.ts
|
|
383
|
+
│ │ │ └── LoanInfoForm.tsx
|
|
384
|
+
│ │ └── ...
|
|
385
|
+
│ │
|
|
386
|
+
│ └── sub-forms/ # Reusable sub-forms
|
|
387
|
+
│ ├── address/
|
|
388
|
+
│ └── personal-data/
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Key Files
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// forms/credit-application/type.ts
|
|
395
|
+
export type { LoanInfoStep } from './steps/loan-info/type';
|
|
396
|
+
export interface CreditApplicationForm {
|
|
397
|
+
loanType: LoanType;
|
|
398
|
+
loanAmount: number;
|
|
399
|
+
// ...
|
|
796
400
|
}
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
## Troubleshooting / FAQ
|
|
800
401
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
### Q: How to access nested field by path string?
|
|
808
|
-
A: Use `form.getFieldByPath('address.city')` for dynamic string-based access. For type-safe access use proxy: `form.address.city`.
|
|
809
|
-
|
|
810
|
-
### Q: TypeScript errors with schema?
|
|
811
|
-
A: Ensure your type interface matches the schema structure exactly. Use `createForm<YourType>()` for proper type inference.
|
|
812
|
-
|
|
813
|
-
### Q: How to reset form to initial values?
|
|
814
|
-
A: Call `form.reset()` for single field or `form.resetAll()` for GroupNode to reset all children.
|
|
815
|
-
|
|
816
|
-
### Q: How to get all form values?
|
|
817
|
-
A: Access `form.value.value` (it's a Signal) or use `form.getValue()` method.
|
|
818
|
-
|
|
819
|
-
### Q: How to programmatically set multiple values?
|
|
820
|
-
A: Use `form.patchValue({ field1: 'value1', field2: 'value2' })` to update multiple fields at once.
|
|
821
|
-
|
|
822
|
-
### Q: Form instance recreated on every render?
|
|
823
|
-
A: Wrap `createForm()` in `useMemo()`:
|
|
824
|
-
```typescript
|
|
825
|
-
const form = useMemo(() => createForm<MyForm>({ form: schema }), []);
|
|
826
|
-
```
|
|
402
|
+
// forms/credit-application/schema.ts
|
|
403
|
+
import { loanInfoSchema } from './steps/loan-info/schema';
|
|
404
|
+
export const creditApplicationSchema = {
|
|
405
|
+
...loanInfoSchema,
|
|
406
|
+
monthlyPayment: { value: 0, disabled: true },
|
|
407
|
+
};
|
|
827
408
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
form.markAsTouched(); // Show all errors
|
|
834
|
-
await form.validate(); // Run all validators
|
|
835
|
-
|
|
836
|
-
if (form.valid.value) {
|
|
837
|
-
const data = form.value.value;
|
|
838
|
-
await submitToServer(data);
|
|
839
|
-
}
|
|
409
|
+
// forms/credit-application/validators.ts
|
|
410
|
+
import { loanValidation } from './steps/loan-info/validators';
|
|
411
|
+
export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
|
|
412
|
+
loanValidation(path);
|
|
413
|
+
// Cross-step validation...
|
|
840
414
|
};
|
|
841
415
|
```
|
|
842
416
|
|
|
843
|
-
|
|
417
|
+
### Scaling
|
|
844
418
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
419
|
+
| Complexity | Structure |
|
|
420
|
+
|------------|-----------|
|
|
421
|
+
| Simple | Single file: `ContactForm.tsx` |
|
|
422
|
+
| Medium | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
|
|
423
|
+
| Complex | Full colocation with `steps/` and `sub-forms/` |
|