@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.
Files changed (99) hide show
  1. package/dist/behaviors-DzYL8kY_.js +499 -0
  2. package/dist/behaviors.d.ts +6 -2
  3. package/dist/behaviors.js +19 -227
  4. package/dist/core/behavior/behavior-context.d.ts +6 -2
  5. package/dist/core/behavior/create-field-path.d.ts +3 -16
  6. package/dist/core/nodes/group-node.d.ts +14 -193
  7. package/dist/core/types/form-context.d.ts +10 -4
  8. package/dist/core/utils/field-path.d.ts +48 -0
  9. package/dist/core/utils/index.d.ts +1 -0
  10. package/dist/core/validation/core/validate-tree.d.ts +10 -4
  11. package/dist/core/validation/field-path.d.ts +3 -39
  12. package/dist/core/validation/validation-context.d.ts +23 -0
  13. package/dist/hooks/types.d.ts +328 -0
  14. package/dist/hooks/useFormControl.d.ts +13 -37
  15. package/dist/hooks/useFormControlValue.d.ts +167 -0
  16. package/dist/hooks/useSignalSubscription.d.ts +17 -0
  17. package/dist/index.d.ts +6 -1
  18. package/dist/index.js +2886 -8
  19. package/dist/{create-field-path-CdPF3lIK.js → registry-helpers-BRxAr6nG.js} +133 -347
  20. package/dist/validators-gXoHPdqM.js +418 -0
  21. package/dist/validators.d.ts +6 -2
  22. package/dist/validators.js +29 -296
  23. package/llms.txt +1283 -22
  24. package/package.json +8 -4
  25. package/dist/core/behavior/behavior-applicator.d.ts +0 -71
  26. package/dist/core/behavior/behavior-applicator.js +0 -92
  27. package/dist/core/behavior/behavior-context.js +0 -38
  28. package/dist/core/behavior/behavior-registry.js +0 -198
  29. package/dist/core/behavior/behaviors/compute-from.js +0 -84
  30. package/dist/core/behavior/behaviors/copy-from.js +0 -64
  31. package/dist/core/behavior/behaviors/enable-when.js +0 -81
  32. package/dist/core/behavior/behaviors/index.js +0 -11
  33. package/dist/core/behavior/behaviors/reset-when.js +0 -63
  34. package/dist/core/behavior/behaviors/revalidate-when.js +0 -51
  35. package/dist/core/behavior/behaviors/sync-fields.js +0 -66
  36. package/dist/core/behavior/behaviors/transform-value.js +0 -110
  37. package/dist/core/behavior/behaviors/watch-field.js +0 -56
  38. package/dist/core/behavior/compose-behavior.js +0 -166
  39. package/dist/core/behavior/create-field-path.js +0 -69
  40. package/dist/core/behavior/index.js +0 -17
  41. package/dist/core/behavior/types.js +0 -7
  42. package/dist/core/context/form-context-impl.js +0 -37
  43. package/dist/core/factories/index.js +0 -6
  44. package/dist/core/factories/node-factory.js +0 -281
  45. package/dist/core/nodes/array-node.js +0 -534
  46. package/dist/core/nodes/field-node.js +0 -510
  47. package/dist/core/nodes/form-node.js +0 -343
  48. package/dist/core/nodes/group-node/field-registry.d.ts +0 -191
  49. package/dist/core/nodes/group-node/field-registry.js +0 -215
  50. package/dist/core/nodes/group-node/index.d.ts +0 -11
  51. package/dist/core/nodes/group-node/index.js +0 -11
  52. package/dist/core/nodes/group-node/proxy-builder.d.ts +0 -71
  53. package/dist/core/nodes/group-node/proxy-builder.js +0 -161
  54. package/dist/core/nodes/group-node/state-manager.d.ts +0 -184
  55. package/dist/core/nodes/group-node/state-manager.js +0 -265
  56. package/dist/core/nodes/group-node.js +0 -770
  57. package/dist/core/types/deep-schema.js +0 -11
  58. package/dist/core/types/field-path.js +0 -4
  59. package/dist/core/types/form-context.js +0 -25
  60. package/dist/core/types/group-node-proxy.js +0 -31
  61. package/dist/core/types/index.js +0 -4
  62. package/dist/core/types/validation-schema.js +0 -10
  63. package/dist/core/utils/create-form.js +0 -24
  64. package/dist/core/utils/debounce.js +0 -197
  65. package/dist/core/utils/error-handler.js +0 -226
  66. package/dist/core/utils/field-path-navigator.js +0 -374
  67. package/dist/core/utils/index.js +0 -14
  68. package/dist/core/utils/registry-helpers.js +0 -79
  69. package/dist/core/utils/registry-stack.js +0 -86
  70. package/dist/core/utils/resources.js +0 -69
  71. package/dist/core/utils/subscription-manager.js +0 -214
  72. package/dist/core/utils/type-guards.js +0 -169
  73. package/dist/core/validation/core/apply-when.js +0 -41
  74. package/dist/core/validation/core/apply.js +0 -38
  75. package/dist/core/validation/core/index.js +0 -8
  76. package/dist/core/validation/core/validate-async.js +0 -45
  77. package/dist/core/validation/core/validate-tree.js +0 -37
  78. package/dist/core/validation/core/validate.js +0 -38
  79. package/dist/core/validation/field-path.js +0 -147
  80. package/dist/core/validation/index.js +0 -33
  81. package/dist/core/validation/validate-form.js +0 -152
  82. package/dist/core/validation/validation-applicator.js +0 -217
  83. package/dist/core/validation/validation-context.js +0 -75
  84. package/dist/core/validation/validation-registry.js +0 -298
  85. package/dist/core/validation/validators/array-validators.js +0 -86
  86. package/dist/core/validation/validators/date.js +0 -117
  87. package/dist/core/validation/validators/email.js +0 -60
  88. package/dist/core/validation/validators/index.js +0 -14
  89. package/dist/core/validation/validators/max-length.js +0 -60
  90. package/dist/core/validation/validators/max.js +0 -60
  91. package/dist/core/validation/validators/min-length.js +0 -60
  92. package/dist/core/validation/validators/min.js +0 -60
  93. package/dist/core/validation/validators/number.js +0 -90
  94. package/dist/core/validation/validators/pattern.js +0 -62
  95. package/dist/core/validation/validators/phone.js +0 -58
  96. package/dist/core/validation/validators/required.js +0 -69
  97. package/dist/core/validation/validators/url.js +0 -55
  98. package/dist/hooks/useFormControl.js +0 -298
  99. 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. QUICK REFERENCE
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
- | `required`, `min`, `max`, `minLength`, `email`, `validate`, `validateTree` | `@reformer/core/validators` |
11
- | `computeFrom`, `enableWhen`, `disableWhen`, `copyFrom`, `watchField` | `@reformer/core/behaviors` |
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
- validate(path, validator: (value) => ValidationError | null)
31
- validateTree(validator: (ctx) => ValidationError | null)
32
- when(condition: (form) => boolean, validatorsFn: () => void)
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
- computeFrom(sourcePaths[], targetPath, compute: (values) => result)
41
- watchField(path, callback: (value, ctx) => void)
42
- copyFrom(sourcePath, targetPath, options?: { when?, fields?, transform? })
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
- ## 4. ⚠️ COMMON MISTAKES
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
- // WRONG
316
+ // WRONG
77
317
  required(path.email, 'Email is required');
78
318
 
79
- // CORRECT
319
+ // CORRECT
80
320
  required(path.email, { message: 'Email is required' });
81
321
  ```
82
322
 
83
323
  ### Types
84
324
 
85
325
  ```typescript
86
- // WRONG
326
+ // WRONG
87
327
  amount: number | null;
88
328
  [key: string]: unknown;
89
329
 
90
- // CORRECT
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
- // WRONG - different nesting levels
338
+ // WRONG - different nesting levels
99
339
  computeFrom([path.nested.a, path.nested.b], path.root, ...)
100
340
 
101
- // CORRECT - use watchField
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
- // WRONG - types are not in submodules
350
+ // WRONG - types are not in submodules
111
351
  import { ValidationSchemaFn } from '@reformer/core/validators';
112
352
 
113
- // CORRECT - types from main module
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, when } from '@reformer/core/validators';
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
- // CORRECT form type definition
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
+ ```