@reformer/core 1.1.0 → 2.0.0-beta.3
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/behaviors-DzYL8kY_.js +499 -0
- package/dist/behaviors.d.ts +6 -2
- package/dist/behaviors.js +19 -227
- package/dist/core/behavior/behavior-context.d.ts +6 -2
- package/dist/core/behavior/create-field-path.d.ts +3 -16
- package/dist/core/nodes/group-node.d.ts +14 -193
- package/dist/core/types/form-context.d.ts +10 -4
- package/dist/core/utils/field-path.d.ts +48 -0
- package/dist/core/utils/index.d.ts +1 -0
- package/dist/core/validation/core/validate-tree.d.ts +10 -4
- package/dist/core/validation/field-path.d.ts +3 -39
- package/dist/core/validation/validation-context.d.ts +23 -0
- package/dist/hooks/types.d.ts +328 -0
- package/dist/hooks/useFormControl.d.ts +13 -37
- package/dist/hooks/useFormControlValue.d.ts +167 -0
- package/dist/hooks/useSignalSubscription.d.ts +17 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2886 -8
- package/dist/{create-field-path-CdPF3lIK.js → registry-helpers-BRxAr6nG.js} +133 -347
- package/dist/validators-gXoHPdqM.js +418 -0
- package/dist/validators.d.ts +6 -2
- package/dist/validators.js +29 -296
- package/llms.txt +1283 -22
- package/package.json +8 -4
- package/dist/core/behavior/behavior-applicator.d.ts +0 -71
- package/dist/core/behavior/behavior-applicator.js +0 -92
- package/dist/core/behavior/behavior-context.js +0 -38
- package/dist/core/behavior/behavior-registry.js +0 -198
- package/dist/core/behavior/behaviors/compute-from.js +0 -84
- package/dist/core/behavior/behaviors/copy-from.js +0 -64
- package/dist/core/behavior/behaviors/enable-when.js +0 -81
- package/dist/core/behavior/behaviors/index.js +0 -11
- package/dist/core/behavior/behaviors/reset-when.js +0 -63
- package/dist/core/behavior/behaviors/revalidate-when.js +0 -51
- package/dist/core/behavior/behaviors/sync-fields.js +0 -66
- package/dist/core/behavior/behaviors/transform-value.js +0 -110
- package/dist/core/behavior/behaviors/watch-field.js +0 -56
- package/dist/core/behavior/compose-behavior.js +0 -166
- package/dist/core/behavior/create-field-path.js +0 -69
- package/dist/core/behavior/index.js +0 -17
- package/dist/core/behavior/types.js +0 -7
- package/dist/core/context/form-context-impl.js +0 -37
- package/dist/core/factories/index.js +0 -6
- package/dist/core/factories/node-factory.js +0 -281
- package/dist/core/nodes/array-node.js +0 -534
- package/dist/core/nodes/field-node.js +0 -510
- package/dist/core/nodes/form-node.js +0 -343
- package/dist/core/nodes/group-node/field-registry.d.ts +0 -191
- package/dist/core/nodes/group-node/field-registry.js +0 -215
- package/dist/core/nodes/group-node/index.d.ts +0 -11
- package/dist/core/nodes/group-node/index.js +0 -11
- package/dist/core/nodes/group-node/proxy-builder.d.ts +0 -71
- package/dist/core/nodes/group-node/proxy-builder.js +0 -161
- package/dist/core/nodes/group-node/state-manager.d.ts +0 -184
- package/dist/core/nodes/group-node/state-manager.js +0 -265
- package/dist/core/nodes/group-node.js +0 -770
- package/dist/core/types/deep-schema.js +0 -11
- package/dist/core/types/field-path.js +0 -4
- package/dist/core/types/form-context.js +0 -25
- package/dist/core/types/group-node-proxy.js +0 -31
- package/dist/core/types/index.js +0 -4
- package/dist/core/types/validation-schema.js +0 -10
- package/dist/core/utils/create-form.js +0 -24
- package/dist/core/utils/debounce.js +0 -197
- package/dist/core/utils/error-handler.js +0 -226
- package/dist/core/utils/field-path-navigator.js +0 -374
- package/dist/core/utils/index.js +0 -14
- package/dist/core/utils/registry-helpers.js +0 -79
- package/dist/core/utils/registry-stack.js +0 -86
- package/dist/core/utils/resources.js +0 -69
- package/dist/core/utils/subscription-manager.js +0 -214
- package/dist/core/utils/type-guards.js +0 -169
- package/dist/core/validation/core/apply-when.js +0 -41
- package/dist/core/validation/core/apply.js +0 -38
- package/dist/core/validation/core/index.js +0 -8
- package/dist/core/validation/core/validate-async.js +0 -45
- package/dist/core/validation/core/validate-tree.js +0 -37
- package/dist/core/validation/core/validate.js +0 -38
- package/dist/core/validation/field-path.js +0 -147
- package/dist/core/validation/index.js +0 -33
- package/dist/core/validation/validate-form.js +0 -152
- package/dist/core/validation/validation-applicator.js +0 -217
- package/dist/core/validation/validation-context.js +0 -75
- package/dist/core/validation/validation-registry.js +0 -298
- package/dist/core/validation/validators/array-validators.js +0 -86
- package/dist/core/validation/validators/date.js +0 -117
- package/dist/core/validation/validators/email.js +0 -60
- package/dist/core/validation/validators/index.js +0 -14
- package/dist/core/validation/validators/max-length.js +0 -60
- package/dist/core/validation/validators/max.js +0 -60
- package/dist/core/validation/validators/min-length.js +0 -60
- package/dist/core/validation/validators/min.js +0 -60
- package/dist/core/validation/validators/number.js +0 -90
- package/dist/core/validation/validators/pattern.js +0 -62
- package/dist/core/validation/validators/phone.js +0 -58
- package/dist/core/validation/validators/required.js +0 -69
- package/dist/core/validation/validators/url.js +0 -55
- package/dist/hooks/useFormControl.js +0 -298
- package/dist/node-factory-D7DOnSSN.js +0 -3200
package/llms.txt
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
# ReFormer - LLM Integration Guide
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1. API Reference
|
|
4
4
|
|
|
5
5
|
### Imports (CRITICALLY IMPORTANT)
|
|
6
6
|
|
|
7
7
|
| What | Where |
|
|
8
8
|
| ------------------------------------------------------------------------------------------- | --------------------------- |
|
|
9
|
+
| `createForm`, `useFormControl`, `useFormControlValue`, `validateForm` | `@reformer/core` |
|
|
9
10
|
| `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `GroupNodeWithControls`, `FieldNode` | `@reformer/core` |
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
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` |
|
|
12
19
|
|
|
13
20
|
### Type Values
|
|
14
21
|
|
|
@@ -16,30 +23,205 @@
|
|
|
16
23
|
- Optional strings: `string` (empty string by default)
|
|
17
24
|
- Do NOT add `[key: string]: unknown` to form interfaces
|
|
18
25
|
|
|
26
|
+
### React Hooks Comparison (CRITICALLY IMPORTANT)
|
|
27
|
+
|
|
28
|
+
| Hook | Return Type | Subscribes To | Use Case |
|
|
29
|
+
|------|-------------|---------------|----------|
|
|
30
|
+
| `useFormControl(field)` | `{ value, errors, disabled, touched, ... }` | All signals | Full field state, form inputs |
|
|
31
|
+
| `useFormControlValue(field)` | `T` (value directly) | Only value signal | Conditional rendering |
|
|
32
|
+
|
|
33
|
+
**CRITICAL**: Do NOT destructure `useFormControlValue`! It returns `T` directly, NOT `{ value: T }`.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// WRONG - will always be undefined!
|
|
37
|
+
const { value: loanType } = useFormControlValue(control.loanType);
|
|
38
|
+
|
|
39
|
+
// CORRECT
|
|
40
|
+
const loanType = useFormControlValue(control.loanType);
|
|
41
|
+
|
|
42
|
+
// CORRECT - useFormControl returns object, destructuring OK
|
|
43
|
+
const { value, errors, disabled } = useFormControl(control.loanType);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 1.5 QUICK START - Minimal Working Form
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { createForm, useFormControl } from '@reformer/core';
|
|
50
|
+
import { required, email } from '@reformer/core/validators';
|
|
51
|
+
import type { GroupNodeWithControls } from '@reformer/core';
|
|
52
|
+
|
|
53
|
+
// 1. Define form type
|
|
54
|
+
interface ContactForm {
|
|
55
|
+
name: string;
|
|
56
|
+
email: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Create form schema with validation
|
|
60
|
+
const form = createForm<ContactForm>({
|
|
61
|
+
form: {
|
|
62
|
+
name: { value: '', component: Input },
|
|
63
|
+
email: { value: '', component: Input },
|
|
64
|
+
},
|
|
65
|
+
validation: (path) => {
|
|
66
|
+
required(path.name, { message: 'Name is required' });
|
|
67
|
+
required(path.email, { message: 'Email is required' });
|
|
68
|
+
email(path.email, { message: 'Invalid email format' });
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 3. Use in React component
|
|
73
|
+
function ContactFormComponent() {
|
|
74
|
+
const nameCtrl = useFormControl(form.name);
|
|
75
|
+
const emailCtrl = useFormControl(form.email);
|
|
76
|
+
|
|
77
|
+
const handleSubmit = async () => {
|
|
78
|
+
await form.submit((values) => {
|
|
79
|
+
console.log('Form submitted:', values);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
|
85
|
+
<div>
|
|
86
|
+
<input
|
|
87
|
+
value={nameCtrl.value}
|
|
88
|
+
onChange={(e) => form.name.setValue(e.target.value)}
|
|
89
|
+
disabled={nameCtrl.disabled}
|
|
90
|
+
/>
|
|
91
|
+
{nameCtrl.errors.map((err) => <span key={err.code}>{err.message}</span>)}
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<input
|
|
95
|
+
value={emailCtrl.value}
|
|
96
|
+
onChange={(e) => form.email.setValue(e.target.value)}
|
|
97
|
+
disabled={emailCtrl.disabled}
|
|
98
|
+
/>
|
|
99
|
+
{emailCtrl.errors.map((err) => <span key={err.code}>{err.message}</span>)}
|
|
100
|
+
</div>
|
|
101
|
+
<button type="submit">Send</button>
|
|
102
|
+
</form>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 4. Pass form to child components via props (NOT context!)
|
|
107
|
+
interface FormStepProps {
|
|
108
|
+
form: GroupNodeWithControls<ContactForm>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function FormStep({ form }: FormStepProps) {
|
|
112
|
+
// Access form fields directly
|
|
113
|
+
const { value } = useFormControl(form.name);
|
|
114
|
+
return <div>Name: {value}</div>;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
19
118
|
## 2. API SIGNATURES
|
|
20
119
|
|
|
21
120
|
### Validators
|
|
22
121
|
|
|
23
122
|
```typescript
|
|
123
|
+
// Basic validators
|
|
24
124
|
required(path, options?: { message?: string })
|
|
25
125
|
min(path, value: number, options?: { message?: string })
|
|
26
126
|
max(path, value: number, options?: { message?: string })
|
|
27
127
|
minLength(path, length: number, options?: { message?: string })
|
|
28
128
|
maxLength(path, length: number, options?: { message?: string })
|
|
29
129
|
email(path, options?: { message?: string })
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
130
|
+
|
|
131
|
+
// Additional validators
|
|
132
|
+
pattern(path, regex: RegExp, options?: { message?: string })
|
|
133
|
+
url(path, options?: { message?: string })
|
|
134
|
+
phone(path, options?: { message?: string; format?: PhoneFormat })
|
|
135
|
+
number(path, options?: { message?: string })
|
|
136
|
+
date(path, options?: { message?: string; minAge?: number; maxAge?: number; noFuture?: boolean; noPast?: boolean })
|
|
137
|
+
|
|
138
|
+
// Custom validators
|
|
139
|
+
validate(path, validator: (value, ctx) => ValidationError | null)
|
|
140
|
+
validateAsync(path, validator: async (value, ctx) => ValidationError | null)
|
|
141
|
+
validateTree(validator: (ctx) => ValidationError | null, options?: { targetField?: string })
|
|
142
|
+
|
|
143
|
+
// Conditional validation (3 arguments!)
|
|
144
|
+
applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
|
|
145
|
+
|
|
146
|
+
// applyWhen Examples - CRITICALLY IMPORTANT
|
|
147
|
+
// applyWhen takes 3 arguments: triggerField, condition, validators
|
|
148
|
+
|
|
149
|
+
// Example 1: Simple condition
|
|
150
|
+
applyWhen(
|
|
151
|
+
path.loanType, // 1st: field to watch
|
|
152
|
+
(type) => type === 'mortgage', // 2nd: condition on field value
|
|
153
|
+
(p) => { // 3rd: validators to apply
|
|
154
|
+
required(p.propertyValue);
|
|
155
|
+
min(p.propertyValue, 100000);
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Example 2: Nested field as trigger
|
|
160
|
+
applyWhen(
|
|
161
|
+
path.address.country,
|
|
162
|
+
(country) => country === 'US',
|
|
163
|
+
(p) => {
|
|
164
|
+
required(p.address.state);
|
|
165
|
+
pattern(p.address.zip, /^\d{5}(-\d{4})?$/);
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Example 3: Boolean trigger
|
|
170
|
+
applyWhen(
|
|
171
|
+
path.hasInsurance,
|
|
172
|
+
(has) => has === true,
|
|
173
|
+
(p) => {
|
|
174
|
+
required(p.insuranceCompany);
|
|
175
|
+
required(p.policyNumber);
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// WRONG - only 2 arguments (React Hook Form pattern)
|
|
180
|
+
applyWhen(
|
|
181
|
+
(form) => form.loanType === 'mortgage', // WRONG!
|
|
182
|
+
() => { required(path.propertyValue); }
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Array validators
|
|
186
|
+
notEmpty(path, options?: { message?: string })
|
|
187
|
+
validateItems(arrayPath, itemValidatorsFn: (itemPath) => void)
|
|
33
188
|
```
|
|
34
189
|
|
|
35
190
|
### Behaviors
|
|
36
191
|
|
|
37
192
|
```typescript
|
|
193
|
+
// Enable/disable fields conditionally
|
|
38
194
|
enableWhen(path, condition: (form) => boolean, options?: { resetOnDisable?: boolean })
|
|
39
195
|
disableWhen(path, condition: (form) => boolean)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
196
|
+
|
|
197
|
+
// Computed fields (same nesting level)
|
|
198
|
+
computeFrom(sourcePaths[], targetPath, compute: (values) => result, options?: { debounce?: number; condition?: (form) => boolean })
|
|
199
|
+
|
|
200
|
+
// Watch field changes (ALWAYS use { immediate: false } to prevent cycle detection!)
|
|
201
|
+
watchField(path, callback: (value, ctx: BehaviorContext) => void, options: { immediate: false; debounce?: number })
|
|
202
|
+
|
|
203
|
+
// Copy values between fields
|
|
204
|
+
copyFrom(sourcePath, targetPath, options?: { when?: (form) => boolean; fields?: string[]; transform?: (value) => value })
|
|
205
|
+
|
|
206
|
+
// Reset field when condition met
|
|
207
|
+
resetWhen(path, condition: (form) => boolean, options?: { toValue?: any })
|
|
208
|
+
|
|
209
|
+
// Re-validate target field when any trigger changes
|
|
210
|
+
revalidateWhen(targetPath, triggerPaths[], options?: { debounce?: number })
|
|
211
|
+
|
|
212
|
+
// Sync multiple fields
|
|
213
|
+
syncFields(paths[], options?: { bidirectional?: boolean })
|
|
214
|
+
|
|
215
|
+
// Transform values
|
|
216
|
+
transformValue(path, transformer: (value) => value, options?: { on?: 'change' | 'blur' })
|
|
217
|
+
transformers.trim, transformers.toUpperCase, transformers.toLowerCase, transformers.toNumber
|
|
218
|
+
|
|
219
|
+
// BehaviorContext interface:
|
|
220
|
+
interface BehaviorContext<TForm> {
|
|
221
|
+
form: GroupNodeWithControls<TForm>; // Form proxy with typed field access
|
|
222
|
+
setFieldValue: (path: string, value: any) => void;
|
|
223
|
+
// To READ field values, use: ctx.form.fieldName.value.value
|
|
224
|
+
}
|
|
43
225
|
```
|
|
44
226
|
|
|
45
227
|
## 3. COMMON PATTERNS
|
|
@@ -59,7 +241,7 @@ enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
|
|
|
59
241
|
// Use watchField instead:
|
|
60
242
|
watchField(path.nested.field, (value, ctx) => {
|
|
61
243
|
ctx.setFieldValue('rootField', computedValue);
|
|
62
|
-
});
|
|
244
|
+
}, { immediate: false });
|
|
63
245
|
```
|
|
64
246
|
|
|
65
247
|
### Type-Safe useFormControl
|
|
@@ -68,26 +250,84 @@ watchField(path.nested.field, (value, ctx) => {
|
|
|
68
250
|
const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
|
|
69
251
|
```
|
|
70
252
|
|
|
71
|
-
|
|
253
|
+
### Validation Priority (IMPORTANT)
|
|
254
|
+
|
|
255
|
+
**Always prefer built-in validators over custom ones:**
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// 1. BEST: Use built-in validators when available
|
|
259
|
+
required(path.email);
|
|
260
|
+
email(path.email);
|
|
261
|
+
min(path.age, 18);
|
|
262
|
+
minLength(path.password, 8);
|
|
263
|
+
pattern(path.phone, /^\+7\d{10}$/);
|
|
264
|
+
|
|
265
|
+
// 2. GOOD: Use validate() only when no built-in validator exists
|
|
266
|
+
validate(path.customField, (value, ctx) => {
|
|
267
|
+
// Custom logic that can't be expressed with built-in validators
|
|
268
|
+
if (customCondition(value)) {
|
|
269
|
+
return { code: 'custom', message: 'Custom error' };
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// 3. WRONG: Don't recreate built-in validators
|
|
275
|
+
validate(path.email, (value) => {
|
|
276
|
+
if (!value) return { code: 'required', message: 'Required' }; // Use required() instead!
|
|
277
|
+
if (!value.includes('@')) return { code: 'email', message: 'Invalid' }; // Use email() instead!
|
|
278
|
+
return null;
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## 4. COMMON MISTAKES
|
|
283
|
+
|
|
284
|
+
### useFormControlValue (CRITICAL)
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// WRONG - useFormControlValue returns T directly, NOT { value: T }
|
|
288
|
+
const { value: loanType } = useFormControlValue(control.loanType);
|
|
289
|
+
// Result: loanType is ALWAYS undefined! Conditional rendering will fail.
|
|
290
|
+
|
|
291
|
+
// CORRECT
|
|
292
|
+
const loanType = useFormControlValue(control.loanType);
|
|
293
|
+
|
|
294
|
+
// ALSO CORRECT - useFormControl returns object
|
|
295
|
+
const { value, errors } = useFormControl(control.loanType);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Reading Field Values in BehaviorContext (CRITICAL)
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// WRONG - getFieldValue does NOT exist!
|
|
302
|
+
watchField(path.amount, (amount, ctx) => {
|
|
303
|
+
const rate = ctx.getFieldValue('rate'); // ERROR: Property 'getFieldValue' does not exist
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// CORRECT - use ctx.form.fieldName.value.value
|
|
307
|
+
watchField(path.amount, (amount, ctx) => {
|
|
308
|
+
const rate = ctx.form.rate.value.value; // Read via signal
|
|
309
|
+
ctx.setFieldValue('total', amount * rate);
|
|
310
|
+
});
|
|
311
|
+
```
|
|
72
312
|
|
|
73
313
|
### Validators
|
|
74
314
|
|
|
75
315
|
```typescript
|
|
76
|
-
//
|
|
316
|
+
// WRONG
|
|
77
317
|
required(path.email, 'Email is required');
|
|
78
318
|
|
|
79
|
-
//
|
|
319
|
+
// CORRECT
|
|
80
320
|
required(path.email, { message: 'Email is required' });
|
|
81
321
|
```
|
|
82
322
|
|
|
83
323
|
### Types
|
|
84
324
|
|
|
85
325
|
```typescript
|
|
86
|
-
//
|
|
326
|
+
// WRONG
|
|
87
327
|
amount: number | null;
|
|
88
328
|
[key: string]: unknown;
|
|
89
329
|
|
|
90
|
-
//
|
|
330
|
+
// CORRECT
|
|
91
331
|
amount: number | undefined;
|
|
92
332
|
// No index signature
|
|
93
333
|
```
|
|
@@ -95,10 +335,10 @@ amount: number | undefined;
|
|
|
95
335
|
### computeFrom
|
|
96
336
|
|
|
97
337
|
```typescript
|
|
98
|
-
//
|
|
338
|
+
// WRONG - different nesting levels
|
|
99
339
|
computeFrom([path.nested.a, path.nested.b], path.root, ...)
|
|
100
340
|
|
|
101
|
-
//
|
|
341
|
+
// CORRECT - use watchField
|
|
102
342
|
watchField(path.nested.a, (_, ctx) => {
|
|
103
343
|
ctx.setFieldValue('root', computed);
|
|
104
344
|
});
|
|
@@ -107,10 +347,10 @@ watchField(path.nested.a, (_, ctx) => {
|
|
|
107
347
|
### Imports
|
|
108
348
|
|
|
109
349
|
```typescript
|
|
110
|
-
//
|
|
350
|
+
// WRONG - types are not in submodules
|
|
111
351
|
import { ValidationSchemaFn } from '@reformer/core/validators';
|
|
112
352
|
|
|
113
|
-
//
|
|
353
|
+
// CORRECT - types from main module
|
|
114
354
|
import type { ValidationSchemaFn } from '@reformer/core';
|
|
115
355
|
import { required, email } from '@reformer/core/validators';
|
|
116
356
|
```
|
|
@@ -124,6 +364,7 @@ import { required, email } from '@reformer/core/validators';
|
|
|
124
364
|
| `FormFields[]` instead of concrete type | Type inference issue | Use `as FieldNode<T>` |
|
|
125
365
|
| `Type 'X' is missing properties from type 'Y'` | Cross-level computeFrom | Use watchField instead |
|
|
126
366
|
| `Module has no exported member` | Wrong import source | Types from core, functions from submodules |
|
|
367
|
+
| `Cycle detected` | Multiple watchers on same field calling disable/setValue | See 22-cycle-detection.md |
|
|
127
368
|
|
|
128
369
|
## 6. COMPLETE IMPORT EXAMPLE
|
|
129
370
|
|
|
@@ -141,7 +382,7 @@ import type {
|
|
|
141
382
|
import { createForm, useFormControl } from '@reformer/core';
|
|
142
383
|
|
|
143
384
|
// Validators - from /validators submodule
|
|
144
|
-
import { required, min, max, email, validate,
|
|
385
|
+
import { required, min, max, email, validate, applyWhen } from '@reformer/core/validators';
|
|
145
386
|
|
|
146
387
|
// Behaviors - from /behaviors submodule
|
|
147
388
|
import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
|
|
@@ -150,7 +391,7 @@ import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/be
|
|
|
150
391
|
## 7. FORM TYPE DEFINITION
|
|
151
392
|
|
|
152
393
|
```typescript
|
|
153
|
-
//
|
|
394
|
+
// CORRECT form type definition
|
|
154
395
|
interface MyForm {
|
|
155
396
|
// Required fields
|
|
156
397
|
name: string;
|
|
@@ -169,10 +410,1030 @@ interface MyForm {
|
|
|
169
410
|
city: string;
|
|
170
411
|
};
|
|
171
412
|
|
|
172
|
-
// Arrays
|
|
413
|
+
// Arrays - use tuple format for schema
|
|
173
414
|
items: Array<{
|
|
174
415
|
id: string;
|
|
175
416
|
name: string;
|
|
176
417
|
}>;
|
|
177
418
|
}
|
|
178
419
|
```
|
|
420
|
+
|
|
421
|
+
## 8. FORMSCHEMA FORMAT (CRITICALLY IMPORTANT)
|
|
422
|
+
|
|
423
|
+
**Every field MUST have `value` and `component` properties!**
|
|
424
|
+
|
|
425
|
+
### FieldConfig Interface
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
interface FieldConfig<T> {
|
|
429
|
+
value: T | null; // Initial value (REQUIRED)
|
|
430
|
+
component: ComponentType; // React component (REQUIRED)
|
|
431
|
+
componentProps?: object; // Props passed to component
|
|
432
|
+
disabled?: boolean; // Disable field initially
|
|
433
|
+
validators?: ValidatorFn[]; // Sync validators
|
|
434
|
+
asyncValidators?: AsyncValidatorFn[]; // Async validators
|
|
435
|
+
updateOn?: 'change' | 'blur' | 'submit';
|
|
436
|
+
debounce?: number;
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Primitive Fields
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import { Input, Select, Checkbox } from '@/components/ui';
|
|
444
|
+
|
|
445
|
+
const schema: FormSchema<MyForm> = {
|
|
446
|
+
// String field
|
|
447
|
+
name: {
|
|
448
|
+
value: '', // Initial value (REQUIRED)
|
|
449
|
+
component: Input, // React component (REQUIRED)
|
|
450
|
+
componentProps: {
|
|
451
|
+
label: 'Name',
|
|
452
|
+
placeholder: 'Enter name',
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
// Number field (optional)
|
|
457
|
+
age: {
|
|
458
|
+
value: undefined, // Use undefined, NOT null
|
|
459
|
+
component: Input,
|
|
460
|
+
componentProps: { type: 'number', label: 'Age' },
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
// Boolean field
|
|
464
|
+
agree: {
|
|
465
|
+
value: false,
|
|
466
|
+
component: Checkbox,
|
|
467
|
+
componentProps: { label: 'I agree to terms' },
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
// Enum/Select field
|
|
471
|
+
status: {
|
|
472
|
+
value: 'active',
|
|
473
|
+
component: Select,
|
|
474
|
+
componentProps: {
|
|
475
|
+
label: 'Status',
|
|
476
|
+
options: [
|
|
477
|
+
{ value: 'active', label: 'Active' },
|
|
478
|
+
{ value: 'inactive', label: 'Inactive' },
|
|
479
|
+
],
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Nested Objects
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
const schema: FormSchema<MyForm> = {
|
|
489
|
+
address: {
|
|
490
|
+
street: { value: '', component: Input, componentProps: { label: 'Street' } },
|
|
491
|
+
city: { value: '', component: Input, componentProps: { label: 'City' } },
|
|
492
|
+
zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Arrays (Tuple Format)
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
const itemSchema = {
|
|
501
|
+
id: { value: '', component: Input, componentProps: { label: 'ID' } },
|
|
502
|
+
name: { value: '', component: Input, componentProps: { label: 'Name' } },
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const schema: FormSchema<MyForm> = {
|
|
506
|
+
items: [itemSchema], // Array with ONE template item
|
|
507
|
+
};
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### WRONG - This will NOT compile
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
// Missing value and component - TypeScript will error!
|
|
514
|
+
const schema = {
|
|
515
|
+
name: '', // Wrong
|
|
516
|
+
email: '', // Wrong
|
|
517
|
+
};
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### createForm API
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
// Full config with behavior and validation
|
|
524
|
+
const form = createForm<MyForm>({
|
|
525
|
+
form: formSchema, // Required: form schema with FieldConfig
|
|
526
|
+
behavior: behaviorSchema, // Optional: behavior rules
|
|
527
|
+
validation: validationSchema, // Optional: validation rules
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Access form controls
|
|
531
|
+
form.name.setValue('John');
|
|
532
|
+
form.address.city.value.value; // Get current value
|
|
533
|
+
form.items.push({ id: '1', name: 'Item' }); // Array operations
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### createForm Returns a Proxy
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// createForm() returns GroupNodeWithControls<T> (a Proxy wrapper around GroupNode)
|
|
540
|
+
// This enables type-safe field access:
|
|
541
|
+
const form = createForm<MyForm>({...});
|
|
542
|
+
|
|
543
|
+
form.email // FieldNode<string> - TypeScript knows the type!
|
|
544
|
+
form.address.city // FieldNode<string> - nested access works
|
|
545
|
+
form.items.at(0) // GroupNodeWithControls<ItemType> - array items
|
|
546
|
+
|
|
547
|
+
// IMPORTANT: Proxy doesn't pass instanceof checks!
|
|
548
|
+
// Use type guards instead:
|
|
549
|
+
import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
|
|
550
|
+
|
|
551
|
+
if (isFieldNode(node)) { /* ... */ } // Works with Proxy
|
|
552
|
+
if (node instanceof FieldNode) { /* ... */ } // Fails with Proxy!
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## 9. ARRAY SCHEMA FORMAT
|
|
556
|
+
|
|
557
|
+
**Array items are sub-forms!** Each array element is a complete sub-form with its own fields, validation, and behavior.
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// CORRECT - use tuple format for arrays
|
|
561
|
+
// The template item defines the sub-form schema for each array element
|
|
562
|
+
const itemSchema = {
|
|
563
|
+
id: { value: '', component: Input },
|
|
564
|
+
name: { value: '', component: Input },
|
|
565
|
+
price: { value: 0, component: Input, componentProps: { type: 'number' } },
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const schema: FormSchema<MyForm> = {
|
|
569
|
+
items: [itemSchema], // Array of sub-forms
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// Each array item is a GroupNode (sub-form) with its own controls:
|
|
573
|
+
form.items.map((item) => {
|
|
574
|
+
// item is a sub-form (GroupNode) - access fields like nested form
|
|
575
|
+
item.name.setValue('New Name');
|
|
576
|
+
item.price.value.value; // Get current value
|
|
577
|
+
});
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
// WRONG - object format is NOT supported
|
|
582
|
+
const schema = {
|
|
583
|
+
items: { schema: itemSchema, initialItems: [] }, // This will NOT work
|
|
584
|
+
};
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Array Item as Sub-Form
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
// Validation for array items (each item is a sub-form)
|
|
591
|
+
validateItems(path.items, (itemPath) => {
|
|
592
|
+
// itemPath provides paths to sub-form fields
|
|
593
|
+
required(itemPath.name);
|
|
594
|
+
min(itemPath.price, 0);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Render array items - each item is a sub-form
|
|
598
|
+
{form.items.map((item, index) => (
|
|
599
|
+
<div key={item.id}>
|
|
600
|
+
{/* item is a sub-form - use FormField for each field */}
|
|
601
|
+
<FormField control={item.name} />
|
|
602
|
+
<FormField control={item.price} />
|
|
603
|
+
<button onClick={() => form.items.removeAt(index)}>Remove</button>
|
|
604
|
+
</div>
|
|
605
|
+
))}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
// CORRECT - async watchField with ALL safeguards
|
|
612
|
+
watchField(
|
|
613
|
+
path.parentField,
|
|
614
|
+
async (value, ctx) => {
|
|
615
|
+
if (!value) return; // Guard clause
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
const { data } = await fetchData(value);
|
|
619
|
+
ctx.form.dependentField.updateComponentProps({ options: data });
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.error('Failed:', error);
|
|
622
|
+
ctx.form.dependentField.updateComponentProps({ options: [] });
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
{ immediate: false, debounce: 300 } // REQUIRED options
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// WRONG - missing safeguards
|
|
629
|
+
watchField(path.field, async (value, ctx) => {
|
|
630
|
+
const { data } = await fetchData(value); // Will fail silently!
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### Required Options for async watchField:
|
|
635
|
+
- `immediate: false` - prevents execution during initialization
|
|
636
|
+
- `debounce: 300` - prevents excessive API calls (300-500ms recommended)
|
|
637
|
+
- Guard clause - skip if value is empty
|
|
638
|
+
- try-catch - handle errors explicitly
|
|
639
|
+
|
|
640
|
+
## 11. ARRAY CLEANUP PATTERN
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
// CORRECT - cleanup array when checkbox unchecked
|
|
644
|
+
watchField(
|
|
645
|
+
path.hasItems,
|
|
646
|
+
(hasItems, ctx) => {
|
|
647
|
+
if (!hasItems && ctx.form.items) {
|
|
648
|
+
ctx.form.items.clear();
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
{ immediate: false }
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// WRONG - no immediate: false, no null check
|
|
655
|
+
watchField(path.hasItems, (hasItems, ctx) => {
|
|
656
|
+
if (!hasItems) ctx.form.items.clear(); // May crash on init!
|
|
657
|
+
});
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## 12. MULTI-STEP FORM VALIDATION
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
// Step-specific validation schemas
|
|
664
|
+
const step1Validation: ValidationSchemaFn<Form> = (path) => {
|
|
665
|
+
required(path.loanType);
|
|
666
|
+
required(path.loanAmount);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const step2Validation: ValidationSchemaFn<Form> = (path) => {
|
|
670
|
+
required(path.personalData.firstName);
|
|
671
|
+
required(path.personalData.lastName);
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// STEP_VALIDATIONS map for useStepForm hook
|
|
675
|
+
export const STEP_VALIDATIONS = {
|
|
676
|
+
1: step1Validation,
|
|
677
|
+
2: step2Validation,
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// Full validation (combines all steps)
|
|
681
|
+
export const fullValidation: ValidationSchemaFn<Form> = (path) => {
|
|
682
|
+
step1Validation(path);
|
|
683
|
+
step2Validation(path);
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// Using validateForm() for step validation
|
|
687
|
+
import { validateForm } from '@reformer/core';
|
|
688
|
+
|
|
689
|
+
const goToNextStep = async () => {
|
|
690
|
+
const currentValidation = STEP_VALIDATIONS[currentStep];
|
|
691
|
+
const isValid = await validateForm(form, currentValidation);
|
|
692
|
+
|
|
693
|
+
if (!isValid) {
|
|
694
|
+
form.markAsTouched(); // Show errors on current step fields
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
setCurrentStep(currentStep + 1);
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// Full form submit with all validations
|
|
702
|
+
const handleSubmit = async () => {
|
|
703
|
+
const isValid = await validateForm(form, fullValidation);
|
|
704
|
+
|
|
705
|
+
if (isValid) {
|
|
706
|
+
await form.submit(onSubmit);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Multi-Step Component Example
|
|
712
|
+
|
|
713
|
+
```tsx
|
|
714
|
+
function MultiStepForm() {
|
|
715
|
+
const [step, setStep] = useState(1);
|
|
716
|
+
|
|
717
|
+
const nextStep = async () => {
|
|
718
|
+
const validation = STEP_VALIDATIONS[step];
|
|
719
|
+
if (await validateForm(form, validation)) {
|
|
720
|
+
setStep(step + 1);
|
|
721
|
+
} else {
|
|
722
|
+
form.markAsTouched();
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
return (
|
|
727
|
+
<div>
|
|
728
|
+
{step === 1 && <Step1Fields form={form} />}
|
|
729
|
+
{step === 2 && <Step2Fields form={form} />}
|
|
730
|
+
|
|
731
|
+
<button onClick={() => setStep(step - 1)} disabled={step === 1}>
|
|
732
|
+
Back
|
|
733
|
+
</button>
|
|
734
|
+
<button onClick={step === 2 ? handleSubmit : nextStep}>
|
|
735
|
+
{step === 2 ? 'Submit' : 'Next'}
|
|
736
|
+
</button>
|
|
737
|
+
</div>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
## 13. EXTENDED COMMON MISTAKES
|
|
743
|
+
|
|
744
|
+
### Behavior Composition (Cycle Error)
|
|
745
|
+
|
|
746
|
+
```typescript
|
|
747
|
+
// WRONG - apply() in behavior causes "Cycle detected"
|
|
748
|
+
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
749
|
+
apply(addressBehavior, path.address); // WILL FAIL!
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// CORRECT - inline or use setup function
|
|
753
|
+
const setupAddressBehavior = (path: FieldPath<Address>) => {
|
|
754
|
+
watchField(path.region, async (region, ctx) => {
|
|
755
|
+
// ...
|
|
756
|
+
}, { immediate: false });
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
760
|
+
setupAddressBehavior(path.address); // Works!
|
|
761
|
+
};
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### Infinite Loop in watchField
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
// WRONG - causes infinite loop
|
|
768
|
+
watchField(path.field, (value, ctx) => {
|
|
769
|
+
ctx.form.field.setValue(value.toUpperCase()); // Loop!
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// CORRECT - write to different field OR add guard
|
|
773
|
+
watchField(path.input, (value, ctx) => {
|
|
774
|
+
const upper = value?.toUpperCase() || '';
|
|
775
|
+
if (ctx.form.display.value.value !== upper) {
|
|
776
|
+
ctx.form.display.setValue(upper);
|
|
777
|
+
}
|
|
778
|
+
}, { immediate: false });
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Multiple Watchers on Same Field (Cycle Error)
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
// WRONG - multiple watchers on insuranceType + missing { immediate: false }
|
|
785
|
+
watchField(path.insuranceType, (_, ctx) => {
|
|
786
|
+
ctx.form.vehicle.vin.disable();
|
|
787
|
+
ctx.form.vehicle.vin.setValue('');
|
|
788
|
+
}); // NO OPTIONS - BAD!
|
|
789
|
+
watchField(path.insuranceType, (_, ctx) => {
|
|
790
|
+
ctx.form.property.type.disable(); // CYCLE!
|
|
791
|
+
}); // NO OPTIONS - BAD!
|
|
792
|
+
|
|
793
|
+
// CORRECT - consolidate into ONE watcher with guards AND { immediate: false }
|
|
794
|
+
watchField(path.insuranceType, (_, ctx) => {
|
|
795
|
+
const type = ctx.form.insuranceType.value.value;
|
|
796
|
+
const isVehicle = type === 'casco';
|
|
797
|
+
|
|
798
|
+
// Guard: only disable if not already disabled
|
|
799
|
+
if (!isVehicle && !ctx.form.vehicle.vin.disabled.value) {
|
|
800
|
+
ctx.form.vehicle.vin.disable();
|
|
801
|
+
}
|
|
802
|
+
// Guard: only setValue if value differs
|
|
803
|
+
if (!isVehicle && ctx.form.vehicle.vin.getValue() !== '') {
|
|
804
|
+
ctx.form.vehicle.vin.setValue('');
|
|
805
|
+
}
|
|
806
|
+
// Arrays: compare by length, not reference
|
|
807
|
+
if (!isVehicle) {
|
|
808
|
+
const drivers = ctx.form.drivers.getValue();
|
|
809
|
+
if (Array.isArray(drivers) && drivers.length > 0) {
|
|
810
|
+
ctx.form.drivers.setValue([]);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}, { immediate: false }); // REQUIRED!
|
|
814
|
+
|
|
815
|
+
// BEST - use enableWhen instead of watchField
|
|
816
|
+
enableWhen(path.vehicle.vin, (form) => form.insuranceType === 'casco', { resetOnDisable: true });
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
See `22-cycle-detection.md` for complete pattern.
|
|
820
|
+
|
|
821
|
+
### validateTree Typing
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
// WRONG - implicit any
|
|
825
|
+
validateTree((ctx) => { ... });
|
|
826
|
+
|
|
827
|
+
// CORRECT - explicit typing
|
|
828
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
829
|
+
if (ctx.form.field1 > ctx.form.field2) {
|
|
830
|
+
return { code: 'error', message: 'Invalid' };
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
});
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
## 14. PROJECT STRUCTURE (COLOCATION)
|
|
837
|
+
|
|
838
|
+
```
|
|
839
|
+
src/
|
|
840
|
+
├── components/ui/ # Reusable UI components
|
|
841
|
+
│ ├── FormField.tsx
|
|
842
|
+
│ └── FormArrayManager.tsx
|
|
843
|
+
│
|
|
844
|
+
├── forms/
|
|
845
|
+
│ └── [form-name]/ # Form module
|
|
846
|
+
│ ├── type.ts # Main form type
|
|
847
|
+
│ ├── schema.ts # Main schema
|
|
848
|
+
│ ├── validators.ts # Validators
|
|
849
|
+
│ ├── behaviors.ts # Behaviors
|
|
850
|
+
│ ├── [FormName]Form.tsx # Main component
|
|
851
|
+
│ │
|
|
852
|
+
│ ├── steps/ # Multi-step wizard
|
|
853
|
+
│ │ ├── loan-info/
|
|
854
|
+
│ │ │ ├── type.ts
|
|
855
|
+
│ │ │ ├── schema.ts
|
|
856
|
+
│ │ │ ├── validators.ts
|
|
857
|
+
│ │ │ ├── behaviors.ts
|
|
858
|
+
│ │ │ └── LoanInfoForm.tsx
|
|
859
|
+
│ │ └── ...
|
|
860
|
+
│ │
|
|
861
|
+
│ └── sub-forms/ # Reusable sub-forms
|
|
862
|
+
│ ├── address/
|
|
863
|
+
│ └── personal-data/
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
### Key Files
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
// forms/credit-application/type.ts
|
|
870
|
+
export type { LoanInfoStep } from './steps/loan-info/type';
|
|
871
|
+
export interface CreditApplicationForm {
|
|
872
|
+
loanType: LoanType;
|
|
873
|
+
loanAmount: number;
|
|
874
|
+
// ...
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// forms/credit-application/schema.ts
|
|
878
|
+
import { loanInfoSchema } from './steps/loan-info/schema';
|
|
879
|
+
export const creditApplicationSchema = {
|
|
880
|
+
...loanInfoSchema,
|
|
881
|
+
monthlyPayment: { value: 0, disabled: true },
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
// forms/credit-application/validators.ts
|
|
885
|
+
import { loanValidation } from './steps/loan-info/validators';
|
|
886
|
+
export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
|
|
887
|
+
loanValidation(path);
|
|
888
|
+
// Cross-step validation...
|
|
889
|
+
};
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
### Scaling
|
|
893
|
+
|
|
894
|
+
| Complexity | Structure |
|
|
895
|
+
|------------|-----------|
|
|
896
|
+
| Simple | Single file: `ContactForm.tsx` |
|
|
897
|
+
| Medium | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
|
|
898
|
+
| Complex | Full colocation with `steps/` and `sub-forms/` |
|
|
899
|
+
|
|
900
|
+
## 14.5 UI COMPONENT PATTERNS
|
|
901
|
+
|
|
902
|
+
ReFormer does NOT provide UI components - you create them yourself or use a UI library.
|
|
903
|
+
|
|
904
|
+
### Generic FormField Component
|
|
905
|
+
|
|
906
|
+
```tsx
|
|
907
|
+
import type { FieldNode } from '@reformer/core';
|
|
908
|
+
import { useFormControl } from '@reformer/core';
|
|
909
|
+
|
|
910
|
+
interface FormFieldProps<T> {
|
|
911
|
+
control: FieldNode<T>;
|
|
912
|
+
label?: string;
|
|
913
|
+
type?: 'text' | 'email' | 'number' | 'password';
|
|
914
|
+
placeholder?: string;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function FormField<T extends string | number>({
|
|
918
|
+
control,
|
|
919
|
+
label,
|
|
920
|
+
type = 'text',
|
|
921
|
+
placeholder
|
|
922
|
+
}: FormFieldProps<T>) {
|
|
923
|
+
const { value, errors, disabled, touched } = useFormControl(control);
|
|
924
|
+
const showError = touched && errors.length > 0;
|
|
925
|
+
|
|
926
|
+
return (
|
|
927
|
+
<div className="form-field">
|
|
928
|
+
{label && <label>{label}</label>}
|
|
929
|
+
<input
|
|
930
|
+
type={type}
|
|
931
|
+
value={value ?? ''}
|
|
932
|
+
onChange={(e) => {
|
|
933
|
+
const val = type === 'number'
|
|
934
|
+
? Number(e.target.value) as T
|
|
935
|
+
: e.target.value as T;
|
|
936
|
+
control.setValue(val);
|
|
937
|
+
}}
|
|
938
|
+
onBlur={() => control.markAsTouched()}
|
|
939
|
+
disabled={disabled}
|
|
940
|
+
placeholder={placeholder}
|
|
941
|
+
className={showError ? 'error' : ''}
|
|
942
|
+
/>
|
|
943
|
+
{showError && (
|
|
944
|
+
<span className="error-message">{errors[0].message}</span>
|
|
945
|
+
)}
|
|
946
|
+
</div>
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Usage
|
|
951
|
+
<FormField control={form.email} label="Email" type="email" />
|
|
952
|
+
<FormField control={form.age} label="Age" type="number" />
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### FormField for Select
|
|
956
|
+
|
|
957
|
+
```tsx
|
|
958
|
+
interface SelectFieldProps<T extends string> {
|
|
959
|
+
control: FieldNode<T>;
|
|
960
|
+
label?: string;
|
|
961
|
+
options: Array<{ value: T; label: string }>;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function SelectField<T extends string>({ control, label, options }: SelectFieldProps<T>) {
|
|
965
|
+
const { value, errors, disabled, touched } = useFormControl(control);
|
|
966
|
+
|
|
967
|
+
return (
|
|
968
|
+
<div className="form-field">
|
|
969
|
+
{label && <label>{label}</label>}
|
|
970
|
+
<select
|
|
971
|
+
value={value}
|
|
972
|
+
onChange={(e) => control.setValue(e.target.value as T)}
|
|
973
|
+
disabled={disabled}
|
|
974
|
+
>
|
|
975
|
+
{options.map((opt) => (
|
|
976
|
+
<option key={opt.value} value={opt.value}>
|
|
977
|
+
{opt.label}
|
|
978
|
+
</option>
|
|
979
|
+
))}
|
|
980
|
+
</select>
|
|
981
|
+
{touched && errors[0] && <span className="error-message">{errors[0].message}</span>}
|
|
982
|
+
</div>
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### Integration with UI Libraries
|
|
988
|
+
|
|
989
|
+
```tsx
|
|
990
|
+
// With shadcn/ui
|
|
991
|
+
import { Input } from '@/components/ui/input';
|
|
992
|
+
import { Label } from '@/components/ui/label';
|
|
993
|
+
|
|
994
|
+
function ShadcnFormField({ control, label }: FormFieldProps<string>) {
|
|
995
|
+
const { value, errors, disabled } = useFormControl(control);
|
|
996
|
+
|
|
997
|
+
return (
|
|
998
|
+
<div className="space-y-2">
|
|
999
|
+
<Label>{label}</Label>
|
|
1000
|
+
<Input value={value} onChange={(e) => control.setValue(e.target.value)} disabled={disabled} />
|
|
1001
|
+
{errors[0] && <p className="text-red-500">{errors[0].message}</p>}
|
|
1002
|
+
</div>
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
## 15. NON-EXISTENT API (DO NOT USE)
|
|
1008
|
+
|
|
1009
|
+
**The following APIs do NOT exist in @reformer/core:**
|
|
1010
|
+
|
|
1011
|
+
| Wrong | Correct | Notes |
|
|
1012
|
+
|----------|-----------|-------|
|
|
1013
|
+
| `useForm` | `createForm` | There is no useForm hook |
|
|
1014
|
+
| `FieldSchema` | `FieldConfig<T>` | Type for individual field config |
|
|
1015
|
+
| `when()` | `applyWhen()` | Conditional validation function |
|
|
1016
|
+
| `FormFields` | `FieldNode<T>` | Type for field nodes |
|
|
1017
|
+
| `FormInstance<T>` | `GroupNodeWithControls<T>` | Form type for component props |
|
|
1018
|
+
| `useArrayField()` | `form.items.push/map/removeAt` | Use ArrayNode methods directly |
|
|
1019
|
+
| `FormProvider` | `<Component form={form} />` | Pass form via props, no context |
|
|
1020
|
+
| `formState` | `form.valid`, `form.dirty`, etc. | Separate signals on form |
|
|
1021
|
+
| `control` prop | Not needed | Form IS the control |
|
|
1022
|
+
| `register('field')` | `useFormControl(form.field)` | Type-safe field access |
|
|
1023
|
+
| `getFieldValue()` | `ctx.form.field.value.value` | Read via signals |
|
|
1024
|
+
|
|
1025
|
+
### Common Import Errors
|
|
1026
|
+
|
|
1027
|
+
```typescript
|
|
1028
|
+
// WRONG - These do NOT exist
|
|
1029
|
+
import { useForm } from '@reformer/core'; // NO!
|
|
1030
|
+
import { when } from '@reformer/core/validators'; // NO!
|
|
1031
|
+
import type { FieldSchema } from '@reformer/core'; // NO!
|
|
1032
|
+
import type { FormFields } from '@reformer/core'; // NO!
|
|
1033
|
+
|
|
1034
|
+
// CORRECT
|
|
1035
|
+
import { createForm, useFormControl } from '@reformer/core';
|
|
1036
|
+
import { applyWhen } from '@reformer/core/validators';
|
|
1037
|
+
import type { FieldConfig, FieldNode } from '@reformer/core';
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### FormSchema Common Mistakes
|
|
1041
|
+
|
|
1042
|
+
```typescript
|
|
1043
|
+
// WRONG - Simple values don't work
|
|
1044
|
+
const schema = {
|
|
1045
|
+
name: '', // Missing { value, component }
|
|
1046
|
+
email: '', // Missing { value, component }
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
// CORRECT - Every field needs value and component
|
|
1050
|
+
const schema: FormSchema<MyForm> = {
|
|
1051
|
+
name: {
|
|
1052
|
+
value: '',
|
|
1053
|
+
component: Input,
|
|
1054
|
+
componentProps: { label: 'Name' },
|
|
1055
|
+
},
|
|
1056
|
+
email: {
|
|
1057
|
+
value: '',
|
|
1058
|
+
component: Input,
|
|
1059
|
+
componentProps: { label: 'Email', type: 'email' },
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
## 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
|
|
1065
|
+
|
|
1066
|
+
### Why .value.value?
|
|
1067
|
+
|
|
1068
|
+
ReFormer uses `@preact/signals-core` for reactivity:
|
|
1069
|
+
- `field.value` -> `Signal<T>` (reactive container)
|
|
1070
|
+
- `field.value.value` -> `T` (actual value)
|
|
1071
|
+
- `field.getValue()` -> `T` (shorthand method, non-reactive)
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
// Reading values in different contexts:
|
|
1075
|
+
|
|
1076
|
+
// In React components - use hooks
|
|
1077
|
+
const { value } = useFormControl(control.email); // Object with value
|
|
1078
|
+
const email = useFormControlValue(control.email); // Value directly
|
|
1079
|
+
|
|
1080
|
+
// In BehaviorContext (watchField, etc.)
|
|
1081
|
+
watchField(path.firstName, (firstName, ctx) => {
|
|
1082
|
+
// ctx.form is typed as the PARENT GROUP of the watched field!
|
|
1083
|
+
// For path.nested.field: ctx.form = NestedType, NOT RootForm!
|
|
1084
|
+
|
|
1085
|
+
const lastName = ctx.form.lastName.value.value; // Read sibling field
|
|
1086
|
+
|
|
1087
|
+
// Use setFieldValue with full path for root-level fields
|
|
1088
|
+
ctx.setFieldValue('fullName', `${firstName} ${lastName}`);
|
|
1089
|
+
}, { immediate: false }); // REQUIRED to prevent cycle detection!
|
|
1090
|
+
|
|
1091
|
+
// Direct access on form controls
|
|
1092
|
+
form.email.value.value; // Read current value
|
|
1093
|
+
form.address.city.value.value; // Read nested value
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
### Reading Nested Values in watchField
|
|
1097
|
+
|
|
1098
|
+
```typescript
|
|
1099
|
+
// IMPORTANT: ctx.form type depends on the watched path!
|
|
1100
|
+
|
|
1101
|
+
// Watching root-level field
|
|
1102
|
+
watchField(path.loanAmount, (amount, ctx) => {
|
|
1103
|
+
// ctx.form is MyForm - can access all fields
|
|
1104
|
+
const rate = ctx.form.interestRate.value.value;
|
|
1105
|
+
ctx.setFieldValue('monthlyPayment', amount * rate / 12);
|
|
1106
|
+
}, { immediate: false });
|
|
1107
|
+
|
|
1108
|
+
// Watching nested field
|
|
1109
|
+
watchField(path.personalData.lastName, (lastName, ctx) => {
|
|
1110
|
+
// ctx.form is PersonalData, NOT MyForm!
|
|
1111
|
+
const firstName = ctx.form.firstName.value.value; // Works
|
|
1112
|
+
const middleName = ctx.form.middleName.value.value; // Works
|
|
1113
|
+
|
|
1114
|
+
// For root-level field, use setFieldValue with full path
|
|
1115
|
+
ctx.setFieldValue('fullName', `${lastName} ${firstName}`);
|
|
1116
|
+
}, { immediate: false });
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
## 17. COMPUTE FROM vs WATCH FIELD
|
|
1120
|
+
|
|
1121
|
+
### computeFrom - Same Nesting Level Only
|
|
1122
|
+
|
|
1123
|
+
```typescript
|
|
1124
|
+
// Works: all source fields and target at same level
|
|
1125
|
+
computeFrom(
|
|
1126
|
+
[path.price, path.quantity],
|
|
1127
|
+
path.total,
|
|
1128
|
+
({ price, quantity }) => (price || 0) * (quantity || 0)
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
// Works: all nested at same level
|
|
1132
|
+
computeFrom(
|
|
1133
|
+
[path.address.houseNumber, path.address.streetName],
|
|
1134
|
+
path.address.fullAddress,
|
|
1135
|
+
({ houseNumber, streetName }) => `${houseNumber} ${streetName}`
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
// FAILS: different nesting levels
|
|
1139
|
+
computeFrom(
|
|
1140
|
+
[path.nested.price, path.nested.quantity],
|
|
1141
|
+
path.rootTotal, // Different level - won't work!
|
|
1142
|
+
...
|
|
1143
|
+
);
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
### watchField - Any Level
|
|
1147
|
+
|
|
1148
|
+
```typescript
|
|
1149
|
+
// Works for cross-level computation
|
|
1150
|
+
watchField(path.nested.price, (price, ctx) => {
|
|
1151
|
+
const quantity = ctx.form.quantity.value.value; // Sibling in nested
|
|
1152
|
+
ctx.setFieldValue('rootTotal', price * quantity); // Full path to root
|
|
1153
|
+
}, { immediate: false }); // REQUIRED!
|
|
1154
|
+
|
|
1155
|
+
// Works for multiple dependencies
|
|
1156
|
+
watchField(path.loanAmount, (amount, ctx) => {
|
|
1157
|
+
const term = ctx.form.loanTerm.value.value;
|
|
1158
|
+
const rate = ctx.form.interestRate.value.value;
|
|
1159
|
+
|
|
1160
|
+
if (amount && term && rate) {
|
|
1161
|
+
const monthly = calculateMonthlyPayment(amount, term, rate);
|
|
1162
|
+
ctx.setFieldValue('monthlyPayment', monthly);
|
|
1163
|
+
}
|
|
1164
|
+
}, { immediate: false }); // REQUIRED!
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
### Rule of Thumb
|
|
1168
|
+
|
|
1169
|
+
| Scenario | Use |
|
|
1170
|
+
|----------|-----|
|
|
1171
|
+
| All fields share same parent | `computeFrom` (simpler, auto-cleanup) |
|
|
1172
|
+
| Fields at different levels | `watchField` (more flexible) |
|
|
1173
|
+
| Multiple dependencies | `watchField` |
|
|
1174
|
+
| Async computation | `watchField` with async callback |
|
|
1175
|
+
|
|
1176
|
+
## 18. ARRAY OPERATIONS
|
|
1177
|
+
|
|
1178
|
+
### Array Access - CRITICAL
|
|
1179
|
+
|
|
1180
|
+
```typescript
|
|
1181
|
+
// WRONG - bracket notation does NOT work!
|
|
1182
|
+
const first = form.items[0]; // undefined or error
|
|
1183
|
+
const second = form.items[1]; // undefined or error
|
|
1184
|
+
|
|
1185
|
+
// CORRECT - use .at() method
|
|
1186
|
+
const first = form.items.at(0); // GroupNodeWithControls<ItemType> | undefined
|
|
1187
|
+
const second = form.items.at(1); // GroupNodeWithControls<ItemType> | undefined
|
|
1188
|
+
|
|
1189
|
+
// CORRECT - iterate with map (most common pattern)
|
|
1190
|
+
form.items.map((item, index) => {
|
|
1191
|
+
// item is fully typed GroupNode
|
|
1192
|
+
item.name.setValue('New Name');
|
|
1193
|
+
item.price.value.value; // read value
|
|
1194
|
+
});
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
### Array Methods
|
|
1198
|
+
|
|
1199
|
+
```typescript
|
|
1200
|
+
// Add items
|
|
1201
|
+
form.items.push({ name: '', price: 0 }); // Add to end
|
|
1202
|
+
form.items.insert(0, { name: '', price: 0 }); // Insert at index
|
|
1203
|
+
|
|
1204
|
+
// Remove items
|
|
1205
|
+
form.items.removeAt(index); // Remove by index
|
|
1206
|
+
form.items.clear(); // Remove all items
|
|
1207
|
+
|
|
1208
|
+
// Reorder
|
|
1209
|
+
form.items.move(fromIndex, toIndex); // Move item
|
|
1210
|
+
|
|
1211
|
+
// Access (use .at(), NOT brackets!)
|
|
1212
|
+
form.items.length.value; // Current length (Signal)
|
|
1213
|
+
form.items.map((item, index) => ...); // Iterate items
|
|
1214
|
+
form.items.at(index); // Get item at index (NOT items[index]!)
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
### Rendering Arrays
|
|
1218
|
+
|
|
1219
|
+
```tsx
|
|
1220
|
+
function ItemsList({ form }: { form: GroupNodeWithControls<MyForm> }) {
|
|
1221
|
+
const { length } = useFormControl(form.items);
|
|
1222
|
+
|
|
1223
|
+
return (
|
|
1224
|
+
<div>
|
|
1225
|
+
{form.items.map((item, index) => (
|
|
1226
|
+
// item is GroupNode (sub-form) - each field is a control
|
|
1227
|
+
<div key={item.id || index}>
|
|
1228
|
+
<FormField control={item.name} />
|
|
1229
|
+
<FormField control={item.price} />
|
|
1230
|
+
<button onClick={() => form.items.removeAt(index)}>Remove</button>
|
|
1231
|
+
</div>
|
|
1232
|
+
))}
|
|
1233
|
+
|
|
1234
|
+
{length === 0 && <p>No items yet</p>}
|
|
1235
|
+
|
|
1236
|
+
<button onClick={() => form.items.push({ name: '', price: 0 })}>
|
|
1237
|
+
Add Item
|
|
1238
|
+
</button>
|
|
1239
|
+
</div>
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
### Array Cross-Validation
|
|
1245
|
+
|
|
1246
|
+
```typescript
|
|
1247
|
+
// Validate uniqueness across array items
|
|
1248
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
1249
|
+
const items = ctx.form.items;
|
|
1250
|
+
const names = items.map(item => item.name.value.value);
|
|
1251
|
+
const uniqueNames = new Set(names);
|
|
1252
|
+
|
|
1253
|
+
if (names.length !== uniqueNames.size) {
|
|
1254
|
+
return { code: 'duplicate', message: 'Item names must be unique' };
|
|
1255
|
+
}
|
|
1256
|
+
return null;
|
|
1257
|
+
}, { targetField: 'items' });
|
|
1258
|
+
|
|
1259
|
+
// Validate sum of percentages
|
|
1260
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
1261
|
+
const items = ctx.form.items;
|
|
1262
|
+
const totalPercent = items.reduce(
|
|
1263
|
+
(sum, item) => sum + (item.percentage.value.value || 0),
|
|
1264
|
+
0
|
|
1265
|
+
);
|
|
1266
|
+
|
|
1267
|
+
if (Math.abs(totalPercent - 100) > 0.01) {
|
|
1268
|
+
return { code: 'invalid_total', message: 'Percentages must sum to 100%' };
|
|
1269
|
+
}
|
|
1270
|
+
return null;
|
|
1271
|
+
}, { targetField: 'items' });
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
## Cycle Detection Prevention Checklist
|
|
1275
|
+
|
|
1276
|
+
**ALWAYS follow these rules to prevent "Cycle detected" error:**
|
|
1277
|
+
|
|
1278
|
+
1. ✅ **ONE watchField per trigger field** - consolidate all logic into single handler
|
|
1279
|
+
2. ✅ **ALWAYS use `{ immediate: false }`** - required option for watchField
|
|
1280
|
+
3. ✅ **Guard all disable/enable calls** - check `field.disabled.value` before calling
|
|
1281
|
+
4. ✅ **Guard all setValue calls** - only call if value actually differs
|
|
1282
|
+
5. ✅ **Arrays: compare by length** - `[] !== []` is always true, use `.length`
|
|
1283
|
+
6. ✅ **Prefer enableWhen over watchField** - for simple enable/disable logic
|
|
1284
|
+
|
|
1285
|
+
---
|
|
1286
|
+
|
|
1287
|
+
## Cycle Detected Error
|
|
1288
|
+
|
|
1289
|
+
### Problem
|
|
1290
|
+
|
|
1291
|
+
Error `Cycle detected` occurs when reactive system detects circular dependency during field updates.
|
|
1292
|
+
|
|
1293
|
+
### Root Cause
|
|
1294
|
+
|
|
1295
|
+
Multiple `watchField` handlers on the same field (e.g., `path.insuranceType`) each calling `disable()` and `setValue()` creates reactive cycles:
|
|
1296
|
+
|
|
1297
|
+
```typescript
|
|
1298
|
+
// WRONG - Multiple watchers on same field + missing { immediate: false }
|
|
1299
|
+
watchField(path.insuranceType, (_, ctx) => {
|
|
1300
|
+
// Handler 1: vehicle fields
|
|
1301
|
+
if (!isVehicle) {
|
|
1302
|
+
ctx.form.vehicle.vin.disable();
|
|
1303
|
+
ctx.form.vehicle.vin.setValue('');
|
|
1304
|
+
}
|
|
1305
|
+
}); // NO OPTIONS - BAD!
|
|
1306
|
+
|
|
1307
|
+
watchField(path.insuranceType, (_, ctx) => {
|
|
1308
|
+
// Handler 2: property fields - CAUSES CYCLE!
|
|
1309
|
+
if (!isProperty) {
|
|
1310
|
+
ctx.form.property.type.disable();
|
|
1311
|
+
ctx.form.property.type.setValue('');
|
|
1312
|
+
}
|
|
1313
|
+
}); // NO OPTIONS - BAD!
|
|
1314
|
+
|
|
1315
|
+
// More watchers on same field = more cycles
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
### Solution
|
|
1319
|
+
|
|
1320
|
+
1. **Consolidate all watchers for same field into ONE handler**
|
|
1321
|
+
2. **Check state before calling disable/enable/setValue**
|
|
1322
|
+
3. **ALWAYS add `{ immediate: false }` option**
|
|
1323
|
+
|
|
1324
|
+
```typescript
|
|
1325
|
+
// CORRECT - Single consolidated watcher with guards AND { immediate: false }
|
|
1326
|
+
watchField(path.insuranceType, (_value, ctx) => {
|
|
1327
|
+
const insuranceType = ctx.form.insuranceType.value.value;
|
|
1328
|
+
const isVehicle = insuranceType === 'casco' || insuranceType === 'osago';
|
|
1329
|
+
const isProperty = insuranceType === 'property';
|
|
1330
|
+
|
|
1331
|
+
// Helper: check if array value needs update (compare by length, not reference)
|
|
1332
|
+
const needsValueUpdate = <T>(current: T, defaultVal: T): boolean => {
|
|
1333
|
+
if (Array.isArray(current) && Array.isArray(defaultVal)) {
|
|
1334
|
+
return current.length !== defaultVal.length;
|
|
1335
|
+
}
|
|
1336
|
+
return current !== defaultVal;
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
// Helper: disable only if not already disabled, setValue only if different
|
|
1340
|
+
const disableAndReset = <T>(
|
|
1341
|
+
field: { disable: () => void; setValue: (v: T) => void; getValue: () => T; disabled: { value: boolean } } | undefined,
|
|
1342
|
+
defaultValue: T
|
|
1343
|
+
) => {
|
|
1344
|
+
if (field) {
|
|
1345
|
+
if (!field.disabled.value) {
|
|
1346
|
+
field.disable();
|
|
1347
|
+
}
|
|
1348
|
+
if (needsValueUpdate(field.getValue(), defaultValue)) {
|
|
1349
|
+
field.setValue(defaultValue);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
const enableField = (field: { enable: () => void; disabled: { value: boolean } } | undefined) => {
|
|
1355
|
+
if (field && field.disabled.value) {
|
|
1356
|
+
field.enable();
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// --- All vehicle fields in one place ---
|
|
1361
|
+
if (isVehicle) {
|
|
1362
|
+
enableField(ctx.form.vehicle.vin);
|
|
1363
|
+
enableField(ctx.form.vehicle.brand);
|
|
1364
|
+
} else {
|
|
1365
|
+
disableAndReset(ctx.form.vehicle.vin, '');
|
|
1366
|
+
disableAndReset(ctx.form.vehicle.brand, '');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// --- All property fields in one place ---
|
|
1370
|
+
if (isProperty) {
|
|
1371
|
+
enableField(ctx.form.property.type);
|
|
1372
|
+
} else {
|
|
1373
|
+
disableAndReset(ctx.form.property.type, '');
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// --- Arrays: compare by length ---
|
|
1377
|
+
if (isVehicle) {
|
|
1378
|
+
enableField(ctx.form.drivers);
|
|
1379
|
+
} else {
|
|
1380
|
+
disableAndReset(ctx.form.drivers, []); // Won't call setValue if already empty
|
|
1381
|
+
}
|
|
1382
|
+
}, { immediate: false }); // REQUIRED!
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
### Prefer Built-in Behaviors
|
|
1386
|
+
|
|
1387
|
+
**Instead of complex watchField with guards, use built-in behaviors when possible:**
|
|
1388
|
+
|
|
1389
|
+
```typescript
|
|
1390
|
+
// ❌ COMPLEX - watchField with manual guards (error-prone)
|
|
1391
|
+
watchField(path.insuranceType, (_value, ctx) => {
|
|
1392
|
+
const isVehicle = ctx.form.insuranceType.value.value === 'casco';
|
|
1393
|
+
if (isVehicle) {
|
|
1394
|
+
if (ctx.form.vehicle.vin.disabled.value) ctx.form.vehicle.vin.enable();
|
|
1395
|
+
} else {
|
|
1396
|
+
if (!ctx.form.vehicle.vin.disabled.value) ctx.form.vehicle.vin.disable();
|
|
1397
|
+
if (ctx.form.vehicle.vin.getValue() !== '') ctx.form.vehicle.vin.setValue('');
|
|
1398
|
+
}
|
|
1399
|
+
}, { immediate: false });
|
|
1400
|
+
|
|
1401
|
+
// ✅ SIMPLE - enableWhen with resetOnDisable (recommended)
|
|
1402
|
+
enableWhen(path.vehicle.vin, (form) => form.insuranceType === 'casco', { resetOnDisable: true });
|
|
1403
|
+
enableWhen(path.vehicle.brand, (form) => form.insuranceType === 'casco', { resetOnDisable: true });
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
### Key Rules
|
|
1407
|
+
|
|
1408
|
+
1. **ONE watcher per trigger field** - consolidate all logic for `insuranceType` into single `watchField`
|
|
1409
|
+
2. **ALWAYS use `{ immediate: false }`** - prevents execution during initialization
|
|
1410
|
+
3. **Guard disable()** - only call if `!field.disabled.value`
|
|
1411
|
+
4. **Guard enable()** - only call if `field.disabled.value`
|
|
1412
|
+
5. **Guard setValue()** - only call if value actually differs
|
|
1413
|
+
6. **Arrays special case** - compare by `.length`, not by reference (`[] !== []` is always true)
|
|
1414
|
+
|
|
1415
|
+
### Other Watchers
|
|
1416
|
+
|
|
1417
|
+
For watchers on different fields (e.g., `path.health.isSmoker`), apply same guards:
|
|
1418
|
+
|
|
1419
|
+
```typescript
|
|
1420
|
+
watchField(path.health.isSmoker, (_value, ctx) => {
|
|
1421
|
+
const isSmoker = ctx.form.health.isSmoker.value.value;
|
|
1422
|
+
const smokingYearsField = ctx.form.health.smokingYears;
|
|
1423
|
+
|
|
1424
|
+
if (smokingYearsField) {
|
|
1425
|
+
if (isSmoker) {
|
|
1426
|
+
if (smokingYearsField.disabled.value) {
|
|
1427
|
+
smokingYearsField.enable();
|
|
1428
|
+
}
|
|
1429
|
+
} else {
|
|
1430
|
+
if (!smokingYearsField.disabled.value) {
|
|
1431
|
+
smokingYearsField.disable();
|
|
1432
|
+
}
|
|
1433
|
+
if (smokingYearsField.getValue() !== null) {
|
|
1434
|
+
smokingYearsField.setValue(null);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}, { immediate: false }); // REQUIRED!
|
|
1439
|
+
```
|