@reformer/core 1.1.0-beta.8 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/behaviors-DzYL8kY_.js +499 -0
- package/dist/behaviors.js +14 -14
- package/dist/core/behavior/create-field-path.d.ts +3 -16
- package/dist/core/nodes/group-node.d.ts +14 -193
- package/dist/core/utils/field-path.d.ts +48 -0
- package/dist/core/utils/index.d.ts +1 -0
- 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 +3 -1
- package/dist/index.js +492 -986
- package/dist/{registry-helpers-BfCZcMkO.js → registry-helpers-BRxAr6nG.js} +136 -72
- package/dist/{validators-DjXtDVoE.js → validators-gXoHPdqM.js} +194 -231
- package/dist/validators.js +25 -25
- package/llms.txt +317 -172
- package/package.json +8 -4
- package/dist/behaviors-BRaiR-UY.js +0 -528
- package/dist/core/behavior/behavior-applicator.d.ts +0 -71
- package/dist/core/nodes/group-node/field-registry.d.ts +0 -191
- package/dist/core/nodes/group-node/index.d.ts +0 -11
- package/dist/core/nodes/group-node/proxy-builder.d.ts +0 -71
- package/dist/core/nodes/group-node/state-manager.d.ts +0 -184
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ReFormer - LLM Integration Guide
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1. API Reference
|
|
4
4
|
|
|
5
5
|
### Imports (CRITICALLY IMPORTANT)
|
|
6
6
|
|
|
@@ -30,16 +30,16 @@
|
|
|
30
30
|
| `useFormControl(field)` | `{ value, errors, disabled, touched, ... }` | All signals | Full field state, form inputs |
|
|
31
31
|
| `useFormControlValue(field)` | `T` (value directly) | Only value signal | Conditional rendering |
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
**CRITICAL**: Do NOT destructure `useFormControlValue`! It returns `T` directly, NOT `{ value: T }`.
|
|
34
34
|
|
|
35
35
|
```typescript
|
|
36
|
-
//
|
|
36
|
+
// WRONG - will always be undefined!
|
|
37
37
|
const { value: loanType } = useFormControlValue(control.loanType);
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// CORRECT
|
|
40
40
|
const loanType = useFormControlValue(control.loanType);
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// CORRECT - useFormControl returns object, destructuring OK
|
|
43
43
|
const { value, errors, disabled } = useFormControl(control.loanType);
|
|
44
44
|
```
|
|
45
45
|
|
|
@@ -143,7 +143,7 @@ validateTree(validator: (ctx) => ValidationError | null, options?: { targetField
|
|
|
143
143
|
// Conditional validation (3 arguments!)
|
|
144
144
|
applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
|
|
145
145
|
|
|
146
|
-
//
|
|
146
|
+
// applyWhen Examples - CRITICALLY IMPORTANT
|
|
147
147
|
// applyWhen takes 3 arguments: triggerField, condition, validators
|
|
148
148
|
|
|
149
149
|
// Example 1: Simple condition
|
|
@@ -176,7 +176,7 @@ applyWhen(
|
|
|
176
176
|
}
|
|
177
177
|
);
|
|
178
178
|
|
|
179
|
-
//
|
|
179
|
+
// WRONG - only 2 arguments (React Hook Form pattern)
|
|
180
180
|
applyWhen(
|
|
181
181
|
(form) => form.loanType === 'mortgage', // WRONG!
|
|
182
182
|
() => { required(path.propertyValue); }
|
|
@@ -197,8 +197,8 @@ disableWhen(path, condition: (form) => boolean)
|
|
|
197
197
|
// Computed fields (same nesting level)
|
|
198
198
|
computeFrom(sourcePaths[], targetPath, compute: (values) => result, options?: { debounce?: number; condition?: (form) => boolean })
|
|
199
199
|
|
|
200
|
-
// Watch field changes
|
|
201
|
-
watchField(path, callback: (value, ctx: BehaviorContext) => void, options
|
|
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
202
|
|
|
203
203
|
// Copy values between fields
|
|
204
204
|
copyFrom(sourcePath, targetPath, options?: { when?: (form) => boolean; fields?: string[]; transform?: (value) => value })
|
|
@@ -206,8 +206,8 @@ copyFrom(sourcePath, targetPath, options?: { when?: (form) => boolean; fields?:
|
|
|
206
206
|
// Reset field when condition met
|
|
207
207
|
resetWhen(path, condition: (form) => boolean, options?: { toValue?: any })
|
|
208
208
|
|
|
209
|
-
// Re-validate when
|
|
210
|
-
revalidateWhen(
|
|
209
|
+
// Re-validate target field when any trigger changes
|
|
210
|
+
revalidateWhen(targetPath, triggerPaths[], options?: { debounce?: number })
|
|
211
211
|
|
|
212
212
|
// Sync multiple fields
|
|
213
213
|
syncFields(paths[], options?: { bidirectional?: boolean })
|
|
@@ -220,7 +220,7 @@ transformers.trim, transformers.toUpperCase, transformers.toLowerCase, transform
|
|
|
220
220
|
interface BehaviorContext<TForm> {
|
|
221
221
|
form: GroupNodeWithControls<TForm>; // Form proxy with typed field access
|
|
222
222
|
setFieldValue: (path: string, value: any) => void;
|
|
223
|
-
//
|
|
223
|
+
// To READ field values, use: ctx.form.fieldName.value.value
|
|
224
224
|
}
|
|
225
225
|
```
|
|
226
226
|
|
|
@@ -241,7 +241,7 @@ enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
|
|
|
241
241
|
// Use watchField instead:
|
|
242
242
|
watchField(path.nested.field, (value, ctx) => {
|
|
243
243
|
ctx.setFieldValue('rootField', computedValue);
|
|
244
|
-
});
|
|
244
|
+
}, { immediate: false });
|
|
245
245
|
```
|
|
246
246
|
|
|
247
247
|
### Type-Safe useFormControl
|
|
@@ -250,31 +250,60 @@ watchField(path.nested.field, (value, ctx) => {
|
|
|
250
250
|
const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
|
|
251
251
|
```
|
|
252
252
|
|
|
253
|
-
|
|
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
|
|
254
283
|
|
|
255
284
|
### useFormControlValue (CRITICAL)
|
|
256
285
|
|
|
257
286
|
```typescript
|
|
258
|
-
//
|
|
287
|
+
// WRONG - useFormControlValue returns T directly, NOT { value: T }
|
|
259
288
|
const { value: loanType } = useFormControlValue(control.loanType);
|
|
260
289
|
// Result: loanType is ALWAYS undefined! Conditional rendering will fail.
|
|
261
290
|
|
|
262
|
-
//
|
|
291
|
+
// CORRECT
|
|
263
292
|
const loanType = useFormControlValue(control.loanType);
|
|
264
293
|
|
|
265
|
-
//
|
|
294
|
+
// ALSO CORRECT - useFormControl returns object
|
|
266
295
|
const { value, errors } = useFormControl(control.loanType);
|
|
267
296
|
```
|
|
268
297
|
|
|
269
298
|
### Reading Field Values in BehaviorContext (CRITICAL)
|
|
270
299
|
|
|
271
300
|
```typescript
|
|
272
|
-
//
|
|
301
|
+
// WRONG - getFieldValue does NOT exist!
|
|
273
302
|
watchField(path.amount, (amount, ctx) => {
|
|
274
303
|
const rate = ctx.getFieldValue('rate'); // ERROR: Property 'getFieldValue' does not exist
|
|
275
304
|
});
|
|
276
305
|
|
|
277
|
-
//
|
|
306
|
+
// CORRECT - use ctx.form.fieldName.value.value
|
|
278
307
|
watchField(path.amount, (amount, ctx) => {
|
|
279
308
|
const rate = ctx.form.rate.value.value; // Read via signal
|
|
280
309
|
ctx.setFieldValue('total', amount * rate);
|
|
@@ -284,21 +313,21 @@ watchField(path.amount, (amount, ctx) => {
|
|
|
284
313
|
### Validators
|
|
285
314
|
|
|
286
315
|
```typescript
|
|
287
|
-
//
|
|
316
|
+
// WRONG
|
|
288
317
|
required(path.email, 'Email is required');
|
|
289
318
|
|
|
290
|
-
//
|
|
319
|
+
// CORRECT
|
|
291
320
|
required(path.email, { message: 'Email is required' });
|
|
292
321
|
```
|
|
293
322
|
|
|
294
323
|
### Types
|
|
295
324
|
|
|
296
325
|
```typescript
|
|
297
|
-
//
|
|
326
|
+
// WRONG
|
|
298
327
|
amount: number | null;
|
|
299
328
|
[key: string]: unknown;
|
|
300
329
|
|
|
301
|
-
//
|
|
330
|
+
// CORRECT
|
|
302
331
|
amount: number | undefined;
|
|
303
332
|
// No index signature
|
|
304
333
|
```
|
|
@@ -306,10 +335,10 @@ amount: number | undefined;
|
|
|
306
335
|
### computeFrom
|
|
307
336
|
|
|
308
337
|
```typescript
|
|
309
|
-
//
|
|
338
|
+
// WRONG - different nesting levels
|
|
310
339
|
computeFrom([path.nested.a, path.nested.b], path.root, ...)
|
|
311
340
|
|
|
312
|
-
//
|
|
341
|
+
// CORRECT - use watchField
|
|
313
342
|
watchField(path.nested.a, (_, ctx) => {
|
|
314
343
|
ctx.setFieldValue('root', computed);
|
|
315
344
|
});
|
|
@@ -318,10 +347,10 @@ watchField(path.nested.a, (_, ctx) => {
|
|
|
318
347
|
### Imports
|
|
319
348
|
|
|
320
349
|
```typescript
|
|
321
|
-
//
|
|
350
|
+
// WRONG - types are not in submodules
|
|
322
351
|
import { ValidationSchemaFn } from '@reformer/core/validators';
|
|
323
352
|
|
|
324
|
-
//
|
|
353
|
+
// CORRECT - types from main module
|
|
325
354
|
import type { ValidationSchemaFn } from '@reformer/core';
|
|
326
355
|
import { required, email } from '@reformer/core/validators';
|
|
327
356
|
```
|
|
@@ -335,6 +364,7 @@ import { required, email } from '@reformer/core/validators';
|
|
|
335
364
|
| `FormFields[]` instead of concrete type | Type inference issue | Use `as FieldNode<T>` |
|
|
336
365
|
| `Type 'X' is missing properties from type 'Y'` | Cross-level computeFrom | Use watchField instead |
|
|
337
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 |
|
|
338
368
|
|
|
339
369
|
## 6. COMPLETE IMPORT EXAMPLE
|
|
340
370
|
|
|
@@ -361,7 +391,7 @@ import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/be
|
|
|
361
391
|
## 7. FORM TYPE DEFINITION
|
|
362
392
|
|
|
363
393
|
```typescript
|
|
364
|
-
//
|
|
394
|
+
// CORRECT form type definition
|
|
365
395
|
interface MyForm {
|
|
366
396
|
// Required fields
|
|
367
397
|
name: string;
|
|
@@ -390,7 +420,7 @@ interface MyForm {
|
|
|
390
420
|
|
|
391
421
|
## 8. FORMSCHEMA FORMAT (CRITICALLY IMPORTANT)
|
|
392
422
|
|
|
393
|
-
|
|
423
|
+
**Every field MUST have `value` and `component` properties!**
|
|
394
424
|
|
|
395
425
|
### FieldConfig Interface
|
|
396
426
|
|
|
@@ -477,13 +507,13 @@ const schema: FormSchema<MyForm> = {
|
|
|
477
507
|
};
|
|
478
508
|
```
|
|
479
509
|
|
|
480
|
-
###
|
|
510
|
+
### WRONG - This will NOT compile
|
|
481
511
|
|
|
482
512
|
```typescript
|
|
483
513
|
// Missing value and component - TypeScript will error!
|
|
484
514
|
const schema = {
|
|
485
|
-
name: '', //
|
|
486
|
-
email: '', //
|
|
515
|
+
name: '', // Wrong
|
|
516
|
+
email: '', // Wrong
|
|
487
517
|
};
|
|
488
518
|
```
|
|
489
519
|
|
|
@@ -503,7 +533,7 @@ form.address.city.value.value; // Get current value
|
|
|
503
533
|
form.items.push({ id: '1', name: 'Item' }); // Array operations
|
|
504
534
|
```
|
|
505
535
|
|
|
506
|
-
###
|
|
536
|
+
### createForm Returns a Proxy
|
|
507
537
|
|
|
508
538
|
```typescript
|
|
509
539
|
// createForm() returns GroupNodeWithControls<T> (a Proxy wrapper around GroupNode)
|
|
@@ -514,12 +544,12 @@ form.email // FieldNode<string> - TypeScript knows the type!
|
|
|
514
544
|
form.address.city // FieldNode<string> - nested access works
|
|
515
545
|
form.items.at(0) // GroupNodeWithControls<ItemType> - array items
|
|
516
546
|
|
|
517
|
-
//
|
|
547
|
+
// IMPORTANT: Proxy doesn't pass instanceof checks!
|
|
518
548
|
// Use type guards instead:
|
|
519
549
|
import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
|
|
520
550
|
|
|
521
|
-
if (isFieldNode(node)) { /* ... */ } //
|
|
522
|
-
if (node instanceof FieldNode) { /* ... */ } //
|
|
551
|
+
if (isFieldNode(node)) { /* ... */ } // Works with Proxy
|
|
552
|
+
if (node instanceof FieldNode) { /* ... */ } // Fails with Proxy!
|
|
523
553
|
```
|
|
524
554
|
|
|
525
555
|
## 9. ARRAY SCHEMA FORMAT
|
|
@@ -527,7 +557,7 @@ if (node instanceof FieldNode) { /* ... */ } // ❌ Fails with Proxy!
|
|
|
527
557
|
**Array items are sub-forms!** Each array element is a complete sub-form with its own fields, validation, and behavior.
|
|
528
558
|
|
|
529
559
|
```typescript
|
|
530
|
-
//
|
|
560
|
+
// CORRECT - use tuple format for arrays
|
|
531
561
|
// The template item defines the sub-form schema for each array element
|
|
532
562
|
const itemSchema = {
|
|
533
563
|
id: { value: '', component: Input },
|
|
@@ -548,7 +578,7 @@ form.items.map((item) => {
|
|
|
548
578
|
```
|
|
549
579
|
|
|
550
580
|
```typescript
|
|
551
|
-
//
|
|
581
|
+
// WRONG - object format is NOT supported
|
|
552
582
|
const schema = {
|
|
553
583
|
items: { schema: itemSchema, initialItems: [] }, // This will NOT work
|
|
554
584
|
};
|
|
@@ -578,7 +608,7 @@ validateItems(path.items, (itemPath) => {
|
|
|
578
608
|
## 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)
|
|
579
609
|
|
|
580
610
|
```typescript
|
|
581
|
-
//
|
|
611
|
+
// CORRECT - async watchField with ALL safeguards
|
|
582
612
|
watchField(
|
|
583
613
|
path.parentField,
|
|
584
614
|
async (value, ctx) => {
|
|
@@ -595,7 +625,7 @@ watchField(
|
|
|
595
625
|
{ immediate: false, debounce: 300 } // REQUIRED options
|
|
596
626
|
);
|
|
597
627
|
|
|
598
|
-
//
|
|
628
|
+
// WRONG - missing safeguards
|
|
599
629
|
watchField(path.field, async (value, ctx) => {
|
|
600
630
|
const { data } = await fetchData(value); // Will fail silently!
|
|
601
631
|
});
|
|
@@ -610,7 +640,7 @@ watchField(path.field, async (value, ctx) => {
|
|
|
610
640
|
## 11. ARRAY CLEANUP PATTERN
|
|
611
641
|
|
|
612
642
|
```typescript
|
|
613
|
-
//
|
|
643
|
+
// CORRECT - cleanup array when checkbox unchecked
|
|
614
644
|
watchField(
|
|
615
645
|
path.hasItems,
|
|
616
646
|
(hasItems, ctx) => {
|
|
@@ -621,7 +651,7 @@ watchField(
|
|
|
621
651
|
{ immediate: false }
|
|
622
652
|
);
|
|
623
653
|
|
|
624
|
-
//
|
|
654
|
+
// WRONG - no immediate: false, no null check
|
|
625
655
|
watchField(path.hasItems, (hasItems, ctx) => {
|
|
626
656
|
if (!hasItems) ctx.form.items.clear(); // May crash on init!
|
|
627
657
|
});
|
|
@@ -709,17 +739,17 @@ function MultiStepForm() {
|
|
|
709
739
|
}
|
|
710
740
|
```
|
|
711
741
|
|
|
712
|
-
## 13.
|
|
742
|
+
## 13. EXTENDED COMMON MISTAKES
|
|
713
743
|
|
|
714
744
|
### Behavior Composition (Cycle Error)
|
|
715
745
|
|
|
716
746
|
```typescript
|
|
717
|
-
//
|
|
747
|
+
// WRONG - apply() in behavior causes "Cycle detected"
|
|
718
748
|
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
719
749
|
apply(addressBehavior, path.address); // WILL FAIL!
|
|
720
750
|
};
|
|
721
751
|
|
|
722
|
-
//
|
|
752
|
+
// CORRECT - inline or use setup function
|
|
723
753
|
const setupAddressBehavior = (path: FieldPath<Address>) => {
|
|
724
754
|
watchField(path.region, async (region, ctx) => {
|
|
725
755
|
// ...
|
|
@@ -734,12 +764,12 @@ const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
|
734
764
|
### Infinite Loop in watchField
|
|
735
765
|
|
|
736
766
|
```typescript
|
|
737
|
-
//
|
|
767
|
+
// WRONG - causes infinite loop
|
|
738
768
|
watchField(path.field, (value, ctx) => {
|
|
739
769
|
ctx.form.field.setValue(value.toUpperCase()); // Loop!
|
|
740
770
|
});
|
|
741
771
|
|
|
742
|
-
//
|
|
772
|
+
// CORRECT - write to different field OR add guard
|
|
743
773
|
watchField(path.input, (value, ctx) => {
|
|
744
774
|
const upper = value?.toUpperCase() || '';
|
|
745
775
|
if (ctx.form.display.value.value !== upper) {
|
|
@@ -748,13 +778,53 @@ watchField(path.input, (value, ctx) => {
|
|
|
748
778
|
}, { immediate: false });
|
|
749
779
|
```
|
|
750
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
|
+
|
|
751
821
|
### validateTree Typing
|
|
752
822
|
|
|
753
823
|
```typescript
|
|
754
|
-
//
|
|
824
|
+
// WRONG - implicit any
|
|
755
825
|
validateTree((ctx) => { ... });
|
|
756
826
|
|
|
757
|
-
//
|
|
827
|
+
// CORRECT - explicit typing
|
|
758
828
|
validateTree((ctx: { form: MyForm }) => {
|
|
759
829
|
if (ctx.form.field1 > ctx.form.field2) {
|
|
760
830
|
return { code: 'error', message: 'Invalid' };
|
|
@@ -891,11 +961,7 @@ interface SelectFieldProps<T extends string> {
|
|
|
891
961
|
options: Array<{ value: T; label: string }>;
|
|
892
962
|
}
|
|
893
963
|
|
|
894
|
-
function SelectField<T extends string>({
|
|
895
|
-
control,
|
|
896
|
-
label,
|
|
897
|
-
options
|
|
898
|
-
}: SelectFieldProps<T>) {
|
|
964
|
+
function SelectField<T extends string>({ control, label, options }: SelectFieldProps<T>) {
|
|
899
965
|
const { value, errors, disabled, touched } = useFormControl(control);
|
|
900
966
|
|
|
901
967
|
return (
|
|
@@ -912,9 +978,7 @@ function SelectField<T extends string>({
|
|
|
912
978
|
</option>
|
|
913
979
|
))}
|
|
914
980
|
</select>
|
|
915
|
-
{touched && errors[0] &&
|
|
916
|
-
<span className="error-message">{errors[0].message}</span>
|
|
917
|
-
)}
|
|
981
|
+
{touched && errors[0] && <span className="error-message">{errors[0].message}</span>}
|
|
918
982
|
</div>
|
|
919
983
|
);
|
|
920
984
|
}
|
|
@@ -933,11 +997,7 @@ function ShadcnFormField({ control, label }: FormFieldProps<string>) {
|
|
|
933
997
|
return (
|
|
934
998
|
<div className="space-y-2">
|
|
935
999
|
<Label>{label}</Label>
|
|
936
|
-
<Input
|
|
937
|
-
value={value}
|
|
938
|
-
onChange={(e) => control.setValue(e.target.value)}
|
|
939
|
-
disabled={disabled}
|
|
940
|
-
/>
|
|
1000
|
+
<Input value={value} onChange={(e) => control.setValue(e.target.value)} disabled={disabled} />
|
|
941
1001
|
{errors[0] && <p className="text-red-500">{errors[0].message}</p>}
|
|
942
1002
|
</div>
|
|
943
1003
|
);
|
|
@@ -946,9 +1006,9 @@ function ShadcnFormField({ control, label }: FormFieldProps<string>) {
|
|
|
946
1006
|
|
|
947
1007
|
## 15. NON-EXISTENT API (DO NOT USE)
|
|
948
1008
|
|
|
949
|
-
|
|
1009
|
+
**The following APIs do NOT exist in @reformer/core:**
|
|
950
1010
|
|
|
951
|
-
|
|
|
1011
|
+
| Wrong | Correct | Notes |
|
|
952
1012
|
|----------|-----------|-------|
|
|
953
1013
|
| `useForm` | `createForm` | There is no useForm hook |
|
|
954
1014
|
| `FieldSchema` | `FieldConfig<T>` | Type for individual field config |
|
|
@@ -965,13 +1025,13 @@ function ShadcnFormField({ control, label }: FormFieldProps<string>) {
|
|
|
965
1025
|
### Common Import Errors
|
|
966
1026
|
|
|
967
1027
|
```typescript
|
|
968
|
-
//
|
|
1028
|
+
// WRONG - These do NOT exist
|
|
969
1029
|
import { useForm } from '@reformer/core'; // NO!
|
|
970
1030
|
import { when } from '@reformer/core/validators'; // NO!
|
|
971
1031
|
import type { FieldSchema } from '@reformer/core'; // NO!
|
|
972
1032
|
import type { FormFields } from '@reformer/core'; // NO!
|
|
973
1033
|
|
|
974
|
-
//
|
|
1034
|
+
// CORRECT
|
|
975
1035
|
import { createForm, useFormControl } from '@reformer/core';
|
|
976
1036
|
import { applyWhen } from '@reformer/core/validators';
|
|
977
1037
|
import type { FieldConfig, FieldNode } from '@reformer/core';
|
|
@@ -980,13 +1040,13 @@ import type { FieldConfig, FieldNode } from '@reformer/core';
|
|
|
980
1040
|
### FormSchema Common Mistakes
|
|
981
1041
|
|
|
982
1042
|
```typescript
|
|
983
|
-
//
|
|
1043
|
+
// WRONG - Simple values don't work
|
|
984
1044
|
const schema = {
|
|
985
1045
|
name: '', // Missing { value, component }
|
|
986
1046
|
email: '', // Missing { value, component }
|
|
987
1047
|
};
|
|
988
1048
|
|
|
989
|
-
//
|
|
1049
|
+
// CORRECT - Every field needs value and component
|
|
990
1050
|
const schema: FormSchema<MyForm> = {
|
|
991
1051
|
name: {
|
|
992
1052
|
value: '',
|
|
@@ -1001,97 +1061,14 @@ const schema: FormSchema<MyForm> = {
|
|
|
1001
1061
|
};
|
|
1002
1062
|
```
|
|
1003
1063
|
|
|
1004
|
-
## 15.5 REACT HOOK FORM MIGRATION
|
|
1005
|
-
|
|
1006
|
-
If you're familiar with React Hook Form, here's how to translate patterns to ReFormer:
|
|
1007
|
-
|
|
1008
|
-
| React Hook Form | ReFormer | Notes |
|
|
1009
|
-
|-----------------|----------|-------|
|
|
1010
|
-
| `const { register, handleSubmit } = useForm()` | `const form = createForm({...})` | Call OUTSIDE component |
|
|
1011
|
-
| `register('fieldName')` | `useFormControl(form.fieldName)` | Returns control object |
|
|
1012
|
-
| `watch('fieldName')` | `useFormControlValue(form.fieldName)` | Returns value directly (NOT `{ value }`) |
|
|
1013
|
-
| `setValue('field', value)` | `form.field.setValue(value)` | Direct method call |
|
|
1014
|
-
| `getValues('field')` | `form.field.value.value` | Signal-based |
|
|
1015
|
-
| `formState.errors` | `useFormControl(form.field).errors` | Per-field errors array |
|
|
1016
|
-
| `formState.isValid` | `form.valid.value` | Signal |
|
|
1017
|
-
| `formState.isDirty` | `form.dirty.value` | Signal |
|
|
1018
|
-
| `handleSubmit(onSubmit)` | `form.submit(onSubmit)` | Built-in validation |
|
|
1019
|
-
| `<FormProvider form={...}>` | `<Component form={form} />` | Props, NOT context |
|
|
1020
|
-
| `useFormContext()` | Not needed | Pass form via props |
|
|
1021
|
-
| `useFieldArray({ name: 'items' })` | `form.items` (ArrayNode) | Direct array access |
|
|
1022
|
-
| `fields.map((field, index) => ...)` | `form.items.map((item, index) => ...)` | item is sub-form |
|
|
1023
|
-
| `append(data)` | `form.items.push(data)` | Add item |
|
|
1024
|
-
| `remove(index)` | `form.items.removeAt(index)` | Remove item |
|
|
1025
|
-
| `control` prop | Not needed | Form IS the control |
|
|
1026
|
-
|
|
1027
|
-
### Key Differences
|
|
1028
|
-
|
|
1029
|
-
1. **No Provider/Context** - Pass form directly via props
|
|
1030
|
-
```typescript
|
|
1031
|
-
// ❌ React Hook Form pattern - doesn't exist in ReFormer
|
|
1032
|
-
<FormProvider form={form}>
|
|
1033
|
-
<NestedComponent />
|
|
1034
|
-
</FormProvider>
|
|
1035
|
-
|
|
1036
|
-
// ✅ ReFormer pattern - pass via props
|
|
1037
|
-
<NestedComponent form={form} />
|
|
1038
|
-
```
|
|
1039
|
-
|
|
1040
|
-
2. **Form Creation Location** - Create outside component
|
|
1041
|
-
```typescript
|
|
1042
|
-
// ❌ WRONG - creates new form on every render
|
|
1043
|
-
function MyComponent() {
|
|
1044
|
-
const form = createForm({...});
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// ✅ CORRECT - create once, outside component
|
|
1048
|
-
const form = createForm({...});
|
|
1049
|
-
function MyComponent() {
|
|
1050
|
-
const ctrl = useFormControl(form.name);
|
|
1051
|
-
}
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
3. **Type-Safe Field Access** - No string paths
|
|
1055
|
-
```typescript
|
|
1056
|
-
// React Hook Form
|
|
1057
|
-
register('user.address.city')
|
|
1058
|
-
|
|
1059
|
-
// ReFormer - fully typed
|
|
1060
|
-
form.user.address.city.setValue('New York')
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
4. **Array Items are Sub-Forms**
|
|
1064
|
-
```typescript
|
|
1065
|
-
// React Hook Form
|
|
1066
|
-
fields.map((field, index) => (
|
|
1067
|
-
<input {...register(`items.${index}.name`)} />
|
|
1068
|
-
))
|
|
1069
|
-
|
|
1070
|
-
// ReFormer - each item is a typed GroupNode
|
|
1071
|
-
form.items.map((item, index) => (
|
|
1072
|
-
<FormField control={item.name} /> // item.name is FieldNode
|
|
1073
|
-
))
|
|
1074
|
-
```
|
|
1075
|
-
|
|
1076
|
-
5. **Reading Values in Behaviors**
|
|
1077
|
-
```typescript
|
|
1078
|
-
// React Hook Form
|
|
1079
|
-
watch('fieldName')
|
|
1080
|
-
|
|
1081
|
-
// ReFormer in watchField callback
|
|
1082
|
-
watchField(path.trigger, (value, ctx) => {
|
|
1083
|
-
const other = ctx.form.otherField.value.value; // Signal access
|
|
1084
|
-
});
|
|
1085
|
-
```
|
|
1086
|
-
|
|
1087
1064
|
## 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
|
|
1088
1065
|
|
|
1089
1066
|
### Why .value.value?
|
|
1090
1067
|
|
|
1091
1068
|
ReFormer uses `@preact/signals-core` for reactivity:
|
|
1092
|
-
- `field.value`
|
|
1093
|
-
- `field.value.value`
|
|
1094
|
-
- `field.getValue()`
|
|
1069
|
+
- `field.value` -> `Signal<T>` (reactive container)
|
|
1070
|
+
- `field.value.value` -> `T` (actual value)
|
|
1071
|
+
- `field.getValue()` -> `T` (shorthand method, non-reactive)
|
|
1095
1072
|
|
|
1096
1073
|
```typescript
|
|
1097
1074
|
// Reading values in different contexts:
|
|
@@ -1102,14 +1079,14 @@ const email = useFormControlValue(control.email); // Value directly
|
|
|
1102
1079
|
|
|
1103
1080
|
// In BehaviorContext (watchField, etc.)
|
|
1104
1081
|
watchField(path.firstName, (firstName, ctx) => {
|
|
1105
|
-
//
|
|
1082
|
+
// ctx.form is typed as the PARENT GROUP of the watched field!
|
|
1106
1083
|
// For path.nested.field: ctx.form = NestedType, NOT RootForm!
|
|
1107
1084
|
|
|
1108
1085
|
const lastName = ctx.form.lastName.value.value; // Read sibling field
|
|
1109
1086
|
|
|
1110
1087
|
// Use setFieldValue with full path for root-level fields
|
|
1111
1088
|
ctx.setFieldValue('fullName', `${firstName} ${lastName}`);
|
|
1112
|
-
});
|
|
1089
|
+
}, { immediate: false }); // REQUIRED to prevent cycle detection!
|
|
1113
1090
|
|
|
1114
1091
|
// Direct access on form controls
|
|
1115
1092
|
form.email.value.value; // Read current value
|
|
@@ -1119,24 +1096,24 @@ form.address.city.value.value; // Read nested value
|
|
|
1119
1096
|
### Reading Nested Values in watchField
|
|
1120
1097
|
|
|
1121
1098
|
```typescript
|
|
1122
|
-
//
|
|
1099
|
+
// IMPORTANT: ctx.form type depends on the watched path!
|
|
1123
1100
|
|
|
1124
1101
|
// Watching root-level field
|
|
1125
1102
|
watchField(path.loanAmount, (amount, ctx) => {
|
|
1126
1103
|
// ctx.form is MyForm - can access all fields
|
|
1127
1104
|
const rate = ctx.form.interestRate.value.value;
|
|
1128
1105
|
ctx.setFieldValue('monthlyPayment', amount * rate / 12);
|
|
1129
|
-
});
|
|
1106
|
+
}, { immediate: false });
|
|
1130
1107
|
|
|
1131
1108
|
// Watching nested field
|
|
1132
1109
|
watchField(path.personalData.lastName, (lastName, ctx) => {
|
|
1133
1110
|
// ctx.form is PersonalData, NOT MyForm!
|
|
1134
|
-
const firstName = ctx.form.firstName.value.value; //
|
|
1135
|
-
const middleName = ctx.form.middleName.value.value; //
|
|
1111
|
+
const firstName = ctx.form.firstName.value.value; // Works
|
|
1112
|
+
const middleName = ctx.form.middleName.value.value; // Works
|
|
1136
1113
|
|
|
1137
1114
|
// For root-level field, use setFieldValue with full path
|
|
1138
1115
|
ctx.setFieldValue('fullName', `${lastName} ${firstName}`);
|
|
1139
|
-
});
|
|
1116
|
+
}, { immediate: false });
|
|
1140
1117
|
```
|
|
1141
1118
|
|
|
1142
1119
|
## 17. COMPUTE FROM vs WATCH FIELD
|
|
@@ -1144,21 +1121,21 @@ watchField(path.personalData.lastName, (lastName, ctx) => {
|
|
|
1144
1121
|
### computeFrom - Same Nesting Level Only
|
|
1145
1122
|
|
|
1146
1123
|
```typescript
|
|
1147
|
-
//
|
|
1124
|
+
// Works: all source fields and target at same level
|
|
1148
1125
|
computeFrom(
|
|
1149
1126
|
[path.price, path.quantity],
|
|
1150
1127
|
path.total,
|
|
1151
1128
|
({ price, quantity }) => (price || 0) * (quantity || 0)
|
|
1152
1129
|
);
|
|
1153
1130
|
|
|
1154
|
-
//
|
|
1131
|
+
// Works: all nested at same level
|
|
1155
1132
|
computeFrom(
|
|
1156
1133
|
[path.address.houseNumber, path.address.streetName],
|
|
1157
1134
|
path.address.fullAddress,
|
|
1158
1135
|
({ houseNumber, streetName }) => `${houseNumber} ${streetName}`
|
|
1159
1136
|
);
|
|
1160
1137
|
|
|
1161
|
-
//
|
|
1138
|
+
// FAILS: different nesting levels
|
|
1162
1139
|
computeFrom(
|
|
1163
1140
|
[path.nested.price, path.nested.quantity],
|
|
1164
1141
|
path.rootTotal, // Different level - won't work!
|
|
@@ -1169,13 +1146,13 @@ computeFrom(
|
|
|
1169
1146
|
### watchField - Any Level
|
|
1170
1147
|
|
|
1171
1148
|
```typescript
|
|
1172
|
-
//
|
|
1149
|
+
// Works for cross-level computation
|
|
1173
1150
|
watchField(path.nested.price, (price, ctx) => {
|
|
1174
1151
|
const quantity = ctx.form.quantity.value.value; // Sibling in nested
|
|
1175
1152
|
ctx.setFieldValue('rootTotal', price * quantity); // Full path to root
|
|
1176
|
-
});
|
|
1153
|
+
}, { immediate: false }); // REQUIRED!
|
|
1177
1154
|
|
|
1178
|
-
//
|
|
1155
|
+
// Works for multiple dependencies
|
|
1179
1156
|
watchField(path.loanAmount, (amount, ctx) => {
|
|
1180
1157
|
const term = ctx.form.loanTerm.value.value;
|
|
1181
1158
|
const rate = ctx.form.interestRate.value.value;
|
|
@@ -1184,7 +1161,7 @@ watchField(path.loanAmount, (amount, ctx) => {
|
|
|
1184
1161
|
const monthly = calculateMonthlyPayment(amount, term, rate);
|
|
1185
1162
|
ctx.setFieldValue('monthlyPayment', monthly);
|
|
1186
1163
|
}
|
|
1187
|
-
});
|
|
1164
|
+
}, { immediate: false }); // REQUIRED!
|
|
1188
1165
|
```
|
|
1189
1166
|
|
|
1190
1167
|
### Rule of Thumb
|
|
@@ -1198,18 +1175,18 @@ watchField(path.loanAmount, (amount, ctx) => {
|
|
|
1198
1175
|
|
|
1199
1176
|
## 18. ARRAY OPERATIONS
|
|
1200
1177
|
|
|
1201
|
-
###
|
|
1178
|
+
### Array Access - CRITICAL
|
|
1202
1179
|
|
|
1203
1180
|
```typescript
|
|
1204
|
-
//
|
|
1181
|
+
// WRONG - bracket notation does NOT work!
|
|
1205
1182
|
const first = form.items[0]; // undefined or error
|
|
1206
1183
|
const second = form.items[1]; // undefined or error
|
|
1207
1184
|
|
|
1208
|
-
//
|
|
1185
|
+
// CORRECT - use .at() method
|
|
1209
1186
|
const first = form.items.at(0); // GroupNodeWithControls<ItemType> | undefined
|
|
1210
1187
|
const second = form.items.at(1); // GroupNodeWithControls<ItemType> | undefined
|
|
1211
1188
|
|
|
1212
|
-
//
|
|
1189
|
+
// CORRECT - iterate with map (most common pattern)
|
|
1213
1190
|
form.items.map((item, index) => {
|
|
1214
1191
|
// item is fully typed GroupNode
|
|
1215
1192
|
item.name.setValue('New Name');
|
|
@@ -1292,3 +1269,171 @@ validateTree((ctx: { form: MyForm }) => {
|
|
|
1292
1269
|
}
|
|
1293
1270
|
return null;
|
|
1294
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
|
+
```
|