@reformer/core 5.0.2 → 6.0.0

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 (131) hide show
  1. package/README.md +7 -5
  2. package/dist/behaviors-O9a1Djj9.js +147 -0
  3. package/dist/behaviors.d.ts +131 -4
  4. package/dist/behaviors.js +298 -27
  5. package/dist/core/model/behaviors.d.ts +123 -0
  6. package/dist/core/model/form-model.d.ts +18 -0
  7. package/dist/core/model/index.d.ts +12 -0
  8. package/dist/core/model/types.d.ts +112 -0
  9. package/dist/core/model/validate-model.d.ts +46 -0
  10. package/dist/core/nodes/array-node.d.ts +34 -2
  11. package/dist/core/nodes/group-node.d.ts +19 -71
  12. package/dist/core/nodes/model-array-node.d.ts +71 -0
  13. package/dist/core/types/deep-schema.d.ts +16 -2
  14. package/dist/core/types/form-proxy.d.ts +17 -6
  15. package/dist/core/types/index.d.ts +5 -23
  16. package/dist/core/types/validation-schema.d.ts +15 -66
  17. package/dist/core/utils/create-form.d.ts +24 -0
  18. package/dist/core/utils/derived-registry.d.ts +5 -0
  19. package/dist/core/utils/index.d.ts +4 -4
  20. package/dist/core/utils/signal-node-registry.d.ts +12 -0
  21. package/dist/core/utils/type-guards.d.ts +2 -3
  22. package/dist/core/validation/index.d.ts +5 -11
  23. package/dist/core/validation/validators/email.d.ts +5 -29
  24. package/dist/core/validation/validators/future-date.d.ts +6 -28
  25. package/dist/core/validation/validators/index.d.ts +11 -4
  26. package/dist/core/validation/validators/integer.d.ts +13 -0
  27. package/dist/core/validation/validators/is-date.d.ts +5 -28
  28. package/dist/core/validation/validators/is-number.d.ts +14 -0
  29. package/dist/core/validation/validators/max-age.d.ts +6 -29
  30. package/dist/core/validation/validators/max-date.d.ts +6 -29
  31. package/dist/core/validation/validators/max-length.d.ts +5 -30
  32. package/dist/core/validation/validators/max.d.ts +7 -29
  33. package/dist/core/validation/validators/min-age.d.ts +6 -29
  34. package/dist/core/validation/validators/min-date.d.ts +6 -29
  35. package/dist/core/validation/validators/min-length.d.ts +5 -30
  36. package/dist/core/validation/validators/min.d.ts +7 -29
  37. package/dist/core/validation/validators/multiple-of.d.ts +13 -0
  38. package/dist/core/validation/validators/non-negative.d.ts +13 -0
  39. package/dist/core/validation/validators/non-zero.d.ts +13 -0
  40. package/dist/core/validation/validators/past-date.d.ts +5 -28
  41. package/dist/core/validation/validators/pattern.d.ts +5 -32
  42. package/dist/core/validation/validators/phone.d.ts +9 -19
  43. package/dist/core/validation/validators/required.d.ts +10 -31
  44. package/dist/core/validation/validators/url.d.ts +13 -15
  45. package/dist/index.d.ts +3 -4
  46. package/dist/index.js +1299 -875
  47. package/dist/validators/email.js +7 -9
  48. package/dist/validators/future-date.js +11 -12
  49. package/dist/validators/integer.d.ts +2 -0
  50. package/dist/validators/integer.js +10 -0
  51. package/dist/validators/is-date.js +6 -7
  52. package/dist/validators/is-number.d.ts +2 -0
  53. package/dist/validators/is-number.js +10 -0
  54. package/dist/validators/max-age.js +11 -12
  55. package/dist/validators/max-date.js +11 -12
  56. package/dist/validators/max-length.js +12 -8
  57. package/dist/validators/max.js +5 -6
  58. package/dist/validators/min-age.js +10 -11
  59. package/dist/validators/min-date.js +12 -13
  60. package/dist/validators/min-length.js +12 -8
  61. package/dist/validators/min.js +6 -7
  62. package/dist/validators/multiple-of.d.ts +2 -0
  63. package/dist/validators/multiple-of.js +10 -0
  64. package/dist/validators/non-negative.d.ts +2 -0
  65. package/dist/validators/non-negative.js +10 -0
  66. package/dist/validators/non-zero.d.ts +2 -0
  67. package/dist/validators/non-zero.js +10 -0
  68. package/dist/validators/past-date.js +11 -12
  69. package/dist/validators/pattern.js +6 -7
  70. package/dist/validators/phone.js +14 -32
  71. package/dist/validators/required.js +9 -10
  72. package/dist/validators/url.js +19 -16
  73. package/dist/validators.js +67 -54
  74. package/llms.txt +959 -2584
  75. package/package.json +19 -35
  76. package/dist/behaviors/compute-from.d.ts +0 -2
  77. package/dist/behaviors/compute-from.js +0 -31
  78. package/dist/behaviors/copy-from.d.ts +0 -2
  79. package/dist/behaviors/copy-from.js +0 -29
  80. package/dist/behaviors/enable-when.d.ts +0 -2
  81. package/dist/behaviors/enable-when.js +0 -25
  82. package/dist/behaviors/reset-when.d.ts +0 -2
  83. package/dist/behaviors/reset-when.js +0 -24
  84. package/dist/behaviors/revalidate-when.d.ts +0 -2
  85. package/dist/behaviors/revalidate-when.js +0 -18
  86. package/dist/behaviors/sync-fields.d.ts +0 -2
  87. package/dist/behaviors/sync-fields.js +0 -41
  88. package/dist/behaviors/transform-value.d.ts +0 -2
  89. package/dist/behaviors/transform-value.js +0 -45
  90. package/dist/behaviors/watch-field.d.ts +0 -2
  91. package/dist/behaviors/watch-field.js +0 -21
  92. package/dist/core/behavior/behavior-context.d.ts +0 -47
  93. package/dist/core/behavior/behavior-registry.d.ts +0 -85
  94. package/dist/core/behavior/behaviors/compute-from.d.ts +0 -70
  95. package/dist/core/behavior/behaviors/copy-from.d.ts +0 -56
  96. package/dist/core/behavior/behaviors/enable-when.d.ts +0 -118
  97. package/dist/core/behavior/behaviors/index.d.ts +0 -11
  98. package/dist/core/behavior/behaviors/reset-when.d.ts +0 -64
  99. package/dist/core/behavior/behaviors/revalidate-when.d.ts +0 -53
  100. package/dist/core/behavior/behaviors/sync-fields.d.ts +0 -48
  101. package/dist/core/behavior/behaviors/transform-value.d.ts +0 -192
  102. package/dist/core/behavior/behaviors/watch-field.d.ts +0 -80
  103. package/dist/core/behavior/compose-behavior.d.ts +0 -96
  104. package/dist/core/behavior/index.d.ts +0 -11
  105. package/dist/core/behavior/types.d.ts +0 -146
  106. package/dist/core/types/field-path.d.ts +0 -42
  107. package/dist/core/types/form-context.d.ts +0 -89
  108. package/dist/core/utils/field-path-navigator.d.ts +0 -240
  109. package/dist/core/utils/field-path.d.ts +0 -60
  110. package/dist/core/utils/registry-helpers.d.ts +0 -43
  111. package/dist/core/validation/core/apply-when.d.ts +0 -22
  112. package/dist/core/validation/core/apply.d.ts +0 -52
  113. package/dist/core/validation/core/index.d.ts +0 -8
  114. package/dist/core/validation/core/validate-async.d.ts +0 -36
  115. package/dist/core/validation/core/validate-tree.d.ts +0 -35
  116. package/dist/core/validation/core/validate.d.ts +0 -26
  117. package/dist/core/validation/validate-form.d.ts +0 -48
  118. package/dist/core/validation/validation-applicator.d.ts +0 -70
  119. package/dist/core/validation/validation-context.d.ts +0 -86
  120. package/dist/core/validation/validation-registry.d.ts +0 -142
  121. package/dist/core/validation/validators/array-validators.d.ts +0 -53
  122. package/dist/core/validation/validators/number.d.ts +0 -31
  123. package/dist/field-path-DuKdGcIE.js +0 -66
  124. package/dist/hooks/useHiddenCondition.d.ts +0 -25
  125. package/dist/index-D25LsbRm.js +0 -73
  126. package/dist/registry-helpers-Bv_BJ1s-.js +0 -615
  127. package/dist/safe-effect-Dh8uw81c.js +0 -20
  128. package/dist/validate-C3XiA_zf.js +0 -10
  129. package/dist/validators/number.d.ts +0 -2
  130. package/dist/validators/number.js +0 -35
  131. package/dist/validators-BGsNOgT1.js +0 -207
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
  # AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.
3
3
 
4
4
  > Reactive form state management library for React with signals-based architecture
5
- > Package: @reformer/core • Version: 1.1.0
5
+ > Package: @reformer/core • Version: 5.0.2
6
6
 
7
7
  ## Table of Contents
8
8
  - 01-api-reference.md — api-reference
@@ -42,18 +42,18 @@
42
42
 
43
43
  ### Imports (CRITICALLY IMPORTANT)
44
44
 
45
- | What | Where |
46
- | ------------------------------------------------------------------------------- | --------------------------- |
47
- | `createForm`, `useFormControl`, `useFormControlValue`, `validateForm` | `@reformer/core` |
45
+ | What | Where |
46
+ | ------------------------------------------------------------------------------------------- | --------------------------- |
47
+ | `createForm`, `useFormControl`, `useFormControlValue`, `validateForm` | `@reformer/core` |
48
48
  | `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `FormProxy`, `FieldNode` | `@reformer/core` |
49
- | `FormSchema`, `FieldConfig`, `ArrayNode` | `@reformer/core` |
50
- | `required`, `min`, `max`, `minLength`, `maxLength`, `email` | `@reformer/core/validators` |
51
- | `pattern`, `url`, `phone`, `number`, `date` | `@reformer/core/validators` |
52
- | `validate`, `validateAsync`, `validateTree`, `applyWhen` | `@reformer/core/validators` |
53
- | `notEmpty`, `validateItems` | `@reformer/core/validators` |
54
- | `computeFrom`, `enableWhen`, `disableWhen`, `watchField`, `copyFrom` | `@reformer/core/behaviors` |
55
- | `resetWhen`, `revalidateWhen`, `syncFields` | `@reformer/core/behaviors` |
56
- | `transformValue`, `transformers` | `@reformer/core/behaviors` |
49
+ | `FormSchema`, `FieldConfig`, `ArrayNode` | `@reformer/core` |
50
+ | `required`, `min`, `max`, `minLength`, `maxLength`, `email` | `@reformer/core/validators` |
51
+ | `pattern`, `url`, `phone`, `isNumber`, `integer`, `multipleOf`, `nonNegative`, `nonZero` | `@reformer/core/validators` |
52
+ | `validate`, `validateAsync`, `applyWhen`, `apply` | `@reformer/core/validators` |
53
+ | `notEmpty`, `validateItems` | `@reformer/core/validators` |
54
+ | `computeFrom`, `enableWhen`, `disableWhen`, `watchField`, `copyFrom` | `@reformer/core/behaviors` |
55
+ | `resetWhen`, `revalidateWhen`, `syncFields` | `@reformer/core/behaviors` |
56
+ | `transformValue`, `transformers` | `@reformer/core/behaviors` |
57
57
 
58
58
  ### Type Values
59
59
 
@@ -63,10 +63,10 @@
63
63
 
64
64
  ### React Hooks Comparison (CRITICALLY IMPORTANT)
65
65
 
66
- | Hook | Return Type | Subscribes To | Use Case |
67
- | ---------------------------- | ------------------------------------------- | ----------------- | ----------------------------- |
68
- | `useFormControl(field)` | `{ value, errors, disabled, touched, ... }` | All signals | Full field state, form inputs |
69
- | `useFormControlValue(field)` | `T` (value directly) | Only value signal | Conditional rendering |
66
+ | Hook | Return Type | Subscribes To | Use Case |
67
+ |------|-------------|---------------|----------|
68
+ | `useFormControl(field)` | `{ value, errors, disabled, touched, ... }` | All signals | Full field state, form inputs |
69
+ | `useFormControlValue(field)` | `T` (value directly) | Only value signal | Conditional rendering |
70
70
 
71
71
  **CRITICAL**: Do NOT destructure `useFormControlValue`! It returns `T` directly, NOT `{ value: T }`.
72
72
 
@@ -188,26 +188,39 @@ const schema: FormSchema<{ properties: PropertyItem[] }> = {
188
188
 
189
189
  ### Validators
190
190
 
191
- ```typescript
192
- // Basic validators
193
- required(path, options?: { message?: string })
194
- min(path, value: number, options?: { message?: string })
195
- max(path, value: number, options?: { message?: string })
196
- minLength(path, length: number, options?: { message?: string })
197
- maxLength(path, length: number, options?: { message?: string })
198
- email(path, options?: { message?: string })
199
-
200
- // Additional validators
201
- pattern(path, regex: RegExp, options?: { message?: string })
202
- url(path, options?: { message?: string })
203
- phone(path, options?: { message?: string; format?: PhoneFormat })
204
- number(path, options?: { message?: string })
205
- date(path, options?: { message?: string; minAge?: number; maxAge?: number; noFuture?: boolean; noPast?: boolean })
206
-
207
- // Custom validators
208
- validate(path, validator: (value, ctx) => ValidationError | null)
209
- validateAsync(path, validator: async (value, ctx) => ValidationError | null)
210
- validateTree(validator: (ctx) => ValidationError | null, options?: { targetField?: string })
191
+ Operators register validators in the schema. Validator factories return pure
192
+ `Validator<TForm, TField>` functions of shape `(value, control, root) => ValidationError | null`.
193
+
194
+ ```typescript
195
+ // Operators (register validators in schema)
196
+ validate(path, validator: Validator<TForm, TField>, options?: { message?: string })
197
+ validateAsync(path, validator: AsyncValidator<TForm, TField>, options?: { message?: string; debounce?: number })
198
+
199
+ // Validator factories (return Validator). Pass to validate().
200
+ required(options?: { message?: string })
201
+ min(value: number, options?: { message?: string })
202
+ max(value: number, options?: { message?: string })
203
+ minLength(length: number, options?: { message?: string })
204
+ maxLength(length: number, options?: { message?: string })
205
+ email(options?: { message?: string })
206
+ pattern(regex: RegExp, options?: { message?: string })
207
+ url(options?: { message?: string; requireProtocol?: boolean })
208
+ phone(options?: { message?: string; format?: PhoneFormat })
209
+ // Number validator factories (atomic compose via several validate() calls)
210
+ isNumber(options?: { message?: string })
211
+ integer(options?: { message?: string })
212
+ multipleOf(divisor: number, options?: { message?: string })
213
+ nonNegative(options?: { message?: string }) // value >= 0
214
+ nonZero(options?: { message?: string }) // value !== 0
215
+ date(options?: { message?: string; minAge?: number; maxAge?: number; noFuture?: boolean; noPast?: boolean })
216
+ notEmpty(options?: { message?: string })
217
+
218
+ // Usage: pass a factory result to validate()
219
+ validate(path.email, required());
220
+ validate(path.age, min(18));
221
+ validate(path.amount, integer());
222
+ validate(path.amount, min(0));
223
+ validate(path.amount, max(1000));
211
224
 
212
225
  // Conditional validation (3 arguments!)
213
226
  applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
@@ -220,8 +233,8 @@ applyWhen(
220
233
  path.loanType, // 1st: field to watch
221
234
  (type) => type === 'mortgage', // 2nd: condition on field value
222
235
  (p) => { // 3rd: validators to apply
223
- required(p.propertyValue);
224
- min(p.propertyValue, 100000);
236
+ validate(p.propertyValue, required());
237
+ validate(p.propertyValue, min(100000));
225
238
  }
226
239
  );
227
240
 
@@ -230,8 +243,8 @@ applyWhen(
230
243
  path.address.country,
231
244
  (country) => country === 'US',
232
245
  (p) => {
233
- required(p.address.state);
234
- pattern(p.address.zip, /^\d{5}(-\d{4})?$/);
246
+ validate(p.address.state, required());
247
+ validate(p.address.zip, pattern(/^\d{5}(-\d{4})?$/));
235
248
  }
236
249
  );
237
250
 
@@ -240,19 +253,18 @@ applyWhen(
240
253
  path.hasInsurance,
241
254
  (has) => has === true,
242
255
  (p) => {
243
- required(p.insuranceCompany);
244
- required(p.policyNumber);
256
+ validate(p.insuranceCompany, required());
257
+ validate(p.policyNumber, required());
245
258
  }
246
259
  );
247
260
 
248
261
  // WRONG - only 2 arguments (React Hook Form pattern)
249
262
  applyWhen(
250
263
  (form) => form.loanType === 'mortgage', // WRONG!
251
- () => { required(path.propertyValue); }
264
+ () => { validate(path.propertyValue, required()); }
252
265
  );
253
266
 
254
- // Array validators
255
- notEmpty(path, options?: { message?: string })
267
+ // Array item schema operator
256
268
  validateItems(arrayPath, itemValidatorsFn: (itemPath) => void)
257
269
  ```
258
270
 
@@ -426,26 +438,29 @@ const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
426
438
 
427
439
  ### Validation Priority (IMPORTANT)
428
440
 
429
- **Always prefer built-in validators over custom ones:**
441
+ **Operators register validators; built-in factories return validators. Pass factories to `validate()`.**
442
+
443
+ Operators: `validate`, `validateAsync`, `applyWhen`, `apply`, `validateItems`.
444
+ Factories (return `Validator<TForm, TField>`): `required`, `email`, `min`, `max`, `minLength`, `maxLength`,
445
+ `pattern`, `url`, `phone`, `number`, `date`, `notEmpty`.
430
446
 
431
447
  ```typescript
432
- // 1. BEST: Use built-in validators when available
433
- required(path.email);
434
- email(path.email);
435
- min(path.age, 18);
436
- minLength(path.password, 8);
437
- pattern(path.phone, /^\+7\d{10}$/);
448
+ // 1. BEST: Pass built-in factories to validate()
449
+ validate(path.email, required());
450
+ validate(path.email, email());
451
+ validate(path.age, min(18));
452
+ validate(path.password, minLength(8));
453
+ validate(path.phone, pattern(/^\+7\d{10}$/));
438
454
 
439
- // 2. GOOD: Use validate() only when no built-in validator exists
440
- validate(path.customField, (value, ctx) => {
441
- // Custom logic that can't be expressed with built-in validators
455
+ // 2. GOOD: Custom validator when no factory fits. Signature: (value, control, root).
456
+ validate(path.customField, (value, control, root) => {
442
457
  if (customCondition(value)) {
443
458
  return { code: 'custom', message: 'Custom error' };
444
459
  }
445
460
  return null;
446
461
  });
447
462
 
448
- // 3. WRONG: Don't recreate built-in validators
463
+ // 3. WRONG: Don't recreate built-in validators inline
449
464
  validate(path.email, (value) => {
450
465
  if (!value) return { code: 'required', message: 'Required' }; // Use required() instead!
451
466
  if (!value.includes('@')) return { code: 'email', message: 'Invalid' }; // Use email() instead!
@@ -532,6 +547,68 @@ const behavior: BehaviorSchemaFn<LoanForm> = (path) => {
532
547
 
533
548
  Reference patterns: `complex-multy-step-form/schemas/credit-application-behavior.ts`.
534
549
 
550
+ ### Extracting Nested Rules
551
+
552
+ When the body of `applyWhen` / `validate` grows beyond a few lines,
553
+ extract it to a **named top-level function** typed with one of the public types from
554
+ `@reformer/core`:
555
+
556
+ - `ValidationSchemaFn<TForm>` — sub-schema passed to `applyWhen` or `apply`.
557
+ - `Validator<TForm, TField>` / `AsyncValidator<TForm, TField>` — field validator passed to `validate` / `validateAsync`. Cross-field правила пишутся в той же сигнатуре — `(value, control, root) => …` — и навешиваются на конкретное поле-носитель ошибки.
558
+
559
+ This keeps the schema body flat (reads like a table of contents) and surfaces the
560
+ **intent** of each rule via a meaningful name.
561
+
562
+ ```typescript
563
+ import type { ValidationSchemaFn, Validator } from '@reformer/core';
564
+
565
+ // 1. Cross-field rule — extracted Validator (reads other fields via `root`)
566
+ const initialPaymentVsPropertyValue: Validator<CreditApplicationForm, unknown> = (
567
+ _value,
568
+ _control,
569
+ root
570
+ ) => {
571
+ const form = root.getValue();
572
+ if (form.initialPayment && form.propertyValue && form.initialPayment > form.propertyValue) {
573
+ return { code: 'initialPaymentTooHigh', message: 'Взнос не может превышать стоимость' };
574
+ }
575
+ return null;
576
+ };
577
+
578
+ // 2. Custom field validator — extracted Validator
579
+ const validateAdultAge: Validator<CreditApplicationForm, string | undefined> = (value) => {
580
+ if (!value) return null;
581
+ const age = new Date().getFullYear() - new Date(value).getFullYear();
582
+ if (age < 18) return { code: 'tooYoung', message: 'Минимум 18 лет' };
583
+ return null;
584
+ };
585
+
586
+ // 3. Nested schema for applyWhen — extracted ValidationSchemaFn
587
+ const mortgageFieldsRules: ValidationSchemaFn<CreditApplicationForm> = (path) => {
588
+ validate(path.propertyValue, required());
589
+ validate(path.propertyValue, min(1000000));
590
+ validate(path.initialPayment, required());
591
+ validate(path.initialPayment, initialPaymentVsPropertyValue);
592
+ };
593
+
594
+ // 4. Main schema — flat, reads as a table of contents
595
+ export const basicInfoValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
596
+ validate(path.loanType, required());
597
+ validate(path.personalData.birthDate, validateAdultAge);
598
+ applyWhen(path.loanType, (type) => type === 'mortgage', mortgageFieldsRules);
599
+ };
600
+ ```
601
+
602
+ **Naming convention** (camelCase, semantic — not echoing the operator name):
603
+
604
+ - `applyWhen` sub-schema → describes the conditional branch: `mortgageFieldsRules`, `employedFieldsRules`.
605
+ - Cross-field `Validator` → describes the invariant: `initialPaymentVsPropertyValue`, `paymentToIncomeUnderHalf`.
606
+ - Field-level `Validator` → describes the check: `validateAdultAge`, `validatePasswordsMatch`.
607
+
608
+ **When to extract.** Inline lambdas are fine for short one-liners
609
+ (`applyWhen(path.x, (v) => v === 'mortgage', mortgageFieldsRules)` — the condition stays inline).
610
+ Extract whenever the body spans more than ~3 lines or contains nested rules.
611
+
535
612
  ## 6. 4. COMMON MISTAKES
536
613
 
537
614
  ### TSC overload-resolution error: `'form' does not exist in FormSchema<T>`
@@ -1274,19 +1351,32 @@ enableWhen(path.vehicle.vin, (form) => form.insuranceType === 'casco', { resetOn
1274
1351
 
1275
1352
  See `22-cycle-detection.md` for complete pattern.
1276
1353
 
1277
- ### validateTree Typing
1354
+ ### Cross-field валидация — через `validate` + `root`
1278
1355
 
1279
1356
  ```typescript
1280
- // WRONG - implicit any
1281
- validateTree((ctx) => { ... });
1357
+ // Validator signature: (value, control, root) => ValidationError | null
1358
+ // Cross-field правило вешается на ПОЛЕ-НОСИТЕЛЬ ошибки. Соседние поля читаются через `root`.
1282
1359
 
1283
- // CORRECT - explicit typing
1284
- validateTree((ctx: { form: MyForm }) => {
1285
- if (ctx.form.field1 > ctx.form.field2) {
1360
+ // CORRECT ошибка ложится на path.field1
1361
+ validate(path.field1, (_value, _control, root) => {
1362
+ const form = root.getValue(); // typed as TForm
1363
+ if (form.field1 > form.field2) {
1286
1364
  return { code: 'error', message: 'Invalid' };
1287
1365
  }
1288
1366
  return null;
1289
1367
  });
1368
+
1369
+ // CORRECT — те же поля через reactive-доступ
1370
+ validate(path.address.city, (value, _control, root) => {
1371
+ if (!value && root.address.region.value.value) {
1372
+ return { code: 'cityRequired', message: 'City required when region is set' };
1373
+ }
1374
+ return null;
1375
+ });
1376
+
1377
+ // WRONG - old API removed
1378
+ validateGroup(scopePath, (scope, root) => { ... }); // validateGroup убран
1379
+ validateTree((ctx) => { ... }); // validateTree no longer exists
1290
1380
  ```
1291
1381
 
1292
1382
  ## 16. 14. PROJECT STRUCTURE (COLOCATION)
@@ -1787,18 +1877,18 @@ The "one watcher per trigger" cycle-prevention rule means **never register two w
1787
1877
 
1788
1878
  ```typescript
1789
1879
  // WRONG - bracket notation does NOT work!
1790
- const first = form.items[0]; // undefined or error
1791
- const second = form.items[1]; // undefined or error
1880
+ const first = form.items[0]; // undefined or error
1881
+ const second = form.items[1]; // undefined or error
1792
1882
 
1793
1883
  // CORRECT - use .at() method
1794
- const first = form.items.at(0); // FormProxy<ItemType> | undefined
1795
- const second = form.items.at(1); // FormProxy<ItemType> | undefined
1884
+ const first = form.items.at(0); // FormProxy<ItemType> | undefined
1885
+ const second = form.items.at(1); // FormProxy<ItemType> | undefined
1796
1886
 
1797
1887
  // CORRECT - iterate with map (most common pattern)
1798
1888
  form.items.map((item, index) => {
1799
1889
  // item is fully typed GroupNode
1800
1890
  item.name.setValue('New Name');
1801
- item.price.value.value; // read value
1891
+ item.price.value.value; // read value
1802
1892
  });
1803
1893
  ```
1804
1894
 
@@ -1841,7 +1931,9 @@ function ItemsList({ form }: { form: FormProxy<MyForm> }) {
1841
1931
 
1842
1932
  {length === 0 && <p>No items yet</p>}
1843
1933
 
1844
- <button onClick={() => form.items.push({ name: '', price: 0 })}>Add Item</button>
1934
+ <button onClick={() => form.items.push({ name: '', price: 0 })}>
1935
+ Add Item
1936
+ </button>
1845
1937
  </div>
1846
1938
  );
1847
1939
  }
@@ -1849,35 +1941,35 @@ function ItemsList({ form }: { form: FormProxy<MyForm> }) {
1849
1941
 
1850
1942
  ### Array Cross-Validation
1851
1943
 
1944
+ Cross-field правила по массиву пишутся как обычный `Validator`, вешаются на сам массив
1945
+ (или на любое поле-носитель ошибки), а данные читаются через `root`.
1946
+
1852
1947
  ```typescript
1853
1948
  // Validate uniqueness across array items
1854
- validateTree(
1855
- (ctx: { form: MyForm }) => {
1856
- const items = ctx.form.items;
1857
- const names = items.map((item) => item.name.value.value);
1858
- const uniqueNames = new Set(names);
1859
-
1860
- if (names.length !== uniqueNames.size) {
1861
- return { code: 'duplicate', message: 'Item names must be unique' };
1862
- }
1863
- return null;
1864
- },
1865
- { targetField: 'items' }
1866
- );
1949
+ validate(path.items, (_value, _control, root) => {
1950
+ const items = root.items;
1951
+ const names = items.map((item) => item.name.value.value);
1952
+ const uniqueNames = new Set(names);
1953
+
1954
+ if (names.length !== uniqueNames.size) {
1955
+ return { code: 'duplicate', message: 'Item names must be unique' };
1956
+ }
1957
+ return null;
1958
+ });
1867
1959
 
1868
1960
  // Validate sum of percentages
1869
- validateTree(
1870
- (ctx: { form: MyForm }) => {
1871
- const items = ctx.form.items;
1872
- const totalPercent = items.reduce((sum, item) => sum + (item.percentage.value.value || 0), 0);
1961
+ validate(path.items, (_value, _control, root) => {
1962
+ const items = root.items;
1963
+ const totalPercent = items.reduce(
1964
+ (sum, item) => sum + (item.percentage.value.value || 0),
1965
+ 0
1966
+ );
1873
1967
 
1874
- if (Math.abs(totalPercent - 100) > 0.01) {
1875
- return { code: 'invalid_total', message: 'Percentages must sum to 100%' };
1876
- }
1877
- return null;
1878
- },
1879
- { targetField: 'items' }
1880
- );
1968
+ if (Math.abs(totalPercent - 100) > 0.01) {
1969
+ return { code: 'invalid_total', message: 'Percentages must sum to 100%' };
1970
+ }
1971
+ return null;
1972
+ });
1881
1973
  ```
1882
1974
 
1883
1975
  ## 22. Cycle Detection Prevention Checklist
@@ -3954,106 +4046,6 @@ export type AnyFunction = (...args: never[]) => unknown;
3954
4046
 
3955
4047
  _Source: src/core/types/index.ts_
3956
4048
 
3957
- ### apply
3958
-
3959
- **Kind:** `function`
3960
-
3961
- Применить validation схему к вложенному полю или полям
3962
-
3963
- Поддерживает:
3964
- - Одно поле или массив полей
3965
- - Одну схему или массив схем
3966
- - Все комбинации (поле + схема, поле + схемы, поля + схема, поля + схемы)
3967
-
3968
- **Signature:**
3969
- ```typescript
3970
- export function apply<TForm, TField>(
3971
- fields:
3972
- | FieldPathNode<TForm, TField>
3973
- | Array<FieldPathNode<TForm, TField> | undefined>
3974
- | undefined,
3975
- validationSchemas: ValidationSchemaFn<TField> | Array<ValidationSchemaFn<TField>>
3976
- ): void;
3977
- ```
3978
-
3979
- **Parameters:**
3980
- - `fields` — - Одно поле или массив полей
3981
- - `validationSchemas` — - Одна схема или массив схем
3982
-
3983
- **Examples:**
3984
-
3985
- ```typescript
3986
- // Одна схема к одному полю
3987
- apply(path.registrationAddress, addressValidation);
3988
-
3989
- // Одна схема к нескольким полям
3990
- apply([path.homeAddress, path.workAddress], addressValidation);
3991
-
3992
- // Несколько схем к одному полю
3993
- apply(path.email, [emailValidation, uniqueEmailValidation]);
3994
-
3995
- // Несколько схем к нескольким полям
3996
- apply(
3997
- [path.email, path.confirmEmail],
3998
- [emailValidation, matchValidation]
3999
- );
4000
- ```
4001
-
4002
- _Source: src/core/validation/core/apply.ts_
4003
-
4004
- ### applyWhen
4005
-
4006
- **Kind:** `function`
4007
-
4008
- Условное применение behavior схем (аналог applyWhen из validation API)
4009
-
4010
- ⚠️ ВАЖНО: Эта функция НЕ создаёт новые behaviors при каждом изменении условия!
4011
- Вместо этого behaviors регистрируются ОДИН РАЗ при первом вызове и затем
4012
- просто не выполняются, если условие не выполнено.
4013
-
4014
- Это отличается от старой реализации, которая создавала утечку памяти,
4015
- регистрируя behaviors при каждом изменении conditionField.
4016
-
4017
- **Signature:**
4018
- ```typescript
4019
- export function applyWhen<TForm extends FormFields, TValue extends FormValue>(
4020
- conditionField: FieldPathNode<TForm, TValue> | undefined,
4021
- condition: (value: TValue) => boolean,
4022
- callback: (path: FieldPath<TForm>) => void
4023
- ): void
4024
- ```
4025
-
4026
- **Parameters:**
4027
- - `conditionField` — - Поле для проверки условия
4028
- - `condition` — - Функция проверки условия
4029
- - `callback` — - Callback для применения behavior схем
4030
-
4031
- **Examples:**
4032
-
4033
- ```typescript
4034
- // Применить addressBehavior только когда sameAsRegistration === false
4035
- applyWhen(
4036
- path.sameAsRegistration,
4037
- (value) => value === false,
4038
- (path) => {
4039
- apply(path.residenceAddress, addressBehavior);
4040
- }
4041
- );
4042
-
4043
- // Или с прямым использованием path
4044
- applyWhen(
4045
- path.hasProperty,
4046
- (value) => value === true,
4047
- (path) => {
4048
- apply(path.properties, propertyBehavior);
4049
- // Можно применить несколько схем
4050
- apply([path.properties, path.items], arrayBehavior);
4051
- }
4052
- );
4053
- ```
4054
-
4055
- _Source: src/core/behavior/compose-behavior.ts_
4056
-
4057
4049
  ### ArrayConfig
4058
4050
 
4059
4051
  **Kind:** `interface`
@@ -4227,7 +4219,7 @@ ArrayNode - массив форм с реактивным состоянием
4227
4219
 
4228
4220
  **Signature:**
4229
4221
  ```typescript
4230
- export class ArrayNode<T extends FormFields> extends FormNode<T[]> {
4222
+ export class ArrayNode<T extends object> extends FormNode<T[]> {
4231
4223
  // ============================================================================
4232
4224
  // Приватные поля
4233
4225
  // ============================================================================ /* … */ }
@@ -4265,6 +4257,25 @@ export interface ArrayNodeLike {
4265
4257
 
4266
4258
  _Source: src/core/types/index.ts_
4267
4259
 
4260
+ ### AsyncValidator
4261
+
4262
+ **Kind:** `type`
4263
+
4264
+ Чистый асинхронный валидатор поля.
4265
+
4266
+ Регистрируется через `validateAsync(path, validator, { debounce })`.
4267
+
4268
+ **Signature:**
4269
+ ```typescript
4270
+ export type AsyncValidator<TForm, TField> = (
4271
+ value: TField,
4272
+ control: FormProxy<TField>,
4273
+ root: FormProxy<TForm>
4274
+ ) => Promise<ValidationError | null>;
4275
+ ```
4276
+
4277
+ _Source: src/core/types/validation-schema.ts_
4278
+
4268
4279
  ### AsyncValidatorFn
4269
4280
 
4270
4281
  **Kind:** `type`
@@ -4324,697 +4335,258 @@ export interface AsyncValidatorOptions {
4324
4335
 
4325
4336
  _Source: src/core/types/index.ts_
4326
4337
 
4327
- ### BehaviorContext
4338
+ ### BehaviorCleanup
4328
4339
 
4329
4340
  **Kind:** `type`
4330
4341
 
4331
- Контекст для behavior callback функций
4332
- Алиас для FormContext
4342
+ Функция отписки от behavior-эффекта.
4333
4343
 
4334
4344
  **Signature:**
4335
4345
  ```typescript
4336
- export type BehaviorContext<TForm> = FormContext<TForm>;
4337
- ```
4338
-
4339
- **Examples:**
4340
-
4341
- ```typescript
4342
- watchField(path.country, async (country, ctx) => {
4343
- // Прямой типизированный доступ к полям
4344
- const cities = await fetchCities(country);
4345
- ctx.form.city.updateComponentProps({ options: cities });
4346
-
4347
- // Безопасная установка значения (без циклов)
4348
- ctx.setFieldValue('city', null);
4349
- });
4346
+ export type BehaviorCleanup = () => void;
4350
4347
  ```
4351
4348
 
4352
- _Source: src/core/behavior/types.ts_
4349
+ _Source: src/core/model/behaviors.ts_
4353
4350
 
4354
- ### BehaviorContextImpl
4351
+ ### computeFrom
4355
4352
 
4356
- **Kind:** `class`
4353
+ **Kind:** `function`
4357
4354
 
4358
- Реализация {@link FormContext} для behaviors. Создаётся фреймворком и передаётся
4359
- в callback'и behaviors (`watchField`, `computeFrom`, …) — напрямую инстанцировать
4360
- не нужно.
4355
+ Вычисляемое поле: `target = fn(...sourceValues)` при изменении источников.
4361
4356
 
4362
4357
  **Signature:**
4363
4358
  ```typescript
4364
- export class BehaviorContextImpl<TForm> implements FormContext<TForm> {
4365
- /**
4366
- * Форма с типизированным Proxy-доступом к полям
4367
- */ /* */ }
4359
+ export function computeFrom<R>(
4360
+ sources: ReadonlySignal<any>[],
4361
+ target: Signal<R>,
4362
+ fn: (...values: any[]) => R,
4363
+ options?: { when?: (...values: any[]) => boolean }
4364
+ ): BehaviorCleanup
4368
4365
  ```
4369
4366
 
4367
+ **Parameters:**
4368
+ - `sources` — Сигналы-источники (`model.$.a`, `model.$.b`).
4369
+ - `target` — Сигнал-цель (`model.$.total`).
4370
+ - `fn` — Функция вычисления значения.
4371
+
4372
+ **Returns:** Cleanup для отписки.
4373
+
4370
4374
  **Examples:**
4371
4375
 
4372
4376
  ```typescript
4373
- import { watchField } from '@reformer/core/behaviors/watch-field';
4374
-
4375
- watchField(path.country, (value, ctx) => {
4376
- // ctx — экземпляр BehaviorContextImpl
4377
- ctx.setFieldValue(path.city, '');
4378
- });
4377
+ computeFrom([model.$.price, model.$.qty], model.$.total, (price, qty) => price * qty);
4379
4378
  ```
4380
4379
 
4381
- _Source: src/core/behavior/behavior-context.ts_
4380
+ _Source: src/core/model/behaviors.ts_
4382
4381
 
4383
- ### BehaviorHandlerFn
4382
+ ### ConditionFn
4384
4383
 
4385
4384
  **Kind:** `type`
4386
4385
 
4387
- Функция-handler для behavior
4388
-
4389
- Создает effect подписку для реактивного поведения формы.
4386
+ Функция условия для applyWhen.
4390
4387
 
4391
4388
  **Signature:**
4392
4389
  ```typescript
4393
- export type BehaviorHandlerFn<TForm> = (
4394
- form: GroupNode<TForm>,
4395
- context: BehaviorContext<TForm>,
4396
- withDebounce: (callback: () => void) => void
4397
- ) => (() => void) | null;
4390
+ export type ConditionFn<T> = (value: T) => boolean;
4398
4391
  ```
4399
4392
 
4400
- **Parameters:**
4401
- - `form` — - Корневой узел формы (GroupNode)
4402
- - `context` — - Контекст для работы с формой
4403
- - `withDebounce` — - Функция-обертка для debounce
4393
+ _Source: src/core/types/validation-schema.ts_
4404
4394
 
4405
- **Returns:** Функция cleanup для отписки от effect или null
4395
+ ### ConfigWithSchema
4406
4396
 
4407
- **Examples:**
4397
+ **Kind:** `interface`
4408
4398
 
4409
- ```typescript
4410
- const handler: BehaviorHandlerFn<MyForm> = (form, context, withDebounce) => {
4411
- const sourceNode = form.getFieldByPath('email');
4399
+ Конфиг с полем schema (для ArrayConfig)
4412
4400
 
4413
- return effect(() => {
4414
- const value = sourceNode.value.value;
4415
- withDebounce(() => {
4416
- // Логика behavior
4417
- });
4418
- });
4419
- };
4401
+ **Signature:**
4402
+ ```typescript
4403
+ export interface ConfigWithSchema {
4404
+ schema: unknown;
4405
+ initialItems?: unknown[];
4406
+ }
4420
4407
  ```
4421
4408
 
4422
- _Source: src/core/behavior/types.ts_
4409
+ _Source: src/core/types/index.ts_
4423
4410
 
4424
- ### BehaviorOptions
4411
+ ### ConfigWithValue
4425
4412
 
4426
4413
  **Kind:** `interface`
4427
4414
 
4428
- Общие опции для behavior
4415
+ Конфиг с полем value (для извлечения значений)
4429
4416
 
4430
4417
  **Signature:**
4431
4418
  ```typescript
4432
- export interface BehaviorOptions {
4433
- /** Debounce в миллисекундах */
4434
- debounce?: number;
4419
+ export interface ConfigWithValue {
4420
+ value: unknown;
4435
4421
  }
4436
4422
  ```
4437
4423
 
4438
- _Source: src/core/behavior/types.ts_
4439
-
4440
- ### BehaviorRegistry
4441
-
4442
- **Kind:** `class`
4424
+ _Source: src/core/types/index.ts_
4443
4425
 
4444
- Реестр behaviors для формы
4426
+ ### copyFrom
4445
4427
 
4446
- Каждый экземпляр GroupNode создает собственный реестр (композиция).
4447
- Устраняет race conditions и изолирует формы друг от друга.
4428
+ **Kind:** `function`
4448
4429
 
4449
- Наследует AbstractRegistry для унификации:
4450
- - Управления global stack
4451
- - Template methods begin/end registration
4430
+ Копирование значения `source → target` (опционально по условию/с трансформом).
4452
4431
 
4453
4432
  **Signature:**
4454
4433
  ```typescript
4455
- export class BehaviorRegistry extends AbstractRegistry<RegisteredBehavior> {
4456
- /**
4457
- * Получить текущий активный реестр из context stack
4458
- *
4459
- * @returns Текущий активный реестр или null
4460
- *
4461
- * @example
4462
- * ```typescript
4463
- * // В schema-behaviors.ts
4464
- * export function copyFrom(...) {
4465
- * const registry = BehaviorRegistry.getCurrent();
4466
- * if (registry) {
4467
- * registry.register({ ... });
4468
- * }
4469
- * }
4470
- * ```
4471
- */ /* … */ }
4434
+ export function copyFrom<T>(
4435
+ source: ReadonlySignal<T>,
4436
+ target: Signal<T>,
4437
+ options?: { when?: () => boolean; transform?: (value: T) => T }
4438
+ ): BehaviorCleanup
4472
4439
  ```
4473
4440
 
4474
4441
  **Examples:**
4475
4442
 
4476
4443
  ```typescript
4477
- class GroupNode {
4478
- private readonly behaviorRegistry = new BehaviorRegistry();
4479
-
4480
- applyBehaviorSchema(schemaFn) {
4481
- this.behaviorRegistry.beginRegistration(); // Pushes this to stack
4482
- schemaFn(createBehaviorFieldPath(this)); // Uses getCurrent()
4483
- return this.behaviorRegistry.endRegistration(this); // Pops from stack
4484
- }
4485
- }
4486
- ```
4487
-
4488
- _Source: src/core/behavior/behavior-registry.ts_
4489
-
4490
- ### BehaviorSchemaFn
4491
-
4492
- **Kind:** `type`
4493
-
4494
- Тип функции behavior схемы
4495
- Принимает FieldPath и описывает поведение формы
4496
-
4497
- **Signature:**
4498
- ```typescript
4499
- export type BehaviorSchemaFn<T> = (path: FieldPath<T>) => void;
4444
+ copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail });
4500
4445
  ```
4501
4446
 
4502
- _Source: src/core/behavior/types.ts_
4447
+ _Source: src/core/model/behaviors.ts_
4503
4448
 
4504
- ### computeFrom
4449
+ ### createForm
4505
4450
 
4506
4451
  **Kind:** `function`
4507
4452
 
4508
- Автоматически вычисляет значение поля на основе других полей
4453
+ Создать форму с полной конфигурацией (form, behavior, validation)
4509
4454
 
4510
4455
  **Signature:**
4511
4456
  ```typescript
4512
- export function computeFrom<TForm, TTarget>(
4513
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4514
- sources: FieldPathNode<TForm, any>[],
4515
- target: FieldPathNode<TForm, TTarget>,
4516
- computeFn: (values: TForm) => TTarget,
4517
- options?: ComputeFromOptions<TForm>
4518
- ): void
4457
+ export function createForm<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>;
4519
4458
  ```
4520
4459
 
4521
4460
  **Parameters:**
4522
- - `sources` — - Массив полей-зависимостей
4523
- - `target` — - Поле для записи результата
4524
- - `computeFn` - Функция вычисления (принимает объект с именами полей)
4525
- - `options` — - Опции (`debounce`, `condition`, `trigger`)
4461
+ - `config` — - Конфигурация формы с полями, поведением и валидацией
4462
+
4463
+ **Returns:** Типизированная форма с Proxy-доступом к полям
4526
4464
 
4527
4465
  **Examples:**
4528
4466
 
4529
- Многополевой расчёт — total = price × quantity
4530
4467
  ```typescript
4531
- import { computeFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';
4532
-
4533
- interface OrderForm {
4534
- price: number;
4535
- quantity: number;
4536
- total: number;
4537
- }
4468
+ const form = createForm<UserForm>({
4469
+ form: {
4470
+ email: { value: '', component: Input },
4471
+ password: { value: '', component: Input },
4472
+ },
4473
+ validation: (path) => {
4474
+ required(path.email);
4475
+ email(path.email);
4476
+ required(path.password);
4477
+ minLength(path.password, 8);
4478
+ },
4479
+ });
4538
4480
 
4539
- export const orderBehavior: BehaviorSchemaFn<OrderForm> = (path) => {
4540
- computeFrom(
4541
- [path.price, path.quantity],
4542
- path.total,
4543
- (values) =>
4544
- (typeof values.price === 'number' ? values.price : 0) *
4545
- (typeof values.quantity === 'number' ? values.quantity : 0),
4546
- );
4547
- };
4481
+ // TypeScript знает о полях:
4482
+ form.email.setValue('test@mail.com');
4548
4483
  ```
4549
4484
 
4550
- Edge case — async-like дорогие вычисления с `debounce` и условием
4551
- ```typescript
4552
- import { computeFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';
4485
+ _Source: src/core/utils/create-form.ts_
4553
4486
 
4554
- interface MortgageForm {
4555
- loanType: 'mortgage' | 'consumer';
4556
- loanAmount: number;
4557
- loanTerm: number;
4558
- interestRate: number;
4559
- monthlyPayment: number;
4560
- }
4487
+ ### createFormFromModel
4561
4488
 
4562
- function annuity(values: MortgageForm): number {
4563
- const { loanAmount, loanTerm, interestRate } = values;
4564
- if (!loanAmount || !loanTerm || !interestRate) return 0;
4565
- const r = interestRate / 100 / 12;
4566
- const n = loanTerm;
4567
- return Math.round((loanAmount * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1));
4568
- }
4489
+ **Kind:** `function`
4569
4490
 
4570
- export const mortgageBehavior: BehaviorSchemaFn<MortgageForm> = (path) => {
4571
- computeFrom(
4572
- [path.loanAmount, path.loanTerm, path.interestRate],
4573
- path.monthlyPayment,
4574
- annuity,
4575
- {
4576
- debounce: 300, // не пересчитываем на каждый keystroke
4577
- condition: (form) => form.loanType === 'mortgage', // считаем только для ипотеки
4578
- },
4579
- );
4580
- };
4491
+ **Signature:**
4492
+ ```typescript
4493
+ export function createFormFromModel<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>
4581
4494
  ```
4582
4495
 
4583
- **See also:**
4584
- - [docs/llms/20-compute-vs-watch.md](../../../../docs/llms/20-compute-vs-watch.md)
4585
-
4586
- _Source: src/core/behavior/behaviors/compute-from.ts_
4496
+ _Source: src/core/utils/create-form.ts_
4587
4497
 
4588
- ### ComputeFromOptions
4498
+ ### CreateFormFromModelArgs
4589
4499
 
4590
4500
  **Kind:** `interface`
4591
4501
 
4592
- Опции для computeFrom
4502
+ Аргументы createForm под архитектуру M1: данные приходят из {@link FormModel},
4503
+ конфиг полей (component/componentProps/validators) — из единой схемы.
4593
4504
 
4594
4505
  **Signature:**
4595
4506
  ```typescript
4596
- export interface ComputeFromOptions<TForm> {
4597
- /** Когда вычислять */
4598
- trigger?: 'change' | 'blur';
4599
-
4600
- /** Debounce в мс */
4601
- debounce?: number;
4602
-
4603
- /** Условие применения */
4604
- condition?: (form: TForm) => boolean;
4507
+ export interface CreateFormFromModelArgs<T> {
4508
+ /** Реактивная модель данных (источник истины значений). */
4509
+ model: FormModel<T>;
4510
+ /**
4511
+ * Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг поля к ноде
4512
+ * по идентичности сигнала (`node.value === model.$.path`). Опциональна.
4513
+ */
4514
+ schema?: unknown;
4515
+ /**
4516
+ * Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
4517
+ * заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
4518
+ */
4519
+ behavior?: FormBehavior<T>;
4605
4520
  }
4606
4521
  ```
4607
4522
 
4608
- _Source: src/core/behavior/types.ts_
4523
+ _Source: src/core/utils/create-form.ts_
4609
4524
 
4610
- ### ConditionFn
4525
+ ### createModel
4611
4526
 
4612
- **Kind:** `type`
4527
+ **Kind:** `function`
4613
4528
 
4614
- Функция условия для applyWhen
4529
+ Создать реактивную модель данных формы (слой M1).
4615
4530
 
4616
4531
  **Signature:**
4617
4532
  ```typescript
4618
- export type ConditionFn<T> = (value: T) => boolean;
4533
+ export function createModel<T extends object>(initial: T): FormModel<T>
4619
4534
  ```
4620
4535
 
4621
- _Source: src/core/types/validation-schema.ts_
4622
-
4623
- ### ConfigWithSchema
4536
+ **Parameters:**
4537
+ - `initial` — Начальные значения (объект). Определяют форму данных и initial-снимок.
4624
4538
 
4625
- **Kind:** `interface`
4539
+ **Returns:**
4626
4540
 
4627
- Конфиг с полем schema (для ArrayConfig)
4541
+ **Examples:**
4628
4542
 
4629
- **Signature:**
4630
4543
  ```typescript
4631
- export interface ConfigWithSchema {
4632
- schema: unknown;
4633
- initialItems?: unknown[];
4634
- }
4544
+ const model = createModel<{ email: string; tags: string[] }>({ email: '', tags: [] });
4545
+ model.email = 'a@b.c';
4546
+ model.$.email.value; // 'a@b.c' (сигнал)
4547
+ model.tags.push('x');
4548
+ model.get(); // { email: 'a@b.c', tags: ['x'] }
4635
4549
  ```
4636
4550
 
4637
- _Source: src/core/types/index.ts_
4551
+ _Source: src/core/model/form-model.ts_
4638
4552
 
4639
- ### ConfigWithValue
4553
+ ### disableWhen
4640
4554
 
4641
- **Kind:** `interface`
4555
+ **Kind:** `function`
4642
4556
 
4643
- Конфиг с полем value (для извлечения значений)
4557
+ Условное выключение поля (инверсия {@link enableWhen}).
4644
4558
 
4645
4559
  **Signature:**
4646
4560
  ```typescript
4647
- export interface ConfigWithValue {
4648
- value: unknown;
4649
- }
4561
+ export function disableWhen(
4562
+ target: ReadonlySignal<unknown>,
4563
+ condition: () => boolean,
4564
+ options?: { resetOnDisable?: boolean }
4565
+ ): BehaviorCleanup
4650
4566
  ```
4651
4567
 
4652
- _Source: src/core/types/index.ts_
4568
+ _Source: src/core/model/behaviors.ts_
4653
4569
 
4654
- ### ContextualAsyncValidatorFn
4655
-
4656
- **Kind:** `type`
4657
-
4658
- Асинхронная функция валидации поля с контекстом
4659
-
4660
- **Signature:**
4661
- ```typescript
4662
- export type ContextualAsyncValidatorFn<TForm, TField> = (
4663
- value: TField,
4664
- ctx: FormContext<TForm>
4665
- ) => Promise<ValidationError | null>;
4666
- ```
4667
-
4668
- **Examples:**
4669
-
4670
- ```typescript
4671
- validateAsync(path.email, async (value, ctx) => {
4672
- const exists = await checkEmailExists(value);
4673
- if (exists) return { code: 'exists', message: 'Email already taken' };
4674
- return null;
4675
- });
4676
- ```
4677
-
4678
- _Source: src/core/types/validation-schema.ts_
4679
-
4680
- ### ContextualValidatorFn
4681
-
4682
- **Kind:** `type`
4683
-
4684
- Функция валидации поля с контекстом
4685
-
4686
- Новый паттерн: (value, ctx: FormContext) => ValidationError | null
4687
-
4688
- **Signature:**
4689
- ```typescript
4690
- export type ContextualValidatorFn<TForm, TField> = (
4691
- value: TField,
4692
- ctx: FormContext<TForm>
4693
- ) => ValidationError | null;
4694
- ```
4695
-
4696
- **Examples:**
4697
-
4698
- ```typescript
4699
- validate(path.email, (value, ctx) => {
4700
- if (!value) return { code: 'required', message: 'Email required' };
4701
- const confirm = ctx.form.confirmEmail.value.value;
4702
- if (value !== confirm) return { code: 'mismatch', message: 'Must match' };
4703
- return null;
4704
- });
4705
- ```
4706
-
4707
- _Source: src/core/types/validation-schema.ts_
4708
-
4709
- ### copyFrom
4710
-
4711
- **Kind:** `function`
4712
-
4713
- Копирует значения из одного поля/группы в другое при выполнении условия
4714
-
4715
- **Signature:**
4716
- ```typescript
4717
- export function copyFrom<TForm, TSource, TTarget>(
4718
- source: FieldPathNode<TForm, TSource>,
4719
- target: FieldPathNode<TForm, TTarget>,
4720
- options?: CopyFromOptions<TSource, TForm>
4721
- ): void
4722
- ```
4723
-
4724
- **Parameters:**
4725
- - `source` — - Откуда копировать
4726
- - `target` — - Куда копировать
4727
- - `options` — - Опции копирования (`when`, `fields`, `transform`, `debounce`)
4728
-
4729
- **Examples:**
4730
-
4731
- Скаляр → скаляр с условием
4732
- ```typescript
4733
- import { copyFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';
4734
-
4735
- interface ContactForm {
4736
- sameEmail: boolean;
4737
- email: string;
4738
- emailAdditional: string;
4739
- }
4740
-
4741
- export const contactBehavior: BehaviorSchemaFn<ContactForm> = (path) => {
4742
- copyFrom(path.email, path.emailAdditional, {
4743
- when: (form) => form.sameEmail === true,
4744
- });
4745
- };
4746
- ```
4747
-
4748
- Копирование группы с подмножеством полей и `transform`
4749
- ```typescript
4750
- import { copyFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';
4751
-
4752
- interface Address { country: string; region: string; city: string; street: string }
4753
- interface ProfileForm {
4754
- sameAsRegistration: boolean;
4755
- registrationAddress: Address;
4756
- residenceAddress: Address;
4757
- }
4758
-
4759
- export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
4760
- copyFrom(path.registrationAddress, path.residenceAddress, {
4761
- when: (form) => form.sameAsRegistration === true,
4762
- fields: ['country', 'region', 'city'], // street НЕ копируем
4763
- transform: (addr) => ({
4764
- ...addr,
4765
- country: addr.country.toUpperCase(), // нормализация при копировании
4766
- }),
4767
- debounce: 200,
4768
- });
4769
- };
4770
- ```
4771
-
4772
- **See also:**
4773
- - [docs/llms/23-copy-from.md](../../../../docs/llms/23-copy-from.md)
4774
-
4775
- _Source: src/core/behavior/behaviors/copy-from.ts_
4776
-
4777
- ### CopyFromOptions
4778
-
4779
- **Kind:** `interface`
4780
-
4781
- Опции для copyFrom
4782
-
4783
- **Signature:**
4784
- ```typescript
4785
- export interface CopyFromOptions<TSource, TForm = unknown> {
4786
- /** Условие копирования */
4787
- when?: (form: TForm) => boolean;
4788
-
4789
- /** Какие поля копировать (для групп) */
4790
- fields?: (keyof TSource)[] | 'all';
4791
-
4792
- /** Трансформация значения */
4793
- transform?: (value: TSource) => unknown;
4794
-
4795
- /** Debounce в мс */
4796
- debounce?: number;
4797
- }
4798
- ```
4799
-
4800
- _Source: src/core/behavior/types.ts_
4801
-
4802
- ### createFieldPath
4803
-
4804
- **Kind:** `function`
4805
-
4806
- Создать FieldPath proxy для формы
4807
-
4808
- **Signature:**
4809
- ```typescript
4810
- export function createFieldPath<T>(): FieldPath<T>
4811
- ```
4812
-
4813
- **Examples:**
4814
-
4815
- ```typescript
4816
- const path = createFieldPath<MyForm>();
4817
- console.log(path.email.__path); // 'email'
4818
- console.log(path.personalData.firstName.__path); // 'personalData.firstName'
4819
- ```
4820
-
4821
- _Source: src/core/utils/field-path.ts_
4822
-
4823
- ### createForm
4824
-
4825
- **Kind:** `function`
4826
-
4827
- Создать форму с полной конфигурацией (form, behavior, validation)
4828
-
4829
- **Signature:**
4830
- ```typescript
4831
- export function createForm<T>(config: GroupNodeConfig<T>): FormProxy<T>;
4832
- ```
4833
-
4834
- **Parameters:**
4835
- - `config` — - Конфигурация формы с полями, поведением и валидацией
4836
-
4837
- **Returns:** Типизированная форма с Proxy-доступом к полям
4838
-
4839
- **Examples:**
4840
-
4841
- ```typescript
4842
- const form = createForm<UserForm>({
4843
- form: {
4844
- email: { value: '', component: Input },
4845
- password: { value: '', component: Input },
4846
- },
4847
- validation: (path) => {
4848
- required(path.email);
4849
- email(path.email);
4850
- required(path.password);
4851
- minLength(path.password, 8);
4852
- },
4853
- });
4854
-
4855
- // TypeScript знает о полях:
4856
- form.email.setValue('test@mail.com');
4857
- ```
4858
-
4859
- _Source: src/core/utils/create-form.ts_
4860
-
4861
- ### createTransformer
4570
+ ### email
4862
4571
 
4863
4572
  **Kind:** `function`
4864
4573
 
4865
- Хелпер для создания переиспользуемых трансформаций
4866
-
4867
- **Signature:**
4868
- ```typescript
4869
- export function createTransformer<TValue extends FormValue = FormValue>(
4870
- transformer: (value: TValue) => TValue,
4871
- defaultOptions?: TransformValueOptions
4872
- )
4873
- ```
4874
-
4875
- **Parameters:**
4876
- - `transformer` — - Идемпотентная функция преобразования значения
4877
- - `defaultOptions` — - Опции, применяемые ко всем вызовам созданного трансформера
4878
-
4879
- **Examples:**
4880
-
4881
- Доменно-специфичные трансформеры (банковский счёт, СНИЛС)
4882
- ```typescript
4883
- import { createTransformer, type BehaviorSchemaFn } from '@reformer/core/behaviors';
4884
-
4885
- // Сохраняем только цифры и форматируем СНИЛС: 000-000-000 00
4886
- const formatSnils = createTransformer<string>((v) => {
4887
- const d = (v ?? '').replace(/\D/g, '').slice(0, 11);
4888
- if (d.length < 9) return d;
4889
- return `${d.slice(0, 3)}-${d.slice(3, 6)}-${d.slice(6, 9)}${d.length > 9 ? ' ' + d.slice(9) : ''}`;
4890
- });
4891
-
4892
- interface ProfileForm { snils: string }
4893
-
4894
- export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
4895
- formatSnils(path.snils, { debounce: 100 });
4896
- };
4897
- ```
4898
-
4899
- С `defaultOptions` — единые настройки на серию полей
4900
- ```typescript
4901
- import { createTransformer, type BehaviorSchemaFn } from '@reformer/core/behaviors';
4902
-
4903
- // Все коды должны быть uppercase, но только после правки пользователем
4904
- const upperOnUserEdit = createTransformer<string>(
4905
- (v) => (v ?? '').toUpperCase(),
4906
- { onUserChangeOnly: true, debounce: 100 },
4907
- );
4908
-
4909
- interface PromoForm { promoCode: string; partnerCode: string }
4910
-
4911
- export const promoBehavior: BehaviorSchemaFn<PromoForm> = (path) => {
4912
- upperOnUserEdit(path.promoCode);
4913
- upperOnUserEdit(path.partnerCode);
4914
- };
4915
- ```
4916
-
4917
- _Source: src/core/behavior/behaviors/transform-value.ts_
4918
-
4919
- ### disableWhen
4920
-
4921
- **Kind:** `function`
4922
-
4923
- Условное выключение поля (инверсия enableWhen)
4924
-
4925
- **Signature:**
4926
- ```typescript
4927
- export function disableWhen<TForm>(
4928
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4929
- field: FieldPathNode<TForm, any>,
4930
- condition: (form: TForm) => boolean,
4931
- options?: EnableWhenOptions
4932
- ): void
4933
- ```
4934
-
4935
- **Parameters:**
4936
- - `field` — - Поле для выключения
4937
- - `condition` — - Функция условия (true = disable, false = enable)
4938
- - `options` — - Опции (`resetOnDisable`, `debounce`)
4574
+ Фабрика валидатора формата email.
4939
4575
 
4940
- **Examples:**
4941
-
4942
- Базовый сценарий — readonly после подтверждения
4943
- ```typescript
4944
- import { disableWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
4945
-
4946
- interface ConfirmForm {
4947
- isConfirmed: boolean;
4948
- editableField: string;
4949
- }
4950
-
4951
- export const confirmBehavior: BehaviorSchemaFn<ConfirmForm> = (path) => {
4952
- // Поле блокируется после установки чекбокса подтверждения
4953
- disableWhen(path.editableField, (form) => form.isConfirmed === true);
4954
- // resetOnDisable НЕ ставим — сохраняем введённый текст
4955
- };
4956
- ```
4957
-
4958
- С `resetOnDisable` для очистки заблокированного поля
4959
- ```typescript
4960
- import { disableWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
4961
-
4962
- interface PromoForm {
4963
- loanType: 'mortgage' | 'consumer';
4964
- promoCode: string;
4965
- }
4966
-
4967
- export const promoBehavior: BehaviorSchemaFn<PromoForm> = (path) => {
4968
- // Промокод недоступен для потребительских кредитов и сбрасывается
4969
- disableWhen(path.promoCode, (form) => form.loanType === 'consumer', {
4970
- resetOnDisable: true,
4971
- });
4972
- };
4973
- ```
4974
-
4975
- _Source: src/core/behavior/behaviors/enable-when.ts_
4976
-
4977
- ### email
4978
-
4979
- **Kind:** `function`
4980
-
4981
- Валидатор формата email
4982
-
4983
- Проверяет, что значение соответствует формату email адреса.
4984
4576
  Пустые значения пропускаются (используйте `required` для обязательности).
4985
4577
 
4986
4578
  **Signature:**
4987
4579
  ```typescript
4988
- export function email<TForm, TField extends string | null | undefined = string>(
4989
- fieldPath: FieldPathNode<TForm, TField> | undefined,
4580
+ export function email<TForm = unknown, TField extends string | undefined = string>(
4990
4581
  options?: ValidateOptions
4991
- ): void
4582
+ ): Validator<TForm, TField>
4992
4583
  ```
4993
4584
 
4994
- **Parameters:**
4995
- - `fieldPath` — - Путь к полю для валидации
4996
- - `options` — - Опции валидации (message, params)
4997
-
4998
4585
  **Examples:**
4999
4586
 
5000
4587
  ```typescript
5001
- // Базовое использование
5002
- validationSchema: (path) => [
5003
- required(path.email),
5004
- email(path.email),
5005
- ]
5006
-
5007
- // С кастомным сообщением
5008
- email(path.email, { message: 'Введите корректный email адрес' })
5009
- ```
5010
-
5011
- ```typescript
5012
- // Ошибка валидации
5013
- {
5014
- code: 'email',
5015
- message: 'Неверный формат email',
5016
- params: {}
5017
- }
4588
+ validate(path.email, email());
4589
+ validate(path.email, email({ message: 'Введите корректный email' }));
5018
4590
  ```
5019
4591
 
5020
4592
  _Source: src/core/validation/validators/email.ts_
@@ -5023,107 +4595,30 @@ _Source: src/core/validation/validators/email.ts_
5023
4595
 
5024
4596
  **Kind:** `function`
5025
4597
 
5026
- Условное включение поля на основе значений других полей
4598
+ Условное включение поля (state-операция). Резолвит ноду по сигналу-цели через реестр
4599
+ сигнал→нода (заполняется `createForm`) и вызывает `enable()`/`disable()` (+`reset()` при
4600
+ `resetOnDisable`). `condition` реактивен (читает свои сигналы модели).
4601
+
4602
+ Запись состояния отложена через `runOutsideEffect` (микротаск) для защиты от «Cycle detected».
4603
+ ⚠️ Поле должно быть материализовано в форме (`createForm`) — иначе ноды в реестре нет (например,
4604
+ элемент массива, который строится per-item).
5027
4605
 
5028
4606
  **Signature:**
5029
4607
  ```typescript
5030
- export function enableWhen<TForm>(
5031
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5032
- field: FieldPathNode<TForm, any>,
5033
- condition: (form: TForm) => boolean,
5034
- options?: EnableWhenOptions
5035
- ): void
4608
+ export function enableWhen(
4609
+ target: ReadonlySignal<unknown>,
4610
+ condition: () => boolean,
4611
+ options?: { resetOnDisable?: boolean }
4612
+ ): BehaviorCleanup
5036
4613
  ```
5037
4614
 
5038
- **Parameters:**
5039
- - `field` — - Поле для включения/выключения
5040
- - `condition` — - Функция условия (true = enable, false = disable)
5041
- - `options` — - Опции (`resetOnDisable`, `debounce`)
5042
-
5043
4615
  **Examples:**
5044
4616
 
5045
- Базовый сценарий с `resetOnDisable: true`
5046
4617
  ```typescript
5047
- import { enableWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
5048
-
5049
- interface LoanForm {
5050
- loanType: 'mortgage' | 'consumer' | 'car';
5051
- propertyValue: number;
5052
- initialPayment: number;
5053
- }
5054
-
5055
- export const loanBehavior: BehaviorSchemaFn<LoanForm> = (path) => {
5056
- // Поля ипотеки активны только для loanType === 'mortgage'.
5057
- // resetOnDisable: true гарантирует чистые initial values при переключении.
5058
- enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage', {
5059
- resetOnDisable: true,
5060
- });
5061
- enableWhen(path.initialPayment, (form) => form.loanType === 'mortgage', {
5062
- resetOnDisable: true,
5063
- });
5064
- };
4618
+ enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', { resetOnDisable: true });
5065
4619
  ```
5066
4620
 
5067
- Множественные independent условия + cycle prevention
5068
- ```typescript
5069
- import { enableWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
5070
-
5071
- interface ProfileForm {
5072
- sameAsRegistration: boolean;
5073
- employmentStatus: 'employed' | 'selfEmployed' | 'unemployed';
5074
- residenceAddress: { city: string; street: string };
5075
- companyName: string;
5076
- companyInn: string;
5077
- businessType: string;
5078
- }
5079
-
5080
- export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
5081
- // Адрес проживания: enabled, когда НЕ совпадает с регистрационным
5082
- enableWhen(path.residenceAddress, (form) => form.sameAsRegistration === false, {
5083
- resetOnDisable: true,
5084
- });
5085
-
5086
- // Поля работодателя: только для employed
5087
- enableWhen(path.companyName, (form) => form.employmentStatus === 'employed', {
5088
- resetOnDisable: true,
5089
- });
5090
- enableWhen(path.companyInn, (form) => form.employmentStatus === 'employed', {
5091
- resetOnDisable: true,
5092
- });
5093
-
5094
- // ИП-поля: только для selfEmployed
5095
- enableWhen(path.businessType, (form) => form.employmentStatus === 'selfEmployed', {
5096
- resetOnDisable: true,
5097
- });
5098
-
5099
- // ВАЖНО: condition не должен читать значение САМОГО поля — иначе цикл.
5100
- // condition зависит ТОЛЬКО от независимых триггеров (loanType, employmentStatus, ...).
5101
- };
5102
- ```
5103
-
5104
- **See also:**
5105
- - [docs/llms/22-cycle-detection.md](../../../../docs/llms/22-cycle-detection.md)
5106
-
5107
- _Source: src/core/behavior/behaviors/enable-when.ts_
5108
-
5109
- ### EnableWhenOptions
5110
-
5111
- **Kind:** `interface`
5112
-
5113
- Опции для enableWhen/disableWhen
5114
-
5115
- **Signature:**
5116
- ```typescript
5117
- export interface EnableWhenOptions {
5118
- /** Сбросить значение при disable */
5119
- resetOnDisable?: boolean;
5120
-
5121
- /** Debounce в мс */
5122
- debounce?: number;
5123
- }
5124
- ```
5125
-
5126
- _Source: src/core/behavior/types.ts_
4621
+ _Source: src/core/model/behaviors.ts_
5127
4622
 
5128
4623
  ### ErrorFilterOptions
5129
4624
 
@@ -5183,58 +4678,6 @@ export enum ErrorStrategy {
5183
4678
 
5184
4679
  _Source: src/core/utils/error-handler.ts_
5185
4680
 
5186
- ### extractKey
5187
-
5188
- **Kind:** `function`
5189
-
5190
- Извлечь имя последнего сегмента ({@link FieldPathSegment}) пути.
5191
-
5192
- **Signature:**
5193
- ```typescript
5194
- export function extractKey(node: FieldPathNode<unknown, unknown> | unknown): string
5195
- ```
5196
-
5197
- **Parameters:**
5198
- - `node` — - Узел `FieldPathNode` либо строка-путь.
5199
-
5200
- **Returns:** Имя поля без префикса родителя (последний сегмент).
5201
-
5202
- **Examples:**
5203
-
5204
- ```typescript
5205
- import { extractKey } from '@reformer/core';
5206
-
5207
- extractKey(path.user.email); // → 'email'
5208
- ```
5209
-
5210
- _Source: src/core/utils/field-path.ts_
5211
-
5212
- ### extractPath
5213
-
5214
- **Kind:** `function`
5215
-
5216
- Извлечь строковый путь из {@link FieldPathNode}.
5217
-
5218
- **Signature:**
5219
- ```typescript
5220
- export function extractPath(node: FieldPathNode<unknown, unknown> | unknown): string
5221
- ```
5222
-
5223
- **Parameters:**
5224
- - `node` — - Узел `FieldPathNode` либо строка-путь.
5225
-
5226
- **Returns:** Путь вида `"a.b.c"`.
5227
-
5228
- **Examples:**
5229
-
5230
- ```typescript
5231
- import { extractPath } from '@reformer/core';
5232
-
5233
- const path = (renderPath) => extractPath(renderPath.user.email); // → 'user.email'
5234
- ```
5235
-
5236
- _Source: src/core/utils/field-path.ts_
5237
-
5238
4681
  ### FieldConfig
5239
4682
 
5240
4683
  **Kind:** `interface`
@@ -5244,9 +4687,22 @@ _Source: src/core/utils/field-path.ts_
5244
4687
  **Signature:**
5245
4688
  ```typescript
5246
4689
  export interface FieldConfig<T> {
5247
- value: T | null;
4690
+ /**
4691
+ * Начальное значение-литерал (legacy-путь). Под архитектурой M1 значение приходит из
4692
+ * {@link FieldConfig.valueSignal} (сигнал {@link FormModel}); тогда `value` не требуется.
4693
+ */
4694
+ value?: T | null;
4695
+ /**
4696
+ * Сигнал значения из {@link FormModel} (M1). Если задан — служит источником истины значения
4697
+ * поля (нода не владеет значением, а ссылается на этот сигнал). Имеет приоритет над `value`.
4698
+ */
4699
+ valueSignal?: Signal<T>;
4700
+ /**
4701
+ * UI-компонент поля. Опционален: core-часть можно использовать без ссылки на компонент
4702
+ * (значение/валидация работают без UI; компонент нужен только для рендеринга).
4703
+ */
5248
4704
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5249
- component: ComponentType<any>;
4705
+ component?: ComponentType<any>;
5250
4706
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5251
4707
  componentProps?: any;
5252
4708
  validators?: ValidatorFn<T>[];
@@ -5500,133 +4956,6 @@ console.log(field.valid.value); // true
5500
4956
 
5501
4957
  _Source: src/core/nodes/field-node.ts_
5502
4958
 
5503
- ### FieldPath
5504
-
5505
- **Kind:** `type`
5506
-
5507
- FieldPath предоставляет типобезопасный доступ к путям полей формы
5508
-
5509
- Рекурсивно обрабатывает вложенные объекты для поддержки вложенных форм.
5510
-
5511
- Использование:
5512
- ```typescript
5513
- const validation = (path: FieldPath<MyForm>) => {
5514
- required(path.email, { message: 'Email обязателен' });
5515
-
5516
- // Вложенные объекты
5517
- required(path.registrationAddress.city);
5518
- minLength(path.registrationAddress.street, 3);
5519
-
5520
- applyWhen(
5521
- path.loanType,
5522
- (type) => type === 'mortgage',
5523
- (path) => {
5524
- required(path.propertyValue, { message: 'Укажите стоимость' });
5525
- }
5526
- );
5527
- };
5528
- ```
5529
-
5530
- **Signature:**
5531
- ```typescript
5532
- export type FieldPath<T> = {
5533
- [K in keyof T]: NonNullable<T[K]> extends Array<unknown>
5534
- ? FieldPathNode<T, T[K], K> // Массивы - не рекурсим (обрабатываются отдельно)
5535
- : NonNullable<T[K]> extends Date | File | Blob | AnyFunction
5536
- ? FieldPathNode<T, T[K], K> // Специальные объекты - не рекурсим
5537
- : NonNullable<T[K]> extends object
5538
- ? FieldPathNode<T, T[K], K> & FieldPath<NonNullable<T[K]>> // Обычные объекты - рекурсия!
5539
- : FieldPathNode<T, T[K], K>; // Примитивы
5540
- };
5541
- ```
5542
-
5543
- _Source: src/core/types/field-path.ts_
5544
-
5545
- ### FieldPathNavigator
5546
-
5547
- **Kind:** `class`
5548
-
5549
- Навигация по путям к полям формы
5550
-
5551
- Централизует логику парсинга и навигации по путям к полям формы.
5552
- Используется в ValidationContext, BehaviorContext, GroupNode для единообразной
5553
- обработки путей вида "address.city" или "items[0].name".
5554
-
5555
- Устраняет дублирование логики парсинга путей в 4 местах кодовой базы.
5556
-
5557
- **Signature:**
5558
- ```typescript
5559
- export class FieldPathNavigator {
5560
- /**
5561
- * Парсит путь в массив сегментов
5562
- *
5563
- * Поддерживаемые форматы:
5564
- * - Простые пути: "name", "email"
5565
- * - Вложенные пути: "address.city", "user.profile.avatar"
5566
- * - Массивы: "items[0]", "items[0].name", "tags[1][0]"
5567
- * - Комбинации: "orders[0].items[1].price"
5568
- *
5569
- * @param path Путь к полю (строка с точками и квадратными скобками)
5570
- * @returns Массив сегментов пути
5571
- *
5572
- * @example
5573
- * ```typescript
5574
- * navigator.parsePath('email');
5575
- * // [{ key: 'email' }]
5576
- *
5577
- * navigator.parsePath('address.city');
5578
- * // [{ key: 'address' }, { key: 'city' }]
5579
- *
5580
- * navigator.parsePath('items[0].name');
5581
- * // [{ key: 'items', index: 0 }, { key: 'name' }]
5582
- * ```
5583
- */ /* … */ }
5584
- ```
5585
-
5586
- **Examples:**
5587
-
5588
- ```typescript
5589
- const navigator = new FieldPathNavigator();
5590
-
5591
- // Парсинг пути
5592
- const segments = navigator.parsePath('items[0].title');
5593
- // [{ key: 'items', index: 0 }, { key: 'title' }]
5594
-
5595
- // Получение значения из объекта
5596
- const obj = { items: [{ title: 'Item 1' }] };
5597
- const value = navigator.getValueByPath(obj, 'items[0].title');
5598
- // 'Item 1'
5599
-
5600
- // Получение узла формы
5601
- const titleNode = navigator.getNodeByPath(form, 'items[0].title');
5602
- titleNode?.setValue('New Title');
5603
- ```
5604
-
5605
- _Source: src/core/utils/field-path-navigator.ts_
5606
-
5607
- ### FieldPathNode
5608
-
5609
- **Kind:** `interface`
5610
-
5611
- Узел в пути поля
5612
- Содержит метаинформацию о поле для валидации
5613
-
5614
- **Signature:**
5615
- ```typescript
5616
- export interface FieldPathNode<TForm, TField, TKey = unknown> {
5617
- /** Ключ поля */
5618
- readonly __key: TKey;
5619
- /** Путь к полю (для вложенных объектов) */
5620
- readonly __path: string;
5621
- /** Тип формы */
5622
- readonly __formType?: TForm;
5623
- /** Тип поля */
5624
- readonly __fieldType?: TField;
5625
- }
5626
- ```
5627
-
5628
- _Source: src/core/types/field-path.ts_
5629
-
5630
4959
  ### FieldPathSegment
5631
4960
 
5632
4961
  **Kind:** `type`
@@ -5667,7 +4996,7 @@ _Source: src/core/types/index.ts_
5667
4996
 
5668
4997
  **Signature:**
5669
4998
  ```typescript
5670
- export type FormArrayProxy<T extends FormFields> = ArrayNode<T> & {
4999
+ export type FormArrayProxy<T extends object> = ArrayNode<T> & {
5671
5000
  /**
5672
5001
  * Безопасный доступ к элементу массива по индексу
5673
5002
  * Возвращает GroupNode с типизированными полями или undefined
@@ -5761,102 +5090,6 @@ export type FormChangeType = 'value' | 'status' | 'errors' | 'touched' | 'dirty'
5761
5090
 
5762
5091
  _Source: src/core/utils/form-observer.ts_
5763
5092
 
5764
- ### FormContext
5765
-
5766
- **Kind:** `interface`
5767
-
5768
- Единый контекст для работы с формой
5769
-
5770
- Предоставляет:
5771
- - `form` - типизированный Proxy-доступ к полям формы
5772
- - `setFieldValue` - безопасная установка значения (emitEvent: false)
5773
-
5774
- **Signature:**
5775
- ```typescript
5776
- export interface FormContext<TForm> {
5777
- /**
5778
- * Форма с типизированным Proxy-доступом к полям
5779
- *
5780
- * Позволяет обращаться к полям напрямую через точечную нотацию:
5781
- * - `ctx.form.email` → FieldNode
5782
- * - `ctx.form.address.city` → FieldNode (вложенный)
5783
- * - `ctx.form.items` → ArrayNode
5784
- *
5785
- * @example
5786
- * ```typescript
5787
- * // Получить значение
5788
- * ctx.form.email.value.value
5789
- *
5790
- * // Установить значение (⚠️ может вызвать цикл в behavior!)
5791
- * ctx.form.email.setValue('new@mail.com')
5792
- *
5793
- * // Безопасно установить значение
5794
- * ctx.form.email.setValue('new@mail.com', { emitEvent: false })
5795
- *
5796
- * // Обновить пропсы компонента
5797
- * ctx.form.city.updateComponentProps({ options: cities })
5798
- *
5799
- * // Валидация поля
5800
- * await ctx.form.email.validate()
5801
- *
5802
- * // Работа с массивами
5803
- * ctx.form.items.push({ title: 'New' })
5804
- * ctx.form.items.clear()
5805
- * ```
5806
- */
5807
- readonly form: FormProxy<TForm>;
5808
-
5809
- /**
5810
- * Безопасно установить значение поля по строковому пути или FieldPath
5811
- *
5812
- * Автоматически использует `emitEvent: false` для предотвращения
5813
- * бесконечных циклов в behavior схемах.
5814
- *
5815
- * @param path - Путь к полю (строка или FieldPath)
5816
- * @param value - Новое значение
5817
- *
5818
- * @example
5819
- * ```typescript
5820
- * // Сброс города при смене страны (строковый путь)
5821
- * watchField(path.country, (country, ctx) => {
5822
- * ctx.setFieldValue('city', null);
5823
- * });
5824
- *
5825
- * // Использование FieldPath напрямую (более типобезопасно)
5826
- * watchField(path.country, (country, ctx) => {
5827
- * ctx.setFieldValue(path.city, null);
5828
- * });
5829
- * ```
5830
- */
5831
- setFieldValue(path: string | FieldPathNode<TForm, unknown>, value: unknown): void;
5832
-
5833
- /**
5834
- * Получить поле формы по строковому пути
5835
- *
5836
- * Используется для динамического доступа к вложенным полям,
5837
- * особенно в модульных behavior схемах, применяемых через apply().
5838
- *
5839
- * @param path - Строковый путь к полю (например "address.city")
5840
- * @returns FormNode или undefined если поле не найдено
5841
- *
5842
- * @example
5843
- * ```typescript
5844
- * // В модульной behavior схеме, применяемой к вложенному полю:
5845
- * // apply([path.registrationAddress, path.residenceAddress], addressBehavior)
5846
- *
5847
- * watchField(path.region, (region, ctx) => {
5848
- * // path.city.__path = "registrationAddress.city" или "residenceAddress.city"
5849
- * const cityField = ctx.getFieldByPath(path.city.__path);
5850
- * cityField?.updateComponentProps({ options: cities });
5851
- * });
5852
- * ```
5853
- */
5854
- getFieldByPath(path: string): FormNode<FormValue> | undefined;
5855
- }
5856
- ```
5857
-
5858
- _Source: src/core/types/form-context.ts_
5859
-
5860
5093
  ### FormControlsProxy
5861
5094
 
5862
5095
  **Kind:** `type`
@@ -5874,15 +5107,16 @@ _Source: src/core/types/form-context.ts_
5874
5107
  **Signature:**
5875
5108
  ```typescript
5876
5109
  export type FormControlsProxy<T> = {
5877
- [K in keyof T]: NonNullable<T[K]> extends Array<infer U>
5878
- ? U extends FormFields
5879
- ? FormArrayProxy<U> // Массив объектов FormArrayProxy
5110
+ // `-?` снимает опциональность: для каждого поля схемы прокси всегда содержит узел
5111
+ // (включая опциональные поля — у них узел существует, опционально лишь значение).
5112
+ // Без этого `control.optionalField` имел бы тип `FieldNode<...> | undefined`.
5113
+ [K in keyof T]-?: NonNullable<T[K]> extends ReadonlyArray<infer U>
5114
+ ? IsGroupObject<U> extends true
5115
+ ? FormArrayProxy<U & object> // Массив объектов → FormArrayProxy
5880
5116
  : FieldNode<T[K]> // Массив примитивов → FieldNode
5881
- : NonNullable<T[K]> extends FormFields
5882
- ? NonNullable<T[K]> extends Date | File | Blob
5883
- ? FieldNode<T[K]> // Специальные объекты → FieldNode
5884
- : FormProxy<NonNullable<T[K]>> // Обычный объект → FormProxy (рекурсивно!)
5885
- : FieldNode<T[K]>; // Примитивные типы → FieldNode
5117
+ : IsGroupObject<NonNullable<T[K]>> extends true
5118
+ ? FormProxy<NonNullable<T[K]>> // Обычный объект FormProxy (рекурсивно!)
5119
+ : FieldNode<T[K]>; // Примитивы и спец-объекты (Date/File/Blob) → FieldNode
5886
5120
  };
5887
5121
  ```
5888
5122
 
@@ -5986,6 +5220,19 @@ export type FormFields = Record<string, FormValue>;
5986
5220
 
5987
5221
  _Source: src/core/types/index.ts_
5988
5222
 
5223
+ ### FormModel
5224
+
5225
+ **Kind:** `type`
5226
+
5227
+ FormModel — value-proxy объекта `T` + {@link ModelApi}.
5228
+
5229
+ **Signature:**
5230
+ ```typescript
5231
+ export type FormModel<T> = ModelObject<T> & ModelApi<T>;
5232
+ ```
5233
+
5234
+ _Source: src/core/model/types.ts_
5235
+
5989
5236
  ### FormNode
5990
5237
 
5991
5238
  **Kind:** `class`
@@ -6276,99 +5523,40 @@ _Source: src/core/types/index.ts_
6276
5523
 
6277
5524
  **Kind:** `function`
6278
5525
 
6279
- Проверяет, что дата находится в будущем (не в прошлом)
5526
+ Фабрика валидатора, проверяющего что дата не в прошлом.
6280
5527
 
6281
- Пустые значения и невалидные даты пропускаются (используйте `required` и `isDate`).
5528
+ Пустые и невалидные даты пропускаются.
6282
5529
 
6283
5530
  **Signature:**
6284
5531
  ```typescript
6285
- export function futureDate<TForm, TField extends string | Date | undefined = string | Date>(
6286
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6287
- options?: ValidateOptions
6288
- ): void
5532
+ export function futureDate<
5533
+ TForm = unknown,
5534
+ TField extends string | Date | undefined = string | Date,
5535
+ >(options?: ValidateOptions): Validator<TForm, TField>
6289
5536
  ```
6290
5537
 
6291
- **Parameters:**
6292
- - `fieldPath` — - Путь к полю для валидации
6293
- - `options` — - Опции валидации (message, params)
6294
-
6295
5538
  **Examples:**
6296
5539
 
6297
5540
  ```typescript
6298
- // Базовое использование - дата события должна быть в будущем
6299
- validationSchema: (path) => [
6300
- futureDate(path.eventDate),
6301
- ]
6302
-
6303
- // С кастомным сообщением
6304
- futureDate(path.appointmentDate, { message: 'Дата записи должна быть в будущем' })
6305
- ```
6306
-
6307
- ```typescript
6308
- // Ошибка валидации
6309
- {
6310
- code: 'date_past',
6311
- message: 'Дата не может быть в прошлом',
6312
- params: {}
6313
- }
5541
+ validate(path.eventDate, futureDate());
5542
+ validate(path.appointmentDate, futureDate({ message: 'Дата записи должна быть в будущем' }));
6314
5543
  ```
6315
5544
 
6316
5545
  _Source: src/core/validation/validators/future-date.ts_
6317
5546
 
6318
- ### getCurrentBehaviorRegistry
6319
-
6320
- **Kind:** `function`
6321
-
6322
- Получить текущий активный BehaviorRegistry из context stack
6323
-
6324
- Используется внутри behavior schema функций (copyFrom, enableWhen, computeFrom и т.д.)
6325
- для регистрации поведений в активном контексте GroupNode
6326
-
6327
- **Signature:**
6328
- ```typescript
6329
- export function getCurrentBehaviorRegistry(): BehaviorRegistry
6330
- ```
6331
-
6332
- **Returns:** Текущий BehaviorRegistry или заглушка в production
6333
-
6334
- **Examples:**
6335
-
6336
- ```typescript
6337
- function copyFrom(target, source, options) {
6338
- const registry = getCurrentBehaviorRegistry();
6339
- const handler = createCopyBehavior(target, source, options);
6340
- registry.register(handler, { debounce: options?.debounce });
6341
- }
6342
- ```
6343
-
6344
- _Source: src/core/utils/registry-helpers.ts_
6345
-
6346
- ### getCurrentValidationRegistry
5547
+ ### getNodeForSignal
6347
5548
 
6348
5549
  **Kind:** `function`
6349
5550
 
6350
- Получить текущий активный ValidationRegistry из context stack
6351
-
6352
- Используется внутри validation schema функций (validate, validateAsync, required и т.д.)
6353
- для регистрации валидаторов в активном контексте GroupNode
5551
+ Получить FieldNode по сигналу модели (или undefined, если форма не построена/поле не материализовано,
5552
+ например элемент массива в родительской форме).
6354
5553
 
6355
5554
  **Signature:**
6356
5555
  ```typescript
6357
- export function getCurrentValidationRegistry(): ValidationRegistry
5556
+ export function getNodeForSignal(signal: Signal<any>): FormNode<any> | undefined
6358
5557
  ```
6359
5558
 
6360
- **Returns:** Текущий ValidationRegistry или заглушка в production
6361
-
6362
- **Examples:**
6363
-
6364
- ```typescript
6365
- function required(fieldPath, options) {
6366
- const registry = getCurrentValidationRegistry();
6367
- registry.registerSync(path, validator, options);
6368
- }
6369
- ```
6370
-
6371
- _Source: src/core/utils/registry-helpers.ts_
5559
+ _Source: src/core/utils/signal-node-registry.ts_
6372
5560
 
6373
5561
  ### getNodeType
6374
5562
 
@@ -6402,9 +5590,9 @@ _Source: src/core/utils/type-guards.ts_
6402
5590
 
6403
5591
  GroupNode - узел для группы полей
6404
5592
 
6405
- Поддерживает два API:
6406
- 1. Старый API (только schema) - обратная совместимость
6407
- 2. Новый API (config с form, behavior, validation) - автоматическое применение схем
5593
+ Создаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`
5594
+ (M1: `createForm({ model, schema })`); валидация/behavior живут на слое модели
5595
+ (`validateFormModel`, `computeFrom`/`enableWhen`/…), а не на ноде.
6408
5596
 
6409
5597
  **Signature:**
6410
5598
  ```typescript
@@ -6417,33 +5605,16 @@ export class GroupNode<T> extends FormNode<T> {
6417
5605
  **Examples:**
6418
5606
 
6419
5607
  ```typescript
6420
- // 1. Старый способ (обратная совместимость)
6421
- const simpleForm = new GroupNode({
6422
- email: { value: '', component: Input },
6423
- password: { value: '', component: Input },
6424
- });
6425
-
6426
- // 2. Новый способ (с behavior и validation схемами)
6427
- const fullForm = new GroupNode({
6428
- form: {
6429
- email: { value: '', component: Input },
6430
- password: { value: '', component: Input },
6431
- },
6432
- behavior: (path) => {
6433
- computeFrom(path.email, [path.email], (values) => values[0]?.trim());
6434
- },
6435
- validation: (path) => {
6436
- required(path.email, { message: 'Email обязателен' });
6437
- email(path.email);
6438
- required(path.password);
6439
- minLength(path.password, 8);
6440
- },
5608
+ const form = new GroupNode({
5609
+ email: { valueSignal: model.$.email, component: Input },
5610
+ password: { valueSignal: model.$.password, component: Input },
6441
5611
  });
6442
5612
 
6443
5613
  // Прямой доступ к полям через Proxy
6444
- fullForm.email.setValue('test@mail.com');
6445
- await fullForm.validate();
6446
- console.log(fullForm.valid.value); // true
5614
+ const proxy = form.getProxy();
5615
+ proxy.email.setValue('test@mail.com');
5616
+ await proxy.validate();
5617
+ console.log(proxy.valid.value);
6447
5618
  ```
6448
5619
 
6449
5620
  _Source: src/core/nodes/group-node.ts_
@@ -6452,38 +5623,44 @@ _Source: src/core/nodes/group-node.ts_
6452
5623
 
6453
5624
  **Kind:** `interface`
6454
5625
 
6455
- Конфигурация GroupNode с поддержкой схем
6456
- Используется для создания форм с автоматическим применением behavior и validation схем
5626
+ Конфигурация GroupNode.
5627
+
5628
+ Под M1 группа создаётся из плоской {@link FormSchema} (дерево field-конфигов). Обёртка
5629
+ `{ form }` сохранена для совместимости вызова, legacy behavior/validation-схемы удалены (Ф7).
6457
5630
 
6458
5631
  **Signature:**
6459
5632
  ```typescript
6460
5633
  export interface GroupNodeConfig<T> {
6461
5634
  /** Схема структуры формы (поля и их конфигурация) */
6462
5635
  form: FormSchema<T>;
5636
+ }
5637
+ ```
6463
5638
 
6464
- /** Схема реактивного поведения (copyFrom, enableWhen, computeFrom и т.д.) */
6465
- behavior?: BehaviorSchemaFn<T>;
5639
+ _Source: src/core/types/index.ts_
6466
5640
 
6467
- /** Схема валидации (required, email, minLength и т.д.) */
6468
- validation?: ValidationSchemaFn<T>;
5641
+ ### integer
6469
5642
 
6470
- /**
6471
- * Опциональный ValidationRegistry для dependency injection
6472
- * Используется для тестирования с mock-реестрами
6473
- * @internal
6474
- */
6475
- _validationRegistry?: unknown;
5643
+ **Kind:** `function`
6476
5644
 
6477
- /**
6478
- * Опциональный BehaviorRegistry для dependency injection
6479
- * Используется для тестирования с mock-реестрами
6480
- * @internal
6481
- */
6482
- _behaviorRegistry?: unknown;
6483
- }
5645
+ Фабрика валидатора, проверяющего что число — целое.
5646
+
5647
+ Пустые значения и не-числа пропускаются (используйте `required` и `isNumber`).
5648
+
5649
+ **Signature:**
5650
+ ```typescript
5651
+ export function integer<TForm = unknown, TField extends number | undefined = number>(
5652
+ options?: ValidateOptions
5653
+ ): Validator<TForm, TField>
6484
5654
  ```
6485
5655
 
6486
- _Source: src/core/types/index.ts_
5656
+ **Examples:**
5657
+
5658
+ ```typescript
5659
+ validate(path.age, integer());
5660
+ validate(path.count, integer({ message: 'Должно быть целым числом' }));
5661
+ ```
5662
+
5663
+ _Source: src/core/validation/validators/integer.ts_
6487
5664
 
6488
5665
  ### isArrayNode
6489
5666
 
@@ -6520,45 +5697,38 @@ _Source: src/core/utils/type-guards.ts_
6520
5697
 
6521
5698
  **Kind:** `function`
6522
5699
 
6523
- Проверяет, что значение является валидной датой
5700
+ Фабрика валидатора, проверяющего что значение валидная дата.
6524
5701
 
6525
5702
  Пустые значения пропускаются (используйте `required` для обязательности).
6526
5703
 
6527
5704
  **Signature:**
6528
5705
  ```typescript
6529
- export function isDate<TForm, TField extends string | Date | undefined = string | Date>(
6530
- fieldPath: FieldPathNode<TForm, TField> | undefined,
5706
+ export function isDate<TForm = unknown, TField extends string | Date | undefined = string | Date>(
6531
5707
  options?: ValidateOptions
6532
- ): void
5708
+ ): Validator<TForm, TField>
6533
5709
  ```
6534
5710
 
6535
- **Parameters:**
6536
- - `fieldPath` — - Путь к полю для валидации
6537
- - `options` — - Опции валидации (message, params)
6538
-
6539
5711
  **Examples:**
6540
5712
 
6541
5713
  ```typescript
6542
- // Базовое использование
6543
- validationSchema: (path) => [
6544
- isDate(path.birthDate),
6545
- isDate(path.eventDate),
6546
- ]
6547
-
6548
- // С кастомным сообщением
6549
- isDate(path.birthDate, { message: 'Введите корректную дату' })
5714
+ validate(path.birthDate, isDate());
5715
+ validate(path.eventDate, isDate({ message: 'Введите корректную дату' }));
6550
5716
  ```
6551
5717
 
5718
+ _Source: src/core/validation/validators/is-date.ts_
5719
+
5720
+ ### isDerived
5721
+
5722
+ **Kind:** `function`
5723
+
5724
+ Производный ли сигнал (читают bulk-сеттеры, чтобы не затирать вычисляемые поля).
5725
+
5726
+ **Signature:**
6552
5727
  ```typescript
6553
- // Ошибка валидации
6554
- {
6555
- code: 'date_invalid',
6556
- message: 'Неверный формат даты',
6557
- params: {}
6558
- }
5728
+ export function isDerived(signal: Signal<any>): boolean
6559
5729
  ```
6560
5730
 
6561
- _Source: src/core/validation/validators/is-date.ts_
5731
+ _Source: src/core/utils/derived-registry.ts_
6562
5732
 
6563
5733
  ### isFieldNode
6564
5734
 
@@ -6626,8 +5796,8 @@ _Source: src/core/utils/type-guards.ts_
6626
5796
 
6627
5797
  Проверить, является ли значение GroupNode (объект с вложенными полями)
6628
5798
 
6629
- GroupNode представляет объект с вложенными полями формы
6630
- и имеет методы для применения validation/behavior схем
5799
+ GroupNode представляет объект с вложенными полями формы: имеет навигацию по полям
5800
+ (`getFieldByPath`/`fields`) и НЕ имеет array-методов (`items`/`push`/`removeAt`).
6631
5801
 
6632
5802
  **Signature:**
6633
5803
  ```typescript
@@ -6643,101 +5813,99 @@ export function isGroupNode(value: unknown): value is GroupNode<FormFields>
6643
5813
 
6644
5814
  ```typescript
6645
5815
  if (isGroupNode(node)) {
6646
- node.applyValidationSchema(schema); // OK
6647
5816
  node.getFieldByPath('user.email'); // OK
6648
5817
  }
6649
5818
  ```
6650
5819
 
6651
5820
  _Source: src/core/utils/type-guards.ts_
6652
5821
 
6653
- ### max
5822
+ ### isNumber
6654
5823
 
6655
5824
  **Kind:** `function`
6656
5825
 
6657
- Валидатор максимального числового значения
5826
+ Фабрика валидатора, проверяющего что значение — конечное число (не NaN, не строка).
6658
5827
 
6659
- Проверяет, что числовое значение не превышает указанный максимум.
6660
- Пустые значения пропускаются (используйте `required` для обязательности).
5828
+ Пустые значения (`null`/`undefined`) пропускаются (используйте `required` для обязательности).
5829
+ В отличие от других number-валидаторов, **не** пропускает не-числа — это его задача.
6661
5830
 
6662
5831
  **Signature:**
6663
5832
  ```typescript
6664
- export function max<TForm, TField extends number | null | undefined = number>(
6665
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6666
- maxValue: number,
5833
+ export function isNumber<TForm = unknown, TField extends number | undefined = number>(
6667
5834
  options?: ValidateOptions
6668
- ): void
5835
+ ): Validator<TForm, TField>
6669
5836
  ```
6670
5837
 
6671
- **Parameters:**
6672
- - `fieldPath` — - Путь к полю для валидации
6673
- - `maxValue` — - Максимально допустимое значение
6674
- - `options` — - Опции валидации (message, params)
6675
-
6676
5838
  **Examples:**
6677
5839
 
6678
5840
  ```typescript
6679
- // Базовое использование
6680
- validationSchema: (path) => [
6681
- max(path.quantity, 100),
6682
- max(path.discount, 50),
6683
- ]
6684
-
6685
- // С кастомным сообщением
6686
- max(path.quantity, 100, { message: 'Максимум 100 единиц' })
5841
+ validate(path.amount, isNumber());
5842
+ validate(path.amount, isNumber({ message: 'Введите число' }));
6687
5843
  ```
6688
5844
 
5845
+ _Source: src/core/validation/validators/is-number.ts_
5846
+
5847
+ ### markDerived
5848
+
5849
+ **Kind:** `function`
5850
+
5851
+ Пометить сигнал производным (вызывают compute/computeFrom).
5852
+
5853
+ **Signature:**
6689
5854
  ```typescript
6690
- // Ошибка валидации
6691
- {
6692
- code: 'max',
6693
- message: 'Максимальное значение: 100',
6694
- params: { max: 100, actual: 150 }
6695
- }
5855
+ export function markDerived(signal: Signal<any>): void
6696
5856
  ```
6697
5857
 
6698
- _Source: src/core/validation/validators/max.ts_
5858
+ _Source: src/core/utils/derived-registry.ts_
6699
5859
 
6700
- ### maxAge
5860
+ ### max
6701
5861
 
6702
5862
  **Kind:** `function`
6703
5863
 
6704
- Проверяет, что возраст (вычисленный из даты рождения) не больше указанного
5864
+ Фабрика валидатора максимального числового значения.
6705
5865
 
6706
- Пустые значения и невалидные даты пропускаются (используйте `required` и `isDate`).
5866
+ Пустые значения пропускаются (используйте `required` для обязательности).
6707
5867
 
6708
5868
  **Signature:**
6709
5869
  ```typescript
6710
- export function maxAge<TForm, TField extends string | Date | null | undefined = string | Date>(
6711
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6712
- maxAgeValue: number,
5870
+ export function max<TForm = unknown, TField extends number | undefined = number>(
5871
+ maxValue: number,
6713
5872
  options?: ValidateOptions
6714
- ): void
5873
+ ): Validator<TForm, TField>
6715
5874
  ```
6716
5875
 
6717
- **Parameters:**
6718
- - `fieldPath` — - Путь к полю для валидации (дата рождения)
6719
- - `maxAgeValue` — - Максимально допустимый возраст в годах
6720
- - `options` — - Опции валидации (message, params)
6721
-
6722
5876
  **Examples:**
6723
5877
 
6724
5878
  ```typescript
6725
- // Базовое использование - максимум 65 лет
6726
- validationSchema: (path) => [
6727
- maxAge(path.birthDate, 65),
6728
- ]
5879
+ import { validate } from '@reformer/core';
5880
+ import { max } from '@reformer/core/validators';
6729
5881
 
6730
- // С кастомным сообщением
6731
- maxAge(path.birthDate, 100, { message: 'Проверьте правильность даты рождения' })
5882
+ validate(path.quantity, max(100));
5883
+ validate(path.discount, max(50, { message: 'Не более 50%' }));
6732
5884
  ```
6733
5885
 
5886
+ _Source: src/core/validation/validators/max.ts_
5887
+
5888
+ ### maxAge
5889
+
5890
+ **Kind:** `function`
5891
+
5892
+ Фабрика валидатора максимального возраста (по дате рождения).
5893
+
5894
+ Пустые и невалидные даты пропускаются.
5895
+
5896
+ **Signature:**
6734
5897
  ```typescript
6735
- // Ошибка валидации
6736
- {
6737
- code: 'date_max_age',
6738
- message: 'Максимальный возраст: 65 лет',
6739
- params: { maxAge: 65, currentAge: 70 }
6740
- }
5898
+ export function maxAge<
5899
+ TForm = unknown,
5900
+ TField extends string | Date | null | undefined = string | Date,
5901
+ >(maxAgeValue: number, options?: ValidateOptions): Validator<TForm, TField>
5902
+ ```
5903
+
5904
+ **Examples:**
5905
+
5906
+ ```typescript
5907
+ validate(path.birthDate, maxAge(65));
5908
+ validate(path.birthDate, maxAge(100, { message: 'Проверьте дату рождения' }));
6741
5909
  ```
6742
5910
 
6743
5911
  _Source: src/core/validation/validators/max-age.ts_
@@ -6746,43 +5914,23 @@ _Source: src/core/validation/validators/max-age.ts_
6746
5914
 
6747
5915
  **Kind:** `function`
6748
5916
 
6749
- Проверяет, что дата не позже указанной максимальной
5917
+ Фабрика валидатора максимальной даты (включительно).
6750
5918
 
6751
- Пустые значения и невалидные даты пропускаются (используйте `required` и `isDate`).
5919
+ Пустые и невалидные даты пропускаются.
6752
5920
 
6753
5921
  **Signature:**
6754
5922
  ```typescript
6755
- export function maxDate<TForm, TField extends string | Date | null | undefined = string | Date>(
6756
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6757
- maxDateValue: Date,
6758
- options?: ValidateOptions
6759
- ): void
5923
+ export function maxDate<
5924
+ TForm = unknown,
5925
+ TField extends string | Date | null | undefined = string | Date,
5926
+ >(maxDateValue: Date, options?: ValidateOptions): Validator<TForm, TField>
6760
5927
  ```
6761
5928
 
6762
- **Parameters:**
6763
- - `fieldPath` — - Путь к полю для валидации
6764
- - `maxDateValue` — - Максимально допустимая дата (включительно)
6765
- - `options` — - Опции валидации (message, params)
6766
-
6767
5929
  **Examples:**
6768
5930
 
6769
5931
  ```typescript
6770
- // Базовое использование
6771
- validationSchema: (path) => [
6772
- maxDate(path.birthDate, new Date()), // Не позже сегодня
6773
- ]
6774
-
6775
- // С кастомным сообщением
6776
- maxDate(path.endDate, new Date('2025-12-31'), { message: 'Дата не может быть позже конца года' })
6777
- ```
6778
-
6779
- ```typescript
6780
- // Ошибка валидации
6781
- {
6782
- code: 'date_max',
6783
- message: 'Дата должна быть не позднее 31.12.2025',
6784
- params: { maxDate: Date }
6785
- }
5932
+ validate(path.birthDate, maxDate(new Date())); // не позже сегодня
5933
+ validate(path.endDate, maxDate(new Date('2025-12-31')));
6786
5934
  ```
6787
5935
 
6788
5936
  _Source: src/core/validation/validators/max-date.ts_
@@ -6791,45 +5939,23 @@ _Source: src/core/validation/validators/max-date.ts_
6791
5939
 
6792
5940
  **Kind:** `function`
6793
5941
 
6794
- Валидатор максимальной длины строки
5942
+ Фабрика валидатора максимальной длины строки или массива.
6795
5943
 
6796
- Проверяет, что длина строки не превышает указанный максимум.
6797
5944
  Пустые значения пропускаются (используйте `required` для обязательности).
6798
5945
 
6799
5946
  **Signature:**
6800
5947
  ```typescript
6801
- export function maxLength<TForm, TField extends string | null | undefined = string>(
6802
- fieldPath: FieldPathNode<TForm, TField> | undefined,
5948
+ export function maxLength<TForm = unknown, TField = unknown>(
6803
5949
  maxLen: number,
6804
5950
  options?: ValidateOptions
6805
- ): void
5951
+ ): Validator<TForm, TField>
6806
5952
  ```
6807
5953
 
6808
- **Parameters:**
6809
- - `fieldPath` — - Путь к полю для валидации
6810
- - `maxLen` — - Максимальная допустимая длина строки
6811
- - `options` — - Опции валидации (message, params)
6812
-
6813
5954
  **Examples:**
6814
5955
 
6815
5956
  ```typescript
6816
- // Базовое использование
6817
- validationSchema: (path) => [
6818
- maxLength(path.name, 50),
6819
- maxLength(path.bio, 500),
6820
- ]
6821
-
6822
- // С кастомным сообщением
6823
- maxLength(path.bio, 500, { message: 'Максимум 500 символов' })
6824
- ```
6825
-
6826
- ```typescript
6827
- // Ошибка валидации
6828
- {
6829
- code: 'maxLength',
6830
- message: 'Максимальная длина: 500 символов',
6831
- params: { maxLength: 500, actualLength: 512 }
6832
- }
5957
+ validate(path.name, maxLength(50));
5958
+ validate(path.bio, maxLength(500, { message: 'Max 500 символов' }));
6833
5959
  ```
6834
5960
 
6835
5961
  _Source: src/core/validation/validators/max-length.ts_
@@ -6838,45 +5964,26 @@ _Source: src/core/validation/validators/max-length.ts_
6838
5964
 
6839
5965
  **Kind:** `function`
6840
5966
 
6841
- Валидатор минимального числового значения
5967
+ Фабрика валидатора минимального числового значения.
6842
5968
 
6843
- Проверяет, что числовое значение не меньше указанного минимума.
6844
5969
  Пустые значения пропускаются (используйте `required` для обязательности).
6845
5970
 
6846
5971
  **Signature:**
6847
5972
  ```typescript
6848
- export function min<TForm, TField extends number | null | undefined = number>(
6849
- fieldPath: FieldPathNode<TForm, TField> | undefined,
5973
+ export function min<TForm = unknown, TField extends number | undefined = number>(
6850
5974
  minValue: number,
6851
5975
  options?: ValidateOptions
6852
- ): void
5976
+ ): Validator<TForm, TField>
6853
5977
  ```
6854
5978
 
6855
- **Parameters:**
6856
- - `fieldPath` — - Путь к полю для валидации
6857
- - `minValue` — - Минимально допустимое значение
6858
- - `options` — - Опции валидации (message, params)
6859
-
6860
5979
  **Examples:**
6861
5980
 
6862
5981
  ```typescript
6863
- // Базовое использование
6864
- validationSchema: (path) => [
6865
- min(path.age, 18),
6866
- min(path.quantity, 1),
6867
- ]
6868
-
6869
- // С кастомным сообщением
6870
- min(path.age, 18, { message: 'Вам должно быть не менее 18 лет' })
6871
- ```
5982
+ import { validate } from '@reformer/core';
5983
+ import { min } from '@reformer/core/validators';
6872
5984
 
6873
- ```typescript
6874
- // Ошибка валидации
6875
- {
6876
- code: 'min',
6877
- message: 'Минимальное значение: 18',
6878
- params: { min: 18, actual: 16 }
6879
- }
5985
+ validate(path.age, min(18));
5986
+ validate(path.quantity, min(1, { message: 'Минимум 1' }));
6880
5987
  ```
6881
5988
 
6882
5989
  _Source: src/core/validation/validators/min.ts_
@@ -6885,138 +5992,304 @@ _Source: src/core/validation/validators/min.ts_
6885
5992
 
6886
5993
  **Kind:** `function`
6887
5994
 
6888
- Проверяет, что возраст (вычисленный из даты рождения) не меньше указанного
5995
+ Фабрика валидатора минимального возраста (по дате рождения).
6889
5996
 
6890
- Пустые значения и невалидные даты пропускаются (используйте `required` и `isDate`).
5997
+ Пустые и невалидные даты пропускаются.
6891
5998
 
6892
5999
  **Signature:**
6893
6000
  ```typescript
6894
- export function minAge<TForm, TField extends string | Date | null | undefined = string | Date>(
6895
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6896
- minAgeValue: number,
6897
- options?: ValidateOptions
6898
- ): void
6001
+ export function minAge<
6002
+ TForm = unknown,
6003
+ TField extends string | Date | null | undefined = string | Date,
6004
+ >(minAgeValue: number, options?: ValidateOptions): Validator<TForm, TField>
6899
6005
  ```
6900
6006
 
6901
- **Parameters:**
6902
- - `fieldPath` — - Путь к полю для валидации (дата рождения)
6903
- - `minAgeValue` — - Минимально допустимый возраст в годах
6904
- - `options` — - Опции валидации (message, params)
6905
-
6906
6007
  **Examples:**
6907
6008
 
6908
6009
  ```typescript
6909
- // Базовое использование - минимум 18 лет
6910
- validationSchema: (path) => [
6911
- minAge(path.birthDate, 18),
6912
- ]
6010
+ validate(path.birthDate, minAge(18));
6011
+ validate(path.birthDate, minAge(21, { message: 'Вам должно быть не менее 21 года' }));
6012
+ ```
6013
+
6014
+ _Source: src/core/validation/validators/min-age.ts_
6015
+
6016
+ ### minDate
6017
+
6018
+ **Kind:** `function`
6019
+
6020
+ Фабрика валидатора минимальной даты (включительно).
6913
6021
 
6914
- // С кастомным сообщением
6915
- minAge(path.birthDate, 21, { message: 'Вам должно быть не менее 21 года' })
6022
+ Пустые и невалидные даты пропускаются (используйте `required` и `isDate`).
6023
+
6024
+ **Signature:**
6025
+ ```typescript
6026
+ export function minDate<
6027
+ TForm = unknown,
6028
+ TField extends string | Date | null | undefined = string | Date,
6029
+ >(minDateValue: Date, options?: ValidateOptions): Validator<TForm, TField>
6916
6030
  ```
6917
6031
 
6032
+ **Examples:**
6033
+
6918
6034
  ```typescript
6919
- // Ошибка валидации
6920
- {
6921
- code: 'date_min_age',
6922
- message: 'Минимальный возраст: 18 лет',
6923
- params: { minAge: 18, currentAge: 16 }
6924
- }
6035
+ validate(path.eventDate, minDate(new Date('2024-01-01')));
6036
+ validate(path.startDate, minDate(new Date(), { message: 'Дата должна быть не раньше сегодня' }));
6925
6037
  ```
6926
6038
 
6927
- _Source: src/core/validation/validators/min-age.ts_
6039
+ _Source: src/core/validation/validators/min-date.ts_
6928
6040
 
6929
- ### minDate
6041
+ ### minLength
6930
6042
 
6931
6043
  **Kind:** `function`
6932
6044
 
6933
- Проверяет, что дата не раньше указанной минимальной
6045
+ Фабрика валидатора минимальной длины строки или массива.
6934
6046
 
6935
- Пустые значения и невалидные даты пропускаются (используйте `required` и `isDate`).
6047
+ Пустые значения пропускаются (используйте `required` для обязательности).
6936
6048
 
6937
6049
  **Signature:**
6938
6050
  ```typescript
6939
- export function minDate<TForm, TField extends string | Date | null | undefined = string | Date>(
6940
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6941
- minDateValue: Date,
6051
+ export function minLength<TForm = unknown, TField = unknown>(
6052
+ minLen: number,
6942
6053
  options?: ValidateOptions
6943
- ): void
6054
+ ): Validator<TForm, TField>
6944
6055
  ```
6945
6056
 
6946
- **Parameters:**
6947
- - `fieldPath` — - Путь к полю для валидации
6948
- - `minDateValue` — - Минимально допустимая дата (включительно)
6949
- - `options` — - Опции валидации (message, params)
6950
-
6951
6057
  **Examples:**
6952
6058
 
6953
6059
  ```typescript
6954
- // Базовое использование
6955
- validationSchema: (path) => [
6956
- minDate(path.eventDate, new Date('2024-01-01')),
6957
- ]
6060
+ validate(path.name, minLength(2));
6061
+ validate(path.password, minLength(8, { message: 'Min 8 символов' }));
6062
+ ```
6063
+
6064
+ _Source: src/core/validation/validators/min-length.ts_
6065
+
6066
+ ### ModelApi
6067
+
6068
+ **Kind:** `interface`
6958
6069
 
6959
- // С кастомным сообщением
6960
- minDate(path.startDate, new Date(), { message: 'Дата должна быть не раньше сегодня' })
6070
+ API уровня модели (доступно на корне и под-моделях элементов массива).
6071
+
6072
+ ⚠️ Имена методов (`$`/`get`/`set`/`patch`/`isDirty`/`reset`/`signalAt`/`captureInitial`)
6073
+ зарезервированы: одноимённое поле формы их затеняет (редкий краевой случай).
6074
+
6075
+ **Signature:**
6076
+ ```typescript
6077
+ export interface ModelApi<T> {
6078
+ /** Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`. */
6079
+ readonly $: ModelSignals<T>;
6080
+ /** Снимок значений (без подписки) — для submit. */
6081
+ get(): T;
6082
+ /** Массовая замена значений (load с сервера). Не меняет initial-снимок. */
6083
+ set(value: Partial<T>): void;
6084
+ /** Частичное слияние значений. */
6085
+ patch(value: Partial<T>): void;
6086
+ /** Отличаются ли текущие значения от initial-снимка (value-diff). */
6087
+ isDirty(): boolean;
6088
+ /** Сбросить значения к initial-снимку. */
6089
+ reset(): void;
6090
+ /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */
6091
+ captureInitial(): void;
6092
+ /** Резолв строкового пути в сигнал (для error-routing/мостов). */
6093
+ signalAt(path: string): PathAwareSignal<unknown> | undefined;
6094
+ }
6961
6095
  ```
6962
6096
 
6097
+ _Source: src/core/model/types.ts_
6098
+
6099
+ ### ModelArray
6100
+
6101
+ **Kind:** `interface`
6102
+
6103
+ Реактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;
6104
+ `map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.
6105
+
6106
+ **Signature:**
6963
6107
  ```typescript
6964
- // Ошибка валидации
6965
- {
6966
- code: 'date_min',
6967
- message: 'Дата должна быть не ранее 01.01.2024',
6968
- params: { minDate: Date }
6108
+ export interface ModelArray<U> {
6109
+ /** Реактивная длина. */
6110
+ readonly length: number;
6111
+ /** Добавить элемент в конец (значение элемента целиком). */
6112
+ push(item: U): void;
6113
+ /** Вставить элемент по индексу. */
6114
+ insertAt(index: number, item: U): void;
6115
+ /** Удалить элемент по индексу. */
6116
+ removeAt(index: number): void;
6117
+ /** Переместить элемент. */
6118
+ move(from: number, to: number): void;
6119
+ /** Поменять местами два элемента. */
6120
+ swap(a: number, b: number): void;
6121
+ /** Очистить массив. */
6122
+ clear(): void;
6123
+ /** Под-модель элемента по индексу (для объектных элементов) или значение (для примитивных). */
6124
+ at(index: number): U extends object ? FormModel<U> : U | undefined;
6125
+ /** Map по элементам: объектные → {@link FormModel}, примитивные → значение. */
6126
+ map<R>(fn: (item: U extends object ? FormModel<U> : U, index: number) => R): R[];
6127
+ /** Итерация по элементам. */
6128
+ forEach(fn: (item: U extends object ? FormModel<U> : U, index: number) => void): void;
6129
+ /** Снимок массива значений (без подписки). */
6130
+ toArray(): U[];
6131
+ /** Индексный value-доступ. */
6132
+ [index: number]: ModelValue<U>;
6969
6133
  }
6970
6134
  ```
6971
6135
 
6972
- _Source: src/core/validation/validators/min-date.ts_
6136
+ _Source: src/core/model/types.ts_
6973
6137
 
6974
- ### minLength
6138
+ ### ModelArrayControl
6975
6139
 
6976
- **Kind:** `function`
6140
+ **Kind:** `interface`
6977
6141
 
6978
- Валидатор минимальной длины строки
6142
+ Минимальный контракт реактивного массива модели, используемый узлом.
6979
6143
 
6980
- Проверяет, что длина строки не меньше указанного минимума.
6981
- Пустые значения пропускаются (используйте `required` для обязательности).
6144
+ **Signature:**
6145
+ ```typescript
6146
+ export interface ModelArrayControl<TItem extends object> {
6147
+ readonly length: number;
6148
+ at(index: number): FormModel<TItem> | undefined;
6149
+ push(item: TItem): void;
6150
+ insertAt(index: number, item: TItem): void;
6151
+ removeAt(index: number): void;
6152
+ move(from: number, to: number): void;
6153
+ swap(a: number, b: number): void;
6154
+ clear(): void;
6155
+ toArray(): TItem[];
6156
+ }
6157
+ ```
6158
+
6159
+ _Source: src/core/nodes/model-array-node.ts_
6160
+
6161
+ ### ModelArrayNode
6162
+
6163
+ **Kind:** `class`
6982
6164
 
6983
6165
  **Signature:**
6984
6166
  ```typescript
6985
- export function minLength<TForm, TField extends string | null | undefined = string>(
6986
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6987
- minLen: number,
6988
- options?: ValidateOptions
6989
- ): void
6167
+ export class ModelArrayNode<T extends object> extends FormNode<T[]> { /* */ }
6990
6168
  ```
6991
6169
 
6992
- **Parameters:**
6993
- - `fieldPath` — - Путь к полю для валидации
6994
- - `minLen` — - Минимальная допустимая длина строки
6995
- - `options` — - Опции валидации (message, params)
6170
+ _Source: src/core/nodes/model-array-node.ts_
6996
6171
 
6997
- **Examples:**
6172
+ ### ModelObject
6173
+
6174
+ **Kind:** `type`
6175
+
6176
+ value-proxy объекта: поля доступны как обычные свойства (чтение реактивно
6177
+ внутри `effect`/`computed`, запись — присваиванием).
6998
6178
 
6179
+ **Signature:**
6999
6180
  ```typescript
7000
- // Базовое использование
7001
- validationSchema: (path) => [
7002
- minLength(path.name, 2),
7003
- minLength(path.password, 8),
7004
- ]
6181
+ export type ModelObject<T> = {
6182
+ [K in keyof T]: ModelValue<T[K]>;
6183
+ };
6184
+ ```
6185
+
6186
+ _Source: src/core/model/types.ts_
6187
+
6188
+ ### ModelSignals
6189
+
6190
+ **Kind:** `type`
6191
+
6192
+ Дерево сигналов (escape-hatch `model.$`): листья → {@link PathAwareSignal},
6193
+ объекты → вложенное дерево, массивы → индексируемое дерево под-сигналов.
7005
6194
 
7006
- // С кастомным сообщением
7007
- minLength(path.password, 8, { message: 'Пароль должен быть не менее 8 символов' })
6195
+ **Signature:**
6196
+ ```typescript
6197
+ export type ModelSignals<T> = {
6198
+ [K in keyof T]: NonNullable<T[K]> extends ReadonlyArray<infer U>
6199
+ ? { readonly length: number; readonly [index: number]: ModelSignals<U & object> }
6200
+ : NonNullable<T[K]> extends Opaque
6201
+ ? PathAwareSignal<T[K]>
6202
+ : NonNullable<T[K]> extends object
6203
+ ? ModelSignals<NonNullable<T[K]>>
6204
+ : PathAwareSignal<T[K]>;
6205
+ };
7008
6206
  ```
7009
6207
 
6208
+ _Source: src/core/model/types.ts_
6209
+
6210
+ ### ModelValidationResult
6211
+
6212
+ **Kind:** `interface`
6213
+
6214
+ Результат валидации: ошибки по пути поля.
6215
+
6216
+ **Signature:**
7010
6217
  ```typescript
7011
- // Ошибка валидации
7012
- {
7013
- code: 'minLength',
7014
- message: 'Минимальная длина: 8 символов',
7015
- params: { minLength: 8, actualLength: 3 }
6218
+ export interface ModelValidationResult {
6219
+ valid: boolean;
6220
+ /** Ключ — путь поля (`'coBorrowers.0.relationship'`). Только поля с ошибками. */
6221
+ errors: Record<string, ValidationError[]>;
7016
6222
  }
7017
6223
  ```
7018
6224
 
7019
- _Source: src/core/validation/validators/min-length.ts_
6225
+ _Source: src/core/model/validate-model.ts_
6226
+
6227
+ ### ModelValidator
6228
+
6229
+ **Kind:** `type`
6230
+
6231
+ Валидатор слоя данных. `value` — значение поля; `model` — ближайший scope (под-модель элемента
6232
+ массива или корень); `root` — корневая модель.
6233
+
6234
+ **Signature:**
6235
+ ```typescript
6236
+ export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
6237
+ value: TValue,
6238
+ model: TModel,
6239
+ root: TRoot
6240
+ ) => ValidationError | null | Promise<ValidationError | null>;
6241
+ ```
6242
+
6243
+ _Source: src/core/model/validate-model.ts_
6244
+
6245
+ ### ModelValue
6246
+
6247
+ **Kind:** `type`
6248
+
6249
+ Значение поля в value-доступе модели:
6250
+ - массив → {@link ModelArray}
6251
+ - спец-объект (Date/File/Blob) → как есть
6252
+ - объект → вложенный value-proxy
6253
+ - примитив → значение
6254
+
6255
+ **Signature:**
6256
+ ```typescript
6257
+ export type ModelValue<V> =
6258
+ NonNullable<V> extends ReadonlyArray<infer U>
6259
+ ? ModelArray<U>
6260
+ : NonNullable<V> extends Opaque
6261
+ ? V
6262
+ : NonNullable<V> extends object
6263
+ ? ModelObject<NonNullable<V>>
6264
+ : V;
6265
+ ```
6266
+
6267
+ _Source: src/core/model/types.ts_
6268
+
6269
+ ### multipleOf
6270
+
6271
+ **Kind:** `function`
6272
+
6273
+ Фабрика валидатора, проверяющего что число кратно заданному.
6274
+
6275
+ Пустые значения и не-числа пропускаются (используйте `required` и `isNumber`).
6276
+
6277
+ **Signature:**
6278
+ ```typescript
6279
+ export function multipleOf<TForm = unknown, TField extends number | undefined = number>(
6280
+ divisor: number,
6281
+ options?: ValidateOptions
6282
+ ): Validator<TForm, TField>
6283
+ ```
6284
+
6285
+ **Examples:**
6286
+
6287
+ ```typescript
6288
+ validate(path.price, multipleOf(0.01));
6289
+ validate(path.rating, multipleOf(0.5, { message: 'Только шаг 0.5' }));
6290
+ ```
6291
+
6292
+ _Source: src/core/validation/validators/multiple-of.ts_
7020
6293
 
7021
6294
  ### NodeFactory
7022
6295
 
@@ -7087,79 +6360,53 @@ const node = NodeFactory.create({ value: '' }); // → FieldNode<string>
7087
6360
 
7088
6361
  _Source: src/core/factories/node-factory.ts_
7089
6362
 
7090
- ### notEmpty
6363
+ ### nonNegative
7091
6364
 
7092
6365
  **Kind:** `function`
7093
6366
 
7094
- Проверить что массив содержит хотя бы один элемент
6367
+ Фабрика валидатора, проверяющего что число неотрицательное (`≥ 0`).
7095
6368
 
7096
- Это удобный алиас для `minLength(field, 1)`, оптимизированный для массивов.
6369
+ Пустые значения и не-числа пропускаются (используйте `required` и `isNumber`).
7097
6370
 
7098
6371
  **Signature:**
7099
6372
  ```typescript
7100
- export function notEmpty<TForm, TItem>(
7101
- fieldPath: FieldPathNode<TForm, TItem[] | undefined> | undefined,
6373
+ export function nonNegative<TForm = unknown, TField extends number | undefined = number>(
7102
6374
  options?: ValidateOptions
7103
- ): void
6375
+ ): Validator<TForm, TField>
7104
6376
  ```
7105
6377
 
7106
- **Parameters:**
7107
- - `fieldPath` — - Поле-массив для валидации
7108
- - `options` — - Опции валидации (message, params и т.д.)
7109
-
7110
6378
  **Examples:**
7111
6379
 
7112
6380
  ```typescript
7113
- // Простая проверка
7114
- notEmpty(path.properties, { message: 'Добавьте хотя бы один объект имущества' });
7115
-
7116
- // С дополнительными параметрами
7117
- notEmpty(path.coBorrowers, {
7118
- message: 'Требуется хотя бы один созаемщик',
7119
- params: { minItems: 1 }
7120
- });
6381
+ validate(path.quantity, nonNegative());
6382
+ validate(path.balance, nonNegative({ message: 'Баланс не может быть отрицательным' }));
7121
6383
  ```
7122
6384
 
7123
- _Source: src/core/validation/validators/array-validators.ts_
6385
+ _Source: src/core/validation/validators/non-negative.ts_
7124
6386
 
7125
- ### number
6387
+ ### nonZero
7126
6388
 
7127
6389
  **Kind:** `function`
7128
6390
 
7129
- Адаптер для number валидатора
7130
- Проверяет, что значение является числом и соответствует заданным ограничениям
6391
+ Фабрика валидатора, проверяющего что число не равно нулю.
6392
+
6393
+ Пустые значения и не-числа пропускаются (используйте `required` и `isNumber`).
7131
6394
 
7132
6395
  **Signature:**
7133
6396
  ```typescript
7134
- export function number<TForm, TField extends number | undefined = number>(
7135
- fieldPath: FieldPathNode<TForm, TField> | undefined,
7136
- options?: ValidateOptions & {
7137
- /** Минимальное значение (включительно) */
7138
- min?: number;
7139
- /** Максимальное значение (включительно) */
7140
- max?: number;
7141
- /** Требовать целое число */
7142
- integer?: boolean;
7143
- /** Значение должно быть кратно этому числу */
7144
- multipleOf?: number;
7145
- /** Разрешить отрицательные числа (по умолчанию true) */
7146
- allowNegative?: boolean;
7147
- /** Разрешить ноль (по умолчанию true) */
7148
- allowZero?: boolean;
7149
- }
7150
- ): void
6397
+ export function nonZero<TForm = unknown, TField extends number | undefined = number>(
6398
+ options?: ValidateOptions
6399
+ ): Validator<TForm, TField>
7151
6400
  ```
7152
6401
 
7153
6402
  **Examples:**
7154
6403
 
7155
6404
  ```typescript
7156
- number(path.age);
7157
- number(path.price, { min: 0, max: 1000000 });
7158
- number(path.percentage, { min: 0, max: 100, integer: true });
7159
- number(path.rating, { min: 0, max: 5, multipleOf: 0.5 });
6405
+ validate(path.quantity, nonZero());
6406
+ validate(path.divisor, nonZero({ message: 'Не может быть нулём' }));
7160
6407
  ```
7161
6408
 
7162
- _Source: src/core/validation/validators/number.ts_
6409
+ _Source: src/core/validation/validators/non-zero.ts_
7163
6410
 
7164
6411
  ### ObservableForm
7165
6412
 
@@ -7204,128 +6451,63 @@ _Source: src/core/utils/form-observer.ts_
7204
6451
 
7205
6452
  **Kind:** `function`
7206
6453
 
7207
- Проверяет, что дата находится в прошлом (не в будущем)
6454
+ Фабрика валидатора, проверяющего что дата не в будущем.
7208
6455
 
7209
- Пустые значения и невалидные даты пропускаются (используйте `required` и `isDate`).
6456
+ Пустые и невалидные даты пропускаются.
7210
6457
 
7211
6458
  **Signature:**
7212
6459
  ```typescript
7213
- export function pastDate<TForm, TField extends string | Date | undefined = string | Date>(
7214
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6460
+ export function pastDate<TForm = unknown, TField extends string | Date | undefined = string | Date>(
7215
6461
  options?: ValidateOptions
7216
- ): void
6462
+ ): Validator<TForm, TField>
7217
6463
  ```
7218
6464
 
7219
- **Parameters:**
7220
- - `fieldPath` — - Путь к полю для валидации
7221
- - `options` — - Опции валидации (message, params)
7222
-
7223
6465
  **Examples:**
7224
6466
 
7225
6467
  ```typescript
7226
- // Базовое использование - дата рождения не может быть в будущем
7227
- validationSchema: (path) => [
7228
- pastDate(path.birthDate),
7229
- ]
7230
-
7231
- // С кастомным сообщением
7232
- pastDate(path.birthDate, { message: 'Дата рождения не может быть в будущем' })
7233
- ```
7234
-
7235
- ```typescript
7236
- // Ошибка валидации
7237
- {
7238
- code: 'date_future',
7239
- message: 'Дата не может быть в будущем',
7240
- params: {}
7241
- }
6468
+ validate(path.birthDate, pastDate({ message: 'Дата рождения не может быть в будущем' }));
7242
6469
  ```
7243
6470
 
7244
6471
  _Source: src/core/validation/validators/past-date.ts_
7245
6472
 
7246
- ### PathSegment
6473
+ ### PathAwareSignal
7247
6474
 
7248
- **Kind:** `interface`
7249
-
7250
- Сегмент пути к полю формы
6475
+ **Kind:** `type`
7251
6476
 
7252
- Представляет один сегмент в пути к полю, например:
7253
- - `"email" { key: 'email' }`
7254
- - `"items[0]" → { key: 'items', index: 0 }`
6477
+ Сигнал, который знает свой путь в модели (`'personalData.lastName'`).
6478
+ Используется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.
7255
6479
 
7256
6480
  **Signature:**
7257
6481
  ```typescript
7258
- export interface PathSegment {
7259
- /**
7260
- * Ключ поля
7261
- */
7262
- key: string;
7263
-
7264
- /**
7265
- * Индекс в массиве (опционально)
7266
- * Присутствует только для сегментов вида "items[0]"
7267
- */
7268
- index?: number;
7269
- }
7270
- ```
7271
-
7272
- **Examples:**
7273
-
7274
- ```typescript
7275
- // Путь "items[0].name" разбивается на:
7276
- [
7277
- { key: 'items', index: 0 },
7278
- { key: 'name' }
7279
- ]
6482
+ export type PathAwareSignal<T> = Signal<T> & {
6483
+ /** Путь поля в модели (dot-нотация). На элементах массива включает индекс. */
6484
+ readonly __path: string;
6485
+ };
7280
6486
  ```
7281
6487
 
7282
- _Source: src/core/utils/field-path-navigator.ts_
6488
+ _Source: src/core/model/types.ts_
7283
6489
 
7284
6490
  ### pattern
7285
6491
 
7286
6492
  **Kind:** `function`
7287
6493
 
7288
- Валидатор паттерна (регулярного выражения)
6494
+ Фабрика валидатора регулярного выражения.
7289
6495
 
7290
- Проверяет, что значение соответствует указанному регулярному выражению.
7291
6496
  Пустые значения пропускаются (используйте `required` для обязательности).
7292
6497
 
7293
6498
  **Signature:**
7294
6499
  ```typescript
7295
- export function pattern<TForm, TField extends string | null | undefined = string>(
7296
- fieldPath: FieldPathNode<TForm, TField> | undefined,
6500
+ export function pattern<TForm = unknown, TField extends string | undefined = string>(
7297
6501
  regex: RegExp,
7298
6502
  options?: ValidateOptions
7299
- ): void
6503
+ ): Validator<TForm, TField>
7300
6504
  ```
7301
6505
 
7302
- **Parameters:**
7303
- - `fieldPath` — - Путь к полю для валидации
7304
- - `regex` — - Регулярное выражение для проверки
7305
- - `options` — - Опции валидации (message, params)
7306
-
7307
6506
  **Examples:**
7308
6507
 
7309
6508
  ```typescript
7310
- // Только буквы
7311
- pattern(path.name, /^[а-яА-Яa-zA-Z]+$/)
7312
-
7313
- // Только цифры
7314
- pattern(path.code, /^\d+$/)
7315
-
7316
- // С кастомным сообщением
7317
- pattern(path.username, /^[a-z0-9_]+$/i, {
7318
- message: 'Только латинские буквы, цифры и подчёркивание'
7319
- })
7320
- ```
7321
-
7322
- ```typescript
7323
- // Ошибка валидации
7324
- {
7325
- code: 'pattern',
7326
- message: 'Значение не соответствует требуемому формату',
7327
- params: { pattern: '^[a-z]+$' }
7328
- }
6509
+ validate(path.name, pattern(/^[а-яА-Яa-zA-Z]+$/));
6510
+ validate(path.code, pattern(/^\d+$/, { message: 'Только цифры' }));
7329
6511
  ```
7330
6512
 
7331
6513
  _Source: src/core/validation/validators/pattern.ts_
@@ -7334,26 +6516,22 @@ _Source: src/core/validation/validators/pattern.ts_
7334
6516
 
7335
6517
  **Kind:** `function`
7336
6518
 
7337
- Адаптер для phone валидатора
7338
- Поддерживает опциональные поля (string | undefined)
6519
+ Фабрика валидатора номера телефона.
6520
+
6521
+ Пустые значения пропускаются (используйте `required` для обязательности).
7339
6522
 
7340
6523
  **Signature:**
7341
6524
  ```typescript
7342
- export function phone<TForm, TField extends string | null | undefined = string>(
7343
- fieldPath: FieldPathNode<TForm, TField> | undefined,
7344
- options?: ValidateOptions & {
7345
- /** Формат телефона */
7346
- format?: PhoneFormat;
7347
- }
7348
- ): void
6525
+ export function phone<TForm = unknown, TField extends string | undefined = string>(
6526
+ options?: PhoneValidatorOptions
6527
+ ): Validator<TForm, TField>
7349
6528
  ```
7350
6529
 
7351
6530
  **Examples:**
7352
6531
 
7353
6532
  ```typescript
7354
- phone(path.phoneNumber);
7355
- phone(path.phoneNumber, { format: 'ru' });
7356
- phone(path.phoneNumber, { format: 'international', message: 'Неверный формат телефона' });
6533
+ validate(path.phone, phone());
6534
+ validate(path.phone, phone({ format: 'ru' }));
7357
6535
  ```
7358
6536
 
7359
6537
  _Source: src/core/validation/validators/phone.ts_
@@ -7362,8 +6540,6 @@ _Source: src/core/validation/validators/phone.ts_
7362
6540
 
7363
6541
  **Kind:** `type`
7364
6542
 
7365
- Формат телефона для валидации
7366
-
7367
6543
  **Signature:**
7368
6544
  ```typescript
7369
6545
  export type PhoneFormat = 'international' | 'ru' | 'us' | 'any';
@@ -7371,6 +6547,19 @@ export type PhoneFormat = 'international' | 'ru' | 'us' | 'any';
7371
6547
 
7372
6548
  _Source: src/core/validation/validators/phone.ts_
7373
6549
 
6550
+ ### registerSignalNode
6551
+
6552
+ **Kind:** `function`
6553
+
6554
+ Связать сигнал модели с его FieldNode.
6555
+
6556
+ **Signature:**
6557
+ ```typescript
6558
+ export function registerSignalNode(signal: Signal<any>, node: FormNode<any>): void
6559
+ ```
6560
+
6561
+ _Source: src/core/utils/signal-node-registry.ts_
6562
+
7374
6563
  ### RegistryStack
7375
6564
 
7376
6565
  **Kind:** `class`
@@ -7411,131 +6600,55 @@ _Source: src/core/utils/registry-stack.ts_
7411
6600
 
7412
6601
  **Kind:** `function`
7413
6602
 
7414
- Валидатор обязательного поля
6603
+ Фабрика валидатора обязательного поля.
6604
+
6605
+ Возвращает чистую функцию-валидатор `(value, control, root)`. Передаётся в `validate()`.
7415
6606
 
7416
- Проверяет, что поле имеет непустое значение.
7417
6607
  Пустыми считаются: `null`, `undefined`, `''` (пустая строка).
7418
6608
  Для boolean полей требуется значение `true`.
7419
6609
 
7420
6610
  **Signature:**
7421
6611
  ```typescript
7422
- export function required<TForm, TField>(
7423
- fieldPath: FieldPathNode<TForm, TField> | undefined,
7424
- options?: ValidateOptions
7425
- ): void
7426
- ```
7427
-
7428
- **Parameters:**
7429
- - `fieldPath` — - Путь к полю для валидации
7430
- - `options` — - Опции валидации (message, params)
7431
-
7432
- **Examples:**
7433
-
7434
- ```typescript
7435
- // Базовое использование
7436
- validationSchema: (path, { validate }) => [
7437
- required(path.name),
7438
- required(path.email),
7439
- ]
7440
-
7441
- // С кастомным сообщением
7442
- required(path.phone, { message: 'Укажите номер телефона' })
7443
-
7444
- // Для checkbox (требует true)
7445
- required(path.agreeToTerms, { message: 'Необходимо принять условия' })
7446
- ```
7447
-
7448
- ```typescript
7449
- // Ошибка валидации
7450
- {
7451
- code: 'required',
7452
- message: 'Поле обязательно для заполнения',
7453
- params: {}
7454
- }
7455
- ```
7456
-
7457
- _Source: src/core/validation/validators/required.ts_
7458
-
7459
- ### resetWhen
7460
-
7461
- **Kind:** `function`
7462
-
7463
- Условный сброс поля при выполнении условия
7464
-
7465
- **Signature:**
7466
- ```typescript
7467
- export function resetWhen<TForm extends FormFields>(
7468
- field: FieldPathNode<TForm, FormValue>,
7469
- condition: (form: TForm) => boolean,
7470
- options?: ResetWhenOptions & { debounce?: number }
7471
- ): void
6612
+ export function required<TForm = unknown, TField = unknown>(
6613
+ options?: ValidateOptions
6614
+ ): Validator<TForm, TField>
7472
6615
  ```
7473
6616
 
7474
- **Parameters:**
7475
- - `field` — - Поле для сброса
7476
- - `condition` — - Функция условия (true = reset)
7477
- - `options` — - Опции (`resetValue`, `onlyIfDirty`, `debounce`)
7478
-
7479
6617
  **Examples:**
7480
6618
 
7481
- Сброс зависимого поля при смене типа платежа
7482
6619
  ```typescript
7483
- import { resetWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
6620
+ import { validate } from '@reformer/core';
6621
+ import { required } from '@reformer/core/validators';
7484
6622
 
7485
- interface CheckoutForm {
7486
- paymentType: 'card' | 'cash';
7487
- cardNumber: string;
7488
- }
7489
-
7490
- export const checkoutBehavior: BehaviorSchemaFn<CheckoutForm> = (path) => {
7491
- // Когда выбрано НЕ card — обнуляем номер карты в пустую строку
7492
- resetWhen(path.cardNumber, (form) => form.paymentType !== 'card', {
7493
- resetValue: '',
7494
- });
7495
- };
6623
+ validate(path.email, required());
6624
+ validate(path.phone, required({ message: 'Укажите номер телефона' }));
6625
+ validate(path.agreeToTerms, required({ message: 'Необходимо принять условия' }));
7496
6626
  ```
7497
6627
 
7498
- `onlyIfDirty` — не трогаем нетронутые initial значения
7499
- ```typescript
7500
- import { resetWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
7501
-
7502
- interface CarForm {
7503
- loanType: 'mortgage' | 'car' | 'consumer';
7504
- carPrice: number;
7505
- }
7506
-
7507
- export const carBehavior: BehaviorSchemaFn<CarForm> = (path) => {
7508
- // Если пользователь не вводил carPrice — оставляем default из схемы.
7509
- // Сбрасываем только если поле dirty (была пользовательская правка).
7510
- resetWhen(path.carPrice, (form) => form.loanType !== 'car', {
7511
- resetValue: 0,
7512
- onlyIfDirty: true,
7513
- });
7514
- };
7515
- ```
6628
+ _Source: src/core/validation/validators/required.ts_
7516
6629
 
7517
- **See also:**
7518
- - [docs/llms/25-reset-when.md](../../../../docs/llms/25-reset-when.md)
6630
+ ### resetWhen
7519
6631
 
7520
- _Source: src/core/behavior/behaviors/reset-when.ts_
6632
+ **Kind:** `function`
7521
6633
 
7522
- ### ResetWhenOptions
6634
+ Сброс значения поля к `resetValue` (по умолчанию `null`), когда `condition` истинно.
7523
6635
 
7524
- **Kind:** `interface`
6636
+ **Signature:**
6637
+ ```typescript
6638
+ export function resetWhen<T>(
6639
+ target: Signal<T>,
6640
+ condition: () => boolean,
6641
+ options?: { resetValue?: T }
6642
+ ): BehaviorCleanup
6643
+ ```
7525
6644
 
7526
- Опции для resetWhen
6645
+ **Examples:**
7527
6646
 
7528
- **Signature:**
7529
6647
  ```typescript
7530
- export interface ResetWhenOptions {
7531
- /** Значение для сброса (по умолчанию null) */
7532
- resetValue?: FormValue;
7533
- /** Сбросить только если поле dirty */
7534
- onlyIfDirty?: boolean;
7535
- }
6648
+ resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
7536
6649
  ```
7537
6650
 
7538
- _Source: src/core/behavior/behaviors/reset-when.ts_
6651
+ _Source: src/core/model/behaviors.ts_
7539
6652
 
7540
6653
  ### ResourceLoadResult
7541
6654
 
@@ -7554,82 +6667,24 @@ _Source: src/core/types/index.ts_
7554
6667
 
7555
6668
  **Kind:** `function`
7556
6669
 
7557
- Перевалидирует поле при изменении других полей
6670
+ Вызывает `revalidate()` при изменении зависимостей (не на инициализации). Под M1 валидация
6671
+ on-demand (`validateFormModel`), поэтому ревалидация выражается явным колбэком.
7558
6672
 
7559
6673
  **Signature:**
7560
6674
  ```typescript
7561
- export function revalidateWhen<TForm>(
7562
- target: FieldPathNode<TForm, FormValue>,
7563
- triggers: FieldPathNode<TForm, FormValue>[],
7564
- options?: RevalidateWhenOptions
7565
- ): void
6675
+ export function revalidateWhen(
6676
+ deps: ReadonlySignal<unknown>[],
6677
+ revalidate: () => void
6678
+ ): BehaviorCleanup
7566
6679
  ```
7567
6680
 
7568
- **Parameters:**
7569
- - `target` — - Поле для перевалидации
7570
- - `triggers` — - Поля-триггеры (НЕ должно содержать `target`)
7571
- - `options` — - Опции (`debounce`)
7572
-
7573
6681
  **Examples:**
7574
6682
 
7575
- Парная перевалидация — confirmPassword при смене password
7576
- ```typescript
7577
- import { revalidateWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
7578
- import { equalTo } from '@reformer/core/validators';
7579
- import type { FieldPath } from '@reformer/core';
7580
-
7581
- interface RegistrationForm { password: string; confirmPassword: string }
7582
-
7583
- export const validation = (path: FieldPath<RegistrationForm>) => {
7584
- equalTo(path.confirmPassword, path.password, { message: 'Пароли не совпадают' });
7585
- };
7586
-
7587
- export const behavior: BehaviorSchemaFn<RegistrationForm> = (path) => {
7588
- // Если пользователь сначала ввёл confirm, потом меняет password —
7589
- // без revalidateWhen ошибка confirmPassword останется устаревшей.
7590
- revalidateWhen(path.confirmPassword, [path.password]);
7591
- };
7592
- ```
7593
-
7594
- Несколько триггеров + `debounce` для async-валидаторов
7595
- ```typescript
7596
- import { revalidateWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
7597
-
7598
- interface MortgageForm {
7599
- propertyValue: number;
7600
- loanAmount: number;
7601
- initialPayment: number; // правило: initialPayment >= propertyValue * 0.2 - loanAmount
7602
- }
7603
-
7604
- export const mortgageBehavior: BehaviorSchemaFn<MortgageForm> = (path) => {
7605
- revalidateWhen(
7606
- path.initialPayment,
7607
- [path.propertyValue, path.loanAmount],
7608
- { debounce: 300 }, // не дёргаем сервер на каждый keystroke
7609
- );
7610
- };
7611
- ```
7612
-
7613
- **See also:**
7614
- - [docs/llms/27-revalidate-when.md](../../../../docs/llms/27-revalidate-when.md)
7615
-
7616
- _Source: src/core/behavior/behaviors/revalidate-when.ts_
7617
-
7618
- ### RevalidateWhenOptions
7619
-
7620
- **Kind:** `interface`
7621
-
7622
- Опции для revalidateWhen
7623
-
7624
- **Signature:**
7625
6683
  ```typescript
7626
- export interface RevalidateWhenOptions {
7627
- /** Debounce в мс */
7628
- debounce?: number;
7629
- }
6684
+ revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
7630
6685
  ```
7631
6686
 
7632
- _Source: src/core/behavior/types.ts_
6687
+ _Source: src/core/model/behaviors.ts_
7633
6688
 
7634
6689
  ### runOutsideEffect
7635
6690
 
@@ -7845,380 +6900,81 @@ _Source: src/core/utils/form-submitter.ts_
7845
6900
  - Автоматически заменять существующие подписки
7846
6901
  - Отслеживать количество активных подписок (для отладки)
7847
6902
 
7848
- **Signature:**
7849
- ```typescript
7850
- export class SubscriptionManager {
7851
- /**
7852
- * Хранилище подписок
7853
- * Ключ: уникальный идентификатор подписки
7854
- * Значение: функция отписки (dispose)
7855
- */ /* … */ }
7856
- ```
7857
-
7858
- **Examples:**
7859
-
7860
- ```typescript
7861
- class FieldNode {
7862
- private subscriptions = new SubscriptionManager();
7863
-
7864
- watch(callback: Function) {
7865
- const dispose = effect(() => callback(this.value.value));
7866
- return this.subscriptions.add('watch', dispose);
7867
- }
7868
-
7869
- dispose() {
7870
- this.subscriptions.clear();
7871
- }
7872
- }
7873
- ```
7874
-
7875
- _Source: src/core/utils/subscription-manager.ts_
7876
-
7877
- ### syncFields
7878
-
7879
- **Kind:** `function`
7880
-
7881
- Двусторонняя синхронизация двух полей
7882
-
7883
- **Signature:**
7884
- ```typescript
7885
- export function syncFields<TForm extends FormFields, T extends FormValue>(
7886
- field1: FieldPathNode<TForm, T>,
7887
- field2: FieldPathNode<TForm, T>,
7888
- options?: SyncFieldsOptions<T>
7889
- ): void
7890
- ```
7891
-
7892
- **Parameters:**
7893
- - `field1` — - Первое поле
7894
- - `field2` — - Второе поле
7895
- - `options` — - Опции (`transform` асимметричен — применяется только field1 → field2; `debounce`)
7896
-
7897
- **Examples:**
7898
-
7899
- Базовый mirror двух текстовых полей
7900
- ```typescript
7901
- import { syncFields, type BehaviorSchemaFn } from '@reformer/core/behaviors';
7902
-
7903
- interface MirrorForm {
7904
- syncField1: string;
7905
- syncField2: string;
7906
- }
7907
-
7908
- export const mirrorBehavior: BehaviorSchemaFn<MirrorForm> = (path) => {
7909
- syncFields(path.syncField1, path.syncField2);
7910
- };
7911
- ```
7912
-
7913
- С `transform` (асимметричный) и `debounce` для защиты от частых перезаписей
7914
- ```typescript
7915
- import { syncFields, type BehaviorSchemaFn } from '@reformer/core/behaviors';
7916
-
7917
- interface CodeForm {
7918
- internalCode: string; // канонический формат
7919
- displayCode: string; // показываем пользователю
7920
- }
7921
-
7922
- export const codeBehavior: BehaviorSchemaFn<CodeForm> = (path) => {
7923
- // internalCode → displayCode: применяется toUpperCase
7924
- // displayCode → internalCode: пишется как есть
7925
- syncFields(path.internalCode, path.displayCode, {
7926
- transform: (value) => (typeof value === 'string' ? value.toUpperCase() : value),
7927
- debounce: 150, // сглаживает дёргание каретки
7928
- });
7929
- };
7930
- ```
7931
-
7932
- **See also:**
7933
- - [docs/llms/24-sync-fields.md](../../../../docs/llms/24-sync-fields.md)
7934
-
7935
- _Source: src/core/behavior/behaviors/sync-fields.ts_
7936
-
7937
- ### SyncFieldsOptions
7938
-
7939
- **Kind:** `interface`
7940
-
7941
- Опции для syncFields
7942
-
7943
- **Signature:**
7944
- ```typescript
7945
- export interface SyncFieldsOptions<T> {
7946
- /** Трансформация значения */
7947
- transform?: (value: T) => T;
7948
-
7949
- /** Debounce в мс */
7950
- debounce?: number;
7951
- }
7952
- ```
7953
-
7954
- _Source: src/core/behavior/types.ts_
7955
-
7956
- ### toBehaviorFieldPath
7957
-
7958
- **Kind:** `function`
7959
-
7960
- Преобразовать FieldPath во вложенный путь для композиции behavior схем
7961
-
7962
- Аналог toFieldPath из validation API.
7963
-
7964
- **Signature:**
7965
- ```typescript
7966
- export function toBehaviorFieldPath<TForm, TField>(
7967
- fieldPath: FieldPathNode<TForm, TField> | undefined
7968
- ): FieldPath<TField>
7969
- ```
7970
-
7971
- **Parameters:**
7972
- - `fieldPath` — - Поле для преобразования
7973
-
7974
- **Returns:** Вложенный FieldPath
7975
-
7976
- **Examples:**
7977
-
7978
- ```typescript
7979
- // address-behavior.ts
7980
- export const addressBehavior = (path: FieldPath<Address>) => {
7981
- watchField(path.country, async (country, ctx) => {
7982
- const regions = await fetchRegions(country);
7983
- ctx.updateComponentProps(path.region, { options: regions });
7984
- });
7985
- };
7986
-
7987
- // user-behavior.ts
7988
- export const userBehavior = (path: FieldPath<User>) => {
7989
- // Композиция: применяем addressBehavior к вложенному полю
7990
- addressBehavior(toBehaviorFieldPath(path.address));
7991
- };
7992
- ```
7993
-
7994
- _Source: src/core/behavior/compose-behavior.ts_
7995
-
7996
- ### toFieldPath
7997
-
7998
- **Kind:** `function`
7999
-
8000
- Преобразовать FieldPathNode в FieldPath для переиспользования схем
8001
-
8002
- Позволяет композировать validation schemas:
8003
-
8004
- **Signature:**
8005
- ```typescript
8006
- export function toFieldPath<T>(
8007
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
8008
- node: FieldPathNode<unknown, T, never> | FieldPathNode<any, T, any>
8009
- ): FieldPath<T>
8010
- ```
8011
-
8012
- **Examples:**
8013
-
8014
- ```typescript
8015
- const personalDataValidation = (path: FieldPath<PersonalData>) => {
8016
- required(path.firstName, { message: 'Имя обязательно' });
8017
- required(path.lastName, { message: 'Фамилия обязательна' });
8018
- };
8019
-
8020
- const mainValidation = (path: FieldPath<MyForm>) => {
8021
- // Переиспользуем схему
8022
- personalDataValidation(toFieldPath(path.personalData));
8023
- required(path.email);
8024
- };
8025
- ```
8026
-
8027
- _Source: src/core/utils/field-path.ts_
8028
-
8029
- ### transformers
8030
-
8031
- **Kind:** `const`
8032
-
8033
- Готовые трансформеры для частых случаев. Все идемпотентны и безопасны для повторного применения.
8034
-
8035
- **Signature:**
8036
- ```typescript
8037
- export const transformers
8038
- ```
8039
-
8040
- **Examples:**
8041
-
8042
- Готовые трансформеры в схеме формы
8043
- ```typescript
8044
- import { transformers, type BehaviorSchemaFn } from '@reformer/core/behaviors';
8045
-
8046
- interface RegistrationForm {
8047
- username: string;
8048
- email: string;
8049
- promoCode: string;
8050
- inn: string;
8051
- amount: number;
8052
- }
8053
-
8054
- export const behavior: BehaviorSchemaFn<RegistrationForm> = (path) => {
8055
- transformers.trim(path.username);
8056
- transformers.toLowerCase(path.email);
8057
- transformers.toUpperCase(path.promoCode);
8058
- transformers.digitsOnly(path.inn);
8059
- transformers.roundTo2(path.amount);
8060
- };
8061
- ```
8062
-
8063
- Композиция готовых трансформеров через createTransformer
8064
- ```typescript
8065
- import { createTransformer, type BehaviorSchemaFn } from '@reformer/core/behaviors';
8066
-
8067
- // trim + lowercase в одном трансформере (применяется как одна операция)
8068
- const normalizeEmail = createTransformer<string>(
8069
- (v) => (v ?? '').trim().toLowerCase(),
8070
- );
8071
-
8072
- interface ContactForm { email: string }
8073
-
8074
- export const contactBehavior: BehaviorSchemaFn<ContactForm> = (path) => {
8075
- normalizeEmail(path.email);
8076
- };
8077
- ```
8078
-
8079
- _Source: src/core/behavior/behaviors/transform-value.ts_
8080
-
8081
- ### transformValue
8082
-
8083
- **Kind:** `function`
8084
-
8085
- Трансформация значения поля при изменении
8086
- Позволяет автоматически форматировать или преобразовывать значения
8087
-
8088
- **Signature:**
8089
- ```typescript
8090
- export function transformValue<TForm extends FormFields, TValue extends FormValue = FormValue>(
8091
- field: FieldPathNode<TForm, TValue>,
8092
- transformer: (value: TValue) => TValue,
8093
- options?: TransformValueOptions & { debounce?: number }
8094
- ): void
8095
- ```
8096
-
8097
- **Parameters:**
8098
- - `field` — - Поле для трансформации
8099
- - `transformer` — - Функция трансформации (ОБЯЗАТЕЛЬНО идемпотентная: f(f(x)) === f(x))
8100
- - `options` — - Опции (`onUserChangeOnly`, `emitEvent`, `debounce`)
8101
-
8102
- **Examples:**
8103
-
8104
- Базовая нормализация — uppercase + trim email
8105
- ```typescript
8106
- import { transformValue, type BehaviorSchemaFn } from '@reformer/core/behaviors';
8107
-
8108
- interface RegistrationForm {
8109
- promoCode: string;
8110
- email: string;
8111
- }
8112
-
8113
- export const registrationBehavior: BehaviorSchemaFn<RegistrationForm> = (path) => {
8114
- // Идемпотентно: toUpperCase(toUpperCase(x)) === toUpperCase(x) ✓
8115
- transformValue(path.promoCode, (value) => (value ?? '').toUpperCase());
8116
- transformValue(path.email, (value) => (value ?? '').trim().toLowerCase());
8117
- };
8118
- ```
8119
-
8120
- `onUserChangeOnly` + idempotent guard для не-тривиальных форматов
8121
- ```typescript
8122
- import { transformValue, type BehaviorSchemaFn } from '@reformer/core/behaviors';
8123
-
8124
- interface ProfileForm {
8125
- inn: string; // ИНН — только цифры
8126
- prefixedCode: string; // должен иметь префикс "ID-"
8127
- }
8128
-
8129
- export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
8130
- // Цифры из ИНН: трансформер идемпотентный естественно
8131
- transformValue(path.inn, (v) => (v ?? '').replace(/\D/g, ''));
8132
-
8133
- // Префикс — ВАЖНО guard «уже преобразовано», иначе бесконечный цикл
8134
- // f("ID-123") должно === "ID-123", а не "ID-ID-123"
8135
- transformValue(
8136
- path.prefixedCode,
8137
- (v) => (v?.startsWith('ID-') ? v : `ID-${v ?? ''}`),
8138
- {
8139
- onUserChangeOnly: true, // не трогаем значение из patchValue/preload
8140
- debounce: 200,
8141
- },
8142
- );
8143
- };
8144
- ```
8145
-
8146
- **See also:**
8147
- - [docs/llms/26-transform-value.md](../../../../docs/llms/26-transform-value.md)
8148
-
8149
- _Source: src/core/behavior/behaviors/transform-value.ts_
6903
+ **Signature:**
6904
+ ```typescript
6905
+ export class SubscriptionManager {
6906
+ /**
6907
+ * Хранилище подписок
6908
+ * Ключ: уникальный идентификатор подписки
6909
+ * Значение: функция отписки (dispose)
6910
+ */ /* … */ }
6911
+ ```
8150
6912
 
8151
- ### TransformValueOptions
6913
+ **Examples:**
8152
6914
 
8153
- **Kind:** `interface`
6915
+ ```typescript
6916
+ class FieldNode {
6917
+ private subscriptions = new SubscriptionManager();
8154
6918
 
8155
- Опции для transformValue
6919
+ watch(callback: Function) {
6920
+ const dispose = effect(() => callback(this.value.value));
6921
+ return this.subscriptions.add('watch', dispose);
6922
+ }
8156
6923
 
8157
- **Signature:**
8158
- ```typescript
8159
- export interface TransformValueOptions {
8160
- /** Трансформировать только при изменении пользователем (не программно) */
8161
- onUserChangeOnly?: boolean;
8162
- /** Триггерить событие изменения после трансформации */
8163
- emitEvent?: boolean;
6924
+ dispose() {
6925
+ this.subscriptions.clear();
6926
+ }
8164
6927
  }
8165
6928
  ```
8166
6929
 
8167
- _Source: src/core/behavior/behaviors/transform-value.ts_
6930
+ _Source: src/core/utils/subscription-manager.ts_
8168
6931
 
8169
- ### TreeValidationContextImpl
6932
+ ### syncFields
8170
6933
 
8171
- **Kind:** `class`
6934
+ **Kind:** `function`
8172
6935
 
8173
- Контекст cross-field валидации. Передаётся в `validateTree`/`validateForm`
8174
- callback'и напрямую инстанцировать не нужно.
6936
+ Двусторонняя синхронизация двух полей (опционально с трансформом `a → b`).
6937
+ Сходимость обеспечивается peek-guard'ом; флаг предотвращает лишние круги.
8175
6938
 
8176
6939
  **Signature:**
8177
6940
  ```typescript
8178
- export class TreeValidationContextImpl<TForm> extends BaseValidationContext<TForm> { /* … */ }
6941
+ export function syncFields<T>(
6942
+ a: Signal<T>,
6943
+ b: Signal<T>,
6944
+ options?: { transform?: (value: T) => T }
6945
+ ): BehaviorCleanup
8179
6946
  ```
8180
6947
 
8181
6948
  **Examples:**
8182
6949
 
8183
6950
  ```typescript
8184
- import { validateForm } from '@reformer/core';
8185
-
8186
- validateForm(form, (ctx) => {
8187
- // ctx — экземпляр TreeValidationContextImpl
8188
- if (ctx.form.startDate.value > ctx.form.endDate.value) {
8189
- return [{ code: 'date-range', message: 'Дата начала позже даты окончания' }];
8190
- }
8191
- return [];
8192
- });
6951
+ syncFields(model.$.field1, model.$.field2);
8193
6952
  ```
8194
6953
 
8195
- _Source: src/core/validation/validation-context.ts_
6954
+ _Source: src/core/model/behaviors.ts_
8196
6955
 
8197
- ### TreeValidatorFn
6956
+ ### transformValue
8198
6957
 
8199
- **Kind:** `type`
6958
+ **Kind:** `function`
8200
6959
 
8201
- Функция cross-field валидации
6960
+ Трансформация значения поля (идемпотентная): при изменении пишет `transformer(value)` обратно.
6961
+ Запись отложена (`runOutsideEffect`) во избежание «Cycle detected» (эффект читает и пишет один сигнал).
8202
6962
 
8203
6963
  **Signature:**
8204
6964
  ```typescript
8205
- export type TreeValidatorFn<TForm> = (ctx: FormContext<TForm>) => ValidationError | null;
6965
+ export function transformValue<T>(
6966
+ target: Signal<T>,
6967
+ transformer: (value: T) => T
6968
+ ): BehaviorCleanup
8206
6969
  ```
8207
6970
 
8208
6971
  **Examples:**
8209
6972
 
8210
6973
  ```typescript
8211
- validateTree((ctx) => {
8212
- const password = ctx.form.password.value.value;
8213
- const confirm = ctx.form.confirmPassword.value.value;
8214
- if (password !== confirm) {
8215
- return { code: 'mismatch', message: 'Passwords must match' };
8216
- }
8217
- return null;
8218
- });
6974
+ transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
8219
6975
  ```
8220
6976
 
8221
- _Source: src/core/types/validation-schema.ts_
6977
+ _Source: src/core/model/behaviors.ts_
8222
6978
 
8223
6979
  ### uniqueId
8224
6980
 
@@ -8293,28 +7049,23 @@ _Source: src/core/types/index.ts_
8293
7049
 
8294
7050
  **Kind:** `function`
8295
7051
 
8296
- Адаптер для URL валидатора
8297
- Поддерживает опциональные поля (string | undefined)
7052
+ Фабрика валидатора URL.
7053
+
7054
+ Пустые значения пропускаются (используйте `required` для обязательности).
8298
7055
 
8299
7056
  **Signature:**
8300
7057
  ```typescript
8301
- export function url<TForm, TField extends string | null | undefined = string>(
8302
- fieldPath: FieldPathNode<TForm, TField> | undefined,
8303
- options?: ValidateOptions & {
8304
- /** Требовать наличие протокола (http:// или https://) */
8305
- requireProtocol?: boolean;
8306
- /** Разрешенные протоколы */
8307
- allowedProtocols?: string[];
8308
- }
8309
- ): void
7058
+ export function url<TForm = unknown, TField extends string | undefined = string>(
7059
+ options?: UrlValidatorOptions
7060
+ ): Validator<TForm, TField>
8310
7061
  ```
8311
7062
 
8312
7063
  **Examples:**
8313
7064
 
8314
7065
  ```typescript
8315
- url(path.website);
8316
- url(path.website, { message: 'Введите корректный URL' });
8317
- url(path.website, { requireProtocol: true });
7066
+ validate(path.website, url());
7067
+ validate(path.website, url({ requireProtocol: true }));
7068
+ validate(path.website, url({ allowedProtocols: ['https'] }));
8318
7069
  ```
8319
7070
 
8320
7071
  _Source: src/core/validation/validators/url.ts_
@@ -8767,130 +7518,11 @@ return (
8767
7518
 
8768
7519
  _Source: src/hooks/useFormControlValue.ts_
8769
7520
 
8770
- ### useHiddenCondition
8771
-
8772
- **Kind:** `function`
8773
-
8774
- Хук для реактивной оценки функции hidden.
8775
-
8776
- Подписывается на изменения формы через сигналы и возвращает текущее
8777
- значение hidden-условия.
8778
-
8779
- **Signature:**
8780
- ```typescript
8781
- export function useHiddenCondition<T>(
8782
- hiddenFn: HiddenFn<T> | undefined,
8783
- form: FormProxy<T>,
8784
- path: FieldPath<T>
8785
- ): boolean
8786
- ```
8787
-
8788
- **Parameters:**
8789
- - `hiddenFn` — - Функция, определяющая скрытие.
8790
- - `form` — - {@link FormProxy} формы.
8791
- - `path` — - Текущий {@link FieldPath}.
8792
-
8793
- **Returns:** `true`, если элемент должен быть скрыт.
8794
-
8795
- **Examples:**
8796
-
8797
- ```tsx
8798
- import { useHiddenCondition } from '@reformer/core';
8799
-
8800
- function MaybeHidden({ form, path, children }) {
8801
- const hidden = useHiddenCondition((f) => !f.subscribeNewsletter.value, form, path);
8802
- return hidden ? null : <>{children}</>;
8803
- }
8804
- ```
8805
-
8806
- _Source: src/hooks/useHiddenCondition.ts_
8807
-
8808
- ### validate
8809
-
8810
- **Kind:** `function`
8811
-
8812
- Зарегистрировать кастомный синхронный валидатор для поля
8813
- Поддерживает опциональные поля
8814
-
8815
- **Signature:**
8816
- ```typescript
8817
- export function validate<TForm, TField>(
8818
- fieldPath: FieldPathNode<TForm, TField> | undefined,
8819
- validatorFn: ContextualValidatorFn<TForm, TField>,
8820
- options?: ValidateOptions
8821
- ): void
8822
- ```
8823
-
8824
- **Examples:**
8825
-
8826
- ```typescript
8827
- validate(path.birthDate, (ctx: ValidationContext<TForm, TField>) => {
8828
- const birthDate = new Date(ctx.value());
8829
- const age = calculateAge(birthDate);
8830
-
8831
- if (age < 18) {
8832
- return {
8833
- code: 'tooYoung',
8834
- message: 'Заемщику должно быть не менее 18 лет',
8835
- };
8836
- }
8837
-
8838
- return null;
8839
- });
8840
- ```
8841
-
8842
- _Source: src/core/validation/core/validate.ts_
8843
-
8844
- ### validateAsync
8845
-
8846
- **Kind:** `function`
8847
-
8848
- Зарегистрировать асинхронный валидатор для поля
8849
-
8850
- **Signature:**
8851
- ```typescript
8852
- export function validateAsync<TForm, TField>(
8853
- fieldPath: FieldPathNode<TForm, TField>,
8854
- validatorFn: ContextualAsyncValidatorFn<TForm, TField>,
8855
- options?: ValidateAsyncOptions
8856
- ): void
8857
- ```
8858
-
8859
- **Examples:**
8860
-
8861
- ```typescript
8862
- validateAsync(
8863
- path.inn,
8864
- async (ctx: ValidationContext<TForm, TField>) => {
8865
- const inn = ctx.value();
8866
- if (!inn) return null;
8867
-
8868
- const response = await fetch('/api/validate-inn', {
8869
- method: 'POST',
8870
- body: JSON.stringify({ inn }),
8871
- });
8872
-
8873
- const data = await response.json();
8874
- if (!data.valid) {
8875
- return {
8876
- code: 'invalidInn',
8877
- message: 'ИНН не найден в базе данных ФНС',
8878
- };
8879
- }
8880
-
8881
- return null;
8882
- },
8883
- { debounce: 1000 }
8884
- );
8885
- ```
8886
-
8887
- _Source: src/core/validation/core/validate-async.ts_
8888
-
8889
7521
  ### ValidateAsyncOptions
8890
7522
 
8891
7523
  **Kind:** `interface`
8892
7524
 
8893
- Опции для функции validateAsync
7525
+ Опции для функции validateAsync.
8894
7526
 
8895
7527
  **Signature:**
8896
7528
  ```typescript
@@ -8902,111 +7534,65 @@ export interface ValidateAsyncOptions extends ValidateOptions {
8902
7534
 
8903
7535
  _Source: src/core/types/validation-schema.ts_
8904
7536
 
8905
- ### validateForm
7537
+ ### validateFormModel
8906
7538
 
8907
7539
  **Kind:** `function`
8908
7540
 
8909
- Валидировать форму в соответствии с указанной схемой
8910
-
8911
- Функция создает временный контекст валидации, применяет валидаторы
8912
- из схемы и очищает контекст без сохранения в реестр.
7541
+ In-form валидация: прогоняет {@link validateModel} и роутит ошибки в ноды формы
7542
+ (через реестр сигнал→нода): `node.setErrors(errors[path] ?? [])` — заодно очищает прошедшие поля
7543
+ И поля выключенных веток (`{ when, children }` с ложным условием). Дерево обходится ОДИН раз.
8913
7544
 
8914
7545
  **Signature:**
8915
7546
  ```typescript
8916
- export async function validateForm<T extends FormFields>(
8917
- form: GroupNode<T>,
8918
- schema: ValidationSchemaFn<T>
8919
- ): Promise<boolean>
7547
+ export async function validateFormModel<T>(
7548
+ model: FormModel<T>,
7549
+ schema: unknown
7550
+ ): Promise<ModelValidationResult>
8920
7551
  ```
8921
7552
 
8922
- **Parameters:**
8923
- - `form` — - GroupNode для валидации
8924
- - `schema` — - Схема валидации (ValidationSchemaFn)
8925
-
8926
- **Returns:** Promise<boolean> - `true` если форма валидна, `false` если есть ошибки
8927
-
8928
- **Examples:**
7553
+ _Source: src/core/model/validate-model.ts_
8929
7554
 
8930
- Multi-step форма: валидация текущего шага
8931
- ```typescript
8932
- const goToNextStep = async () => {
8933
- const isValid = await validateForm(form, step1LoanValidation);
7555
+ ### validateModel
8934
7556
 
8935
- if (!isValid) {
8936
- form.markAsTouched(); // Показать ошибки
8937
- return false;
8938
- }
7557
+ **Kind:** `function`
8939
7558
 
8940
- setCurrentStep(2);
8941
- return true;
8942
- };
8943
- ```
7559
+ Headless-валидация данных (sync + async). Работает без UI/нод.
8944
7560
 
8945
- Полная валидация перед submit
7561
+ **Signature:**
8946
7562
  ```typescript
8947
- const handleSubmit = async () => {
8948
- const isValid = await validateForm(form, fullValidationSchema);
8949
-
8950
- if (isValid) {
8951
- await form.submit(onSubmit);
8952
- }
8953
- };
7563
+ export async function validateModel<T>(
7564
+ model: FormModel<T>,
7565
+ schema: unknown
7566
+ ): Promise<ModelValidationResult>
8954
7567
  ```
8955
7568
 
8956
- Условная валидация
8957
- ```typescript
8958
- const schema = isBusinessAccount
8959
- ? businessValidation
8960
- : personalValidation;
7569
+ **Examples:**
8961
7570
 
8962
- const isValid = await validateForm(form, schema);
7571
+ ```typescript
7572
+ const res = await validateModel(model, schema);
7573
+ if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
8963
7574
  ```
8964
7575
 
8965
- _Source: src/core/validation/validate-form.ts_
7576
+ _Source: src/core/model/validate-model.ts_
8966
7577
 
8967
- ### validateItems
7578
+ ### validateModelSync
8968
7579
 
8969
7580
  **Kind:** `function`
8970
7581
 
8971
- Применить validation schema к каждому элементу массива
8972
-
8973
- Регистрирует схему валидации, которая будет автоматически применяться
8974
- к каждому элементу ArrayNode (как существующим, так и новым).
7582
+ Синхронная headless-валидация данных. Асинхронные валидаторы пропускаются.
8975
7583
 
8976
7584
  **Signature:**
8977
7585
  ```typescript
8978
- export function validateItems<TForm, TItem>(
8979
- fieldPath: FieldPathNode<TForm, TItem[] | undefined> | undefined,
8980
- itemSchemaFn: ValidationSchemaFn<TItem>
8981
- ): void
8982
- ```
8983
-
8984
- **Parameters:**
8985
- - `fieldPath` — - Поле-массив для валидации элементов
8986
- - `itemSchemaFn` — - Validation schema для одного элемента
8987
-
8988
- **Examples:**
8989
-
8990
- ```typescript
8991
- import { propertyValidation } from './property-validation';
8992
-
8993
- // В additionalValidation
8994
- applyWhen(path.hasProperty, (value) => value === true, (path) => {
8995
- // Проверка что массив не пустой
8996
- notEmpty(path.properties, { message: 'Добавьте хотя бы один объект имущества' });
8997
-
8998
- // Валидация каждого элемента
8999
- validateItems(path.properties, propertyValidation);
9000
- });
7586
+ export function validateModelSync<T>(model: FormModel<T>, schema: unknown): ModelValidationResult
9001
7587
  ```
9002
7588
 
9003
- _Source: src/core/validation/validators/array-validators.ts_
7589
+ _Source: src/core/model/validate-model.ts_
9004
7590
 
9005
7591
  ### ValidateOptions
9006
7592
 
9007
7593
  **Kind:** `interface`
9008
7594
 
9009
- Опции для функции validate
7595
+ Опции для функции validate.
9010
7596
 
9011
7597
  **Signature:**
9012
7598
  ```typescript
@@ -9020,88 +7606,6 @@ export interface ValidateOptions {
9020
7606
 
9021
7607
  _Source: src/core/types/validation-schema.ts_
9022
7608
 
9023
- ### validateTree
9024
-
9025
- **Kind:** `function`
9026
-
9027
- Зарегистрировать cross-field валидатор
9028
-
9029
- Используется для валидации, которая зависит от нескольких полей
9030
-
9031
- **Signature:**
9032
- ```typescript
9033
- export function validateTree<TForm>(
9034
- validatorFn: TreeValidatorFn<TForm>,
9035
- options?: ValidateTreeOptions
9036
- ): void
9037
- ```
9038
-
9039
- **Examples:**
9040
-
9041
- ```typescript
9042
- // Явная типизация ctx для избежания implicit any
9043
- validateTree(
9044
- (ctx: { form: MyForm }) => {
9045
- if (ctx.form.initialPayment && ctx.form.propertyValue) {
9046
- if (ctx.form.initialPayment > ctx.form.propertyValue) {
9047
- return {
9048
- code: 'initialPaymentTooHigh',
9049
- message: 'Первоначальный взнос не может превышать стоимость',
9050
- };
9051
- }
9052
- }
9053
- return null;
9054
- },
9055
- { targetField: 'initialPayment' }
9056
- );
9057
- ```
9058
-
9059
- _Source: src/core/validation/core/validate-tree.ts_
9060
-
9061
- ### ValidateTreeOptions
9062
-
9063
- **Kind:** `interface`
9064
-
9065
- Опции для функции validateTree
9066
-
9067
- **Signature:**
9068
- ```typescript
9069
- export interface ValidateTreeOptions {
9070
- /** Поле, к которому привязать ошибку */
9071
- targetField?: string;
9072
- }
9073
- ```
9074
-
9075
- _Source: src/core/types/validation-schema.ts_
9076
-
9077
- ### ValidationContextImpl
9078
-
9079
- **Kind:** `class`
9080
-
9081
- Контекст валидации одного поля. Создаётся фреймворком и передаётся в валидаторы
9082
- (`required`, `validate`, …) — напрямую инстанцировать не нужно.
9083
-
9084
- **Signature:**
9085
- ```typescript
9086
- export class ValidationContextImpl<TForm, TField> extends BaseValidationContext<TForm> { /* … */ }
9087
- ```
9088
-
9089
- **Examples:**
9090
-
9091
- ```typescript
9092
- import { validate } from '@reformer/core/validators';
9093
-
9094
- validate(path.password, (value, ctx) => {
9095
- // ctx — экземпляр ValidationContextImpl, доступ к ctx.form для cross-field логики
9096
- if (value !== ctx.form.confirmPassword.value) {
9097
- return { code: 'mismatch', message: 'Пароли не совпадают' };
9098
- }
9099
- return null;
9100
- });
9101
- ```
9102
-
9103
- _Source: src/core/validation/validation-context.ts_
9104
-
9105
7609
  ### ValidationError
9106
7610
 
9107
7611
  **Kind:** `interface`
@@ -9121,52 +7625,32 @@ export interface ValidationError {
9121
7625
 
9122
7626
  _Source: src/core/types/index.ts_
9123
7627
 
9124
- ### ValidationRegistry
9125
-
9126
- **Kind:** `class`
7628
+ ### Validator
9127
7629
 
9128
- Реестр валидаторов для формы
7630
+ **Kind:** `type`
9129
7631
 
9130
- Каждый экземпляр GroupNode создает собственный реестр (композиция).
9131
- Устраняет race conditions и изолирует формы друг от друга.
7632
+ Чистый синхронный валидатор поля.
9132
7633
 
9133
- Наследует AbstractRegistry для унификации:
9134
- - Управления global stack
9135
- - Template methods begin/end registration
7634
+ Принимает значение поля, прокси текущего поля (control) и прокси корня формы (root).
7635
+ Возвращает ValidationError либо null. Не знает про реестр валидации.
9136
7636
 
9137
7637
  **Signature:**
9138
7638
  ```typescript
9139
- export class ValidationRegistry extends AbstractRegistry<ValidatorRegistration> {
9140
- /** Внутренний стек контекстов для управления condition blocks */ /* … */ }
7639
+ export type Validator<TForm, TField> = (
7640
+ value: TField,
7641
+ control: FormProxy<TField>,
7642
+ root: FormProxy<TForm>
7643
+ ) => ValidationError | null;
9141
7644
  ```
9142
7645
 
9143
7646
  **Examples:**
9144
7647
 
9145
7648
  ```typescript
9146
- class GroupNode {
9147
- private readonly validationRegistry = new ValidationRegistry();
9148
-
9149
- applyValidationSchema(schemaFn) {
9150
- this.validationRegistry.beginRegistration(); // Pushes this to global stack
9151
- schemaFn(createFieldPath(this)); // Uses getCurrent()
9152
- this.validationRegistry.endRegistration(this); // Pops from global stack
9153
- }
9154
- }
9155
- ```
9156
-
9157
- _Source: src/core/validation/validation-registry.ts_
9158
-
9159
- ### ValidationSchemaFn
9160
-
9161
- **Kind:** `type`
9162
-
9163
- Функция validation schema
9164
-
9165
- Принимает FieldPath и определяет все правила валидации для формы
9166
-
9167
- **Signature:**
9168
- ```typescript
9169
- export type ValidationSchemaFn<T> = (path: FieldPath<T>) => void;
7649
+ const isAdult: Validator<MyForm, number> = (value, control, root) => {
7650
+ if (value < 18) return { code: 'tooYoung', message: '18+' };
7651
+ return null;
7652
+ };
7653
+ validate(path.age, isAdult);
9170
7654
  ```
9171
7655
 
9172
7656
  _Source: src/core/types/validation-schema.ts_
@@ -9184,141 +7668,32 @@ export type ValidatorFn<T = FormValue> = (value: T) => ValidationError | null;
9184
7668
 
9185
7669
  _Source: src/core/types/index.ts_
9186
7670
 
9187
- ### ValidatorRegistration
9188
-
9189
- **Kind:** `interface`
9190
-
9191
- Регистрация валидатора в системе
9192
-
9193
- **Signature:**
9194
- ```typescript
9195
- export interface ValidatorRegistration {
9196
- fieldPath: string;
9197
- type: 'sync' | 'async' | 'tree';
9198
- validator:
9199
- | ContextualValidatorFn<unknown, unknown>
9200
- | ContextualAsyncValidatorFn<unknown, unknown>
9201
- | TreeValidatorFn<unknown>;
9202
- options?: ValidateOptions | ValidateAsyncOptions | ValidateTreeOptions;
9203
- condition?: {
9204
- fieldPath: string;
9205
- conditionFn: ConditionFn<unknown>;
9206
- };
9207
- }
9208
- ```
9209
-
9210
- _Source: src/core/types/validation-schema.ts_
9211
-
9212
7671
  ### watchField
9213
7672
 
9214
7673
  **Kind:** `function`
9215
7674
 
9216
- Выполняет callback при изменении поля
7675
+ Реакция на изменение поля: вызывает `cb(value)` при каждом изменении (по умолчанию без вызова на
7676
+ инициализации; `immediate: true` — вызвать сразу).
9217
7677
 
9218
7678
  **Signature:**
9219
7679
  ```typescript
9220
- export function watchField<TForm, TField>(
9221
- field: FieldPathNode<TForm, TField>,
9222
- callback: (value: TField, ctx: BehaviorContext<TForm>) => void | Promise<void>,
9223
- options?: WatchFieldOptions
9224
- ): void
7680
+ export function watchField<T>(
7681
+ source: ReadonlySignal<T>,
7682
+ cb: (value: T) => void,
7683
+ options?: { immediate?: boolean }
7684
+ ): BehaviorCleanup
9225
7685
  ```
9226
7686
 
9227
- **Parameters:**
9228
- - `field` — - Поле для отслеживания
9229
- - `callback` — - Функция обратного вызова
9230
- - `options` — - Опции (`debounce`, `immediate`)
9231
-
9232
7687
  **Examples:**
9233
7688
 
9234
- Async loader with try/catch + guard + debounce
9235
- ```typescript
9236
- import { watchField, type BehaviorSchemaFn } from '@reformer/core/behaviors';
9237
-
9238
- interface AddressForm { region: string; city: string }
9239
-
9240
- export const addressBehavior: BehaviorSchemaFn<AddressForm> = (path) => {
9241
- watchField(
9242
- path.region,
9243
- async (region, ctx) => {
9244
- if (!region) {
9245
- ctx.form.city.updateComponentProps({ options: [] });
9246
- return; // guard: пустое значение не триггерит fetch
9247
- }
9248
- try {
9249
- const { data: cities } = await fetchCities(region);
9250
- ctx.form.city.updateComponentProps({ options: cities });
9251
- } catch (error) {
9252
- console.error('[addressBehavior] failed to load cities:', error);
9253
- ctx.form.city.updateComponentProps({ options: [] });
9254
- }
9255
- },
9256
- { immediate: false, debounce: 300 }, // обязательные опции для async
9257
- );
9258
- };
9259
- ```
9260
-
9261
- Sync handler с консолидацией нескольких зависимостей в один watcher
9262
- ```typescript
9263
- import { watchField, type BehaviorSchemaFn } from '@reformer/core/behaviors';
9264
-
9265
- interface InsuranceForm {
9266
- insuranceType: 'casco' | 'osago' | 'property' | '';
9267
- vehicleVin: string;
9268
- propertyType: string;
9269
- }
9270
-
9271
- export const insuranceBehavior: BehaviorSchemaFn<InsuranceForm> = (path) => {
9272
- // ОДИН watchField на trigger-поле — несколько watcher'ов на одно поле = "Cycle detected"
9273
- watchField(
9274
- path.insuranceType,
9275
- (type, ctx) => {
9276
- const isVehicle = type === 'casco' || type === 'osago';
9277
- const isProperty = type === 'property';
9278
-
9279
- // Guard — ставим только если состояние реально меняется
9280
- if (isVehicle) {
9281
- if (ctx.form.vehicleVin.disabled.value) ctx.form.vehicleVin.enable();
9282
- } else {
9283
- if (!ctx.form.vehicleVin.disabled.value) ctx.form.vehicleVin.disable();
9284
- if (ctx.form.vehicleVin.getValue() !== '') ctx.form.vehicleVin.setValue('');
9285
- }
9286
-
9287
- if (isProperty) {
9288
- if (ctx.form.propertyType.disabled.value) ctx.form.propertyType.enable();
9289
- } else {
9290
- if (!ctx.form.propertyType.disabled.value) ctx.form.propertyType.disable();
9291
- if (ctx.form.propertyType.getValue() !== '') ctx.form.propertyType.setValue('');
9292
- }
9293
- },
9294
- { immediate: false }, // CRITICAL: предотвращает запуск во время инициализации
9295
- );
9296
- };
9297
- ```
9298
-
9299
- **See also:**
9300
- - [docs/llms/22-cycle-detection.md](../../../../docs/llms/22-cycle-detection.md)
9301
-
9302
- _Source: src/core/behavior/behaviors/watch-field.ts_
9303
-
9304
- ### WatchFieldOptions
9305
-
9306
- **Kind:** `interface`
9307
-
9308
- Опции для watchField
9309
-
9310
- **Signature:**
9311
7689
  ```typescript
9312
- export interface WatchFieldOptions {
9313
- /** Debounce в мс */
9314
- debounce?: number;
9315
-
9316
- /** Вызвать сразу при инициализации */
9317
- immediate?: boolean;
9318
- }
7690
+ watchField(model.$.country, async (country) => {
7691
+ model.city = '';
7692
+ // ... загрузить города
7693
+ });
9319
7694
  ```
9320
7695
 
9321
- _Source: src/core/behavior/types.ts_
7696
+ _Source: src/core/model/behaviors.ts_
9322
7697
 
9323
7698
  ### WithBehaviorSchema
9324
7699