@reformer/core 11.0.0-beta.8 → 11.0.0-beta.9

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 (224) hide show
  1. package/README.md +91 -73
  2. package/dist/behaviors-value-HA_Q03hc.js +108 -0
  3. package/dist/behaviors.js +370 -242
  4. package/dist/{create-model-BBRHhHwK.js → create-model-BvcbNPgL.js} +6 -6
  5. package/dist/date-utils-90-qZbse.js +43 -0
  6. package/dist/{en-CjAqn7h0.js → en-HP8XEych.js} +6 -3
  7. package/dist/form/aggregate-signals.d.ts +21 -63
  8. package/dist/form/behaviors/collections.d.ts +28 -7
  9. package/dist/form/behaviors/context.d.ts +19 -2
  10. package/dist/form/behaviors/index.d.ts +3 -3
  11. package/dist/form/behaviors/internals.d.ts +9 -2
  12. package/dist/form/behaviors/node.d.ts +25 -2
  13. package/dist/form/behaviors/operators.d.ts +17 -9
  14. package/dist/form/behaviors/schema-operators.d.ts +6 -4
  15. package/dist/form/behaviors/types.d.ts +12 -4
  16. package/dist/form/create-form.d.ts +47 -35
  17. package/dist/form/flow/flow.d.ts +87 -0
  18. package/dist/form/form-bundle.d.ts +45 -28
  19. package/dist/form/form-config-base.d.ts +46 -0
  20. package/dist/form/form-level.d.ts +121 -0
  21. package/dist/form/form-proxy-builder.d.ts +6 -22
  22. package/dist/form/index.d.ts +16 -21
  23. package/dist/form/nodes/form-array-node.d.ts +100 -0
  24. package/dist/form/nodes/form-control-node.d.ts +73 -0
  25. package/dist/form/nodes/form-group-node.d.ts +66 -0
  26. package/dist/form/nodes/node-kind.d.ts +29 -0
  27. package/dist/form/nodes/state/availability.d.ts +6 -0
  28. package/dist/form/nodes/state/component.d.ts +6 -0
  29. package/dist/form/nodes/state/index.d.ts +12 -0
  30. package/dist/form/nodes/state/interaction.d.ts +13 -0
  31. package/dist/form/nodes/state/issues.d.ts +9 -0
  32. package/dist/form/nodes/state/pending.d.ts +6 -0
  33. package/dist/form/schema/compile.d.ts +59 -0
  34. package/dist/form/schema/compiled.d.ts +143 -0
  35. package/dist/form/schema/row-key.d.ts +11 -0
  36. package/dist/form/schema-controller.d.ts +30 -14
  37. package/dist/form/signal-node-registry.d.ts +6 -7
  38. package/dist/form/submission.d.ts +93 -0
  39. package/dist/form/type-guards.d.ts +14 -73
  40. package/dist/form/types/contracts.d.ts +19 -92
  41. package/dist/form/types/field-config.d.ts +27 -0
  42. package/dist/form/types/index.d.ts +5 -57
  43. package/dist/form/types/nodes.d.ts +159 -0
  44. package/dist/form/types/schema-node.d.ts +139 -63
  45. package/dist/form/types/validation-schema.d.ts +23 -27
  46. package/dist/form/validation/context.d.ts +42 -7
  47. package/dist/form/validation/index.d.ts +9 -8
  48. package/dist/form/validation/operators.d.ts +22 -30
  49. package/dist/form/validation/run.d.ts +93 -18
  50. package/dist/form/validation/strategy.d.ts +41 -13
  51. package/dist/form/validation/types.d.ts +74 -20
  52. package/dist/form/validators/date-utils.d.ts +13 -1
  53. package/dist/form/validators/email.d.ts +8 -7
  54. package/dist/form/validators/file-type.d.ts +8 -7
  55. package/dist/form/validators/file-utils.d.ts +2 -1
  56. package/dist/form/validators/future-date.d.ts +8 -7
  57. package/dist/form/validators/index.d.ts +1 -1
  58. package/dist/form/validators/integer.d.ts +8 -7
  59. package/dist/form/validators/is-date.d.ts +11 -9
  60. package/dist/form/validators/is-number.d.ts +11 -9
  61. package/dist/form/validators/max-age.d.ts +13 -9
  62. package/dist/form/validators/max-date.d.ts +8 -7
  63. package/dist/form/validators/max-file-size.d.ts +8 -7
  64. package/dist/form/validators/max-files.d.ts +8 -7
  65. package/dist/form/validators/max-length.d.ts +14 -7
  66. package/dist/form/validators/max-total-file-size.d.ts +8 -7
  67. package/dist/form/validators/max.d.ts +10 -8
  68. package/dist/form/validators/min-age.d.ts +12 -9
  69. package/dist/form/validators/min-date.d.ts +8 -7
  70. package/dist/form/validators/min-file-size.d.ts +8 -7
  71. package/dist/form/validators/min-files.d.ts +11 -9
  72. package/dist/form/validators/min-length.d.ts +14 -7
  73. package/dist/form/validators/min.d.ts +10 -8
  74. package/dist/form/validators/multiple-of.d.ts +10 -8
  75. package/dist/form/validators/non-negative.d.ts +8 -7
  76. package/dist/form/validators/non-zero.d.ts +8 -7
  77. package/dist/form/validators/past-date.d.ts +8 -7
  78. package/dist/form/validators/pattern.d.ts +9 -8
  79. package/dist/form/validators/phone.d.ts +8 -7
  80. package/dist/form/validators/required.d.ts +11 -9
  81. package/dist/form/validators/url.d.ts +20 -13
  82. package/dist/form/validators/validation-issue.d.ts +8 -0
  83. package/dist/i18n/descriptor.d.ts +1 -1
  84. package/dist/i18n/en.json.d.ts +5 -1
  85. package/dist/i18n/locale-store.d.ts +7 -0
  86. package/dist/i18n/message-format.d.ts +24 -4
  87. package/dist/i18n/ru.json.d.ts +5 -1
  88. package/dist/i18n/translator.d.ts +11 -0
  89. package/dist/i18n/validation-message.d.ts +42 -7
  90. package/dist/i18n.d.ts +7 -12
  91. package/dist/i18n.js +66 -283
  92. package/dist/index.d.ts +16 -7
  93. package/dist/index.js +924 -2855
  94. package/dist/loader-BWkigpVe.js +52 -0
  95. package/dist/locale/en.js +1 -1
  96. package/dist/locale/en.json +4 -1
  97. package/dist/locale/ru.js +4 -1
  98. package/dist/locale/ru.json +4 -1
  99. package/dist/locale-CG54AU2Q.js +219 -0
  100. package/dist/locale.js +1 -1
  101. package/dist/model/behaviors-value.d.ts +24 -6
  102. package/dist/model/create-model.d.ts +2 -2
  103. package/dist/model/derived-registry.d.ts +1 -1
  104. package/dist/model/index.d.ts +11 -5
  105. package/dist/model/model-nodes.d.ts +79 -12
  106. package/dist/model/model-signals-proxy.d.ts +5 -6
  107. package/dist/model/model-value-proxy.d.ts +20 -15
  108. package/dist/model/safe-effect.d.ts +33 -1
  109. package/dist/model/types.d.ts +63 -13
  110. package/dist/model.js +20 -18
  111. package/dist/node-kind-C-r6ZMdo.js +15 -0
  112. package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
  113. package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
  114. package/dist/platforms/react/form-bundle-context.d.ts +2 -2
  115. package/dist/platforms/react/hooks/types.d.ts +57 -38
  116. package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
  117. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  118. package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
  119. package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
  120. package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
  121. package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
  122. package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
  123. package/dist/platforms/react/i18n/context.d.ts +6 -1
  124. package/dist/platforms/react/i18n/hooks.d.ts +16 -10
  125. package/dist/platforms/react/i18n/provider.d.ts +9 -2
  126. package/dist/platforms/react/index.d.ts +6 -4
  127. package/dist/react.d.ts +22 -0
  128. package/dist/react.js +505 -0
  129. package/dist/runtime-token.d.ts +1 -2
  130. package/dist/safe-effect-BbtX3KuN.js +643 -0
  131. package/dist/schema-controller-CpiO2JoK.js +143 -0
  132. package/dist/signals.d.ts +15 -9
  133. package/dist/signals.js +11 -6
  134. package/dist/strategy-CoFSemuP.js +375 -0
  135. package/dist/type-guards-D-N_5Ddh.js +19 -0
  136. package/dist/validation-issue-B9mjI8jz.js +7 -0
  137. package/dist/validation-message-Ced9MGxs.js +194 -0
  138. package/dist/validation.js +9 -9
  139. package/dist/validators/email.js +5 -5
  140. package/dist/validators/future-date.js +8 -8
  141. package/dist/validators/integer.js +3 -3
  142. package/dist/validators/is-date.js +4 -4
  143. package/dist/validators/is-number.js +4 -4
  144. package/dist/validators/max-age.js +8 -9
  145. package/dist/validators/max-date.js +8 -8
  146. package/dist/validators/max-length.js +7 -7
  147. package/dist/validators/max.js +4 -4
  148. package/dist/validators/min-age.js +10 -8
  149. package/dist/validators/min-date.js +3 -3
  150. package/dist/validators/min-length.js +5 -5
  151. package/dist/validators/min.js +4 -4
  152. package/dist/validators/multiple-of.js +11 -7
  153. package/dist/validators/non-negative.js +3 -3
  154. package/dist/validators/non-zero.js +3 -3
  155. package/dist/validators/past-date.js +8 -8
  156. package/dist/validators/pattern.js +5 -5
  157. package/dist/validators/phone.js +11 -9
  158. package/dist/validators/required.js +2 -2
  159. package/dist/validators/url.js +48 -12
  160. package/dist/validators.js +17 -17
  161. package/docs/llms/01-api-reference.md +60 -34
  162. package/docs/llms/02-quick-start.md +111 -80
  163. package/docs/llms/03-api-signatures.md +368 -153
  164. package/docs/llms/04-common-patterns.md +58 -56
  165. package/docs/llms/05-common-mistakes.md +69 -44
  166. package/docs/llms/06-troubleshooting.md +14 -9
  167. package/docs/llms/07-complete-import.md +74 -43
  168. package/docs/llms/08-form-types.md +11 -4
  169. package/docs/llms/09-formschema.md +129 -81
  170. package/docs/llms/10-arrays.md +106 -75
  171. package/docs/llms/11-async-watchfield.md +2 -2
  172. package/docs/llms/13-multi-step.md +134 -63
  173. package/docs/llms/14-extended-mistakes.md +20 -17
  174. package/docs/llms/15-project-structure.md +221 -74
  175. package/docs/llms/16-ui-components.md +55 -42
  176. package/docs/llms/17-nonexistent-api.md +199 -32
  177. package/docs/llms/18-conditional-fields.md +66 -32
  178. package/docs/llms/19-reading-values.md +15 -3
  179. package/docs/llms/20-compute-vs-watch.md +16 -10
  180. package/docs/llms/21-array-operations.md +51 -27
  181. package/docs/llms/22-cycle-detection.md +1 -1
  182. package/docs/llms/23-copy-from.md +2 -2
  183. package/docs/llms/24-sync-fields.md +6 -5
  184. package/docs/llms/25-reset-when.md +14 -7
  185. package/docs/llms/26-transform-value.md +4 -4
  186. package/docs/llms/27-revalidate-when.md +16 -15
  187. package/docs/llms/28-submit-and-reset.md +276 -128
  188. package/docs/llms/29-async-preload.md +39 -29
  189. package/docs/llms/30-type-safety-recipes.md +105 -51
  190. package/docs/llms/31-async-validator-debounce.md +96 -53
  191. package/docs/llms/33-validation-strategy.md +177 -65
  192. package/docs/llms/34-i18n.md +69 -43
  193. package/docs/llms/35-unified-contract.md +100 -39
  194. package/docs/llms/36-issue-messages.md +284 -0
  195. package/docs/llms/37-compiled-schema.md +332 -0
  196. package/docs/llms/38-form-flow.md +397 -0
  197. package/llms-index.json +1 -1
  198. package/llms.txt +6084 -3420
  199. package/package.json +23 -4
  200. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  201. package/dist/date-utils-dTT_x_Be.js +0 -31
  202. package/dist/form/create-core-form.d.ts +0 -72
  203. package/dist/form/factories/index.d.ts +0 -6
  204. package/dist/form/factories/node-factory.d.ts +0 -189
  205. package/dist/form/form-submitter.d.ts +0 -121
  206. package/dist/form/nodes/array-node.d.ts +0 -276
  207. package/dist/form/nodes/field-node.d.ts +0 -297
  208. package/dist/form/nodes/form-node.d.ts +0 -326
  209. package/dist/form/nodes/group-node.d.ts +0 -284
  210. package/dist/form/nodes/model-array-node.d.ts +0 -126
  211. package/dist/form/nodes/subscription-manager.d.ts +0 -180
  212. package/dist/form/status-machine.d.ts +0 -115
  213. package/dist/form/types/deep-schema.d.ts +0 -107
  214. package/dist/form/types/form-proxy.d.ts +0 -141
  215. package/dist/form/unique-id.d.ts +0 -45
  216. package/dist/form/validation/config.d.ts +0 -73
  217. package/dist/form/validation/error-handler.d.ts +0 -166
  218. package/dist/form/validators/validation-error.d.ts +0 -8
  219. package/dist/index-BrNkQj1y.js +0 -105
  220. package/dist/loader-Dv_Hl7JC.js +0 -216
  221. package/dist/model-value-proxy-caTmAiUd.js +0 -419
  222. package/dist/node-BeV_lV5i.js +0 -100
  223. package/dist/strategy-DgzELWno.js +0 -204
  224. package/dist/validation-error-B-U3RsV0.js +0 -11
@@ -7,181 +7,363 @@
7
7
  createModel<T extends object>(initial: T): FormModel<T>
8
8
  // model.get() / model.set(full) / model.patch(partial) / model.isDirty()
9
9
  // model.reset() / model.captureInitial() / model.signalAt(path)
10
- // model.$.field → PathAwareSignal<FieldType> (escape-hatch к сигналу)
11
- // model.arrayField → ModelArray<Item> (push/removeAt/insertAt/move/swap/clear/at/map/length)
12
-
13
- // Форма (ноды поверх сигналов модели)
14
- createFormFromModel<T>({ model, schema, behavior? }): FormProxy<T>
15
- // form.<field> → FieldNode / GroupNode / FormArrayProxy
16
- // form.<field>.setValue(v) / .value.value / .errors.value / .disabled.value
17
- // form.<field>.enable() / .disable() / .reset() / .markAsTouched() / .setErrors([...])
18
- // form.<field>.updateComponentProps({ ... })
19
-
20
- // Валидация данных — ОТДЕЛЬНЫЙ контракт `@reformer/core/validation` (НЕ дерево createForm: то несёт
21
- // только layout, без validators). Схема — функция над моделью; внешний раннер разносит ошибки по нодам
22
- // формы (setErrors), warning не блокирует submit, устаревший прогон отменяется.
23
- validateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean> // из @reformer/core/validation
24
- // schema = defineValidationSchema<T>(({ model }) => { validate(...); ... })
25
- // ⚠️ form.submit() / form.validate() БОЛЬШЕ НЕ прогоняют schema-валидацию — гоняйте validateModel(model, schema) снаружи.
10
+ // model.$.field → PathAwareSignal<FieldType> (ручка: привязка в схеме, правиле, операторе)
11
+ // model.$.group.value → снимок группы/массива; в dev заморожен — правка на месте бросает
12
+ // model.arrayField → ModelArray<Item> (push/insertAt/removeAt/move/swap/clear/at/map/length)
13
+ // итерируем (for…of, spread); Object.keys — индексы; insertAt/move за концом — RangeError
14
+
15
+ // Сборка одним вызовом: модель, форма, уровень формы и дерево для рендера
16
+ createForm<T extends object>({
17
+ initial | model,
18
+ schema?: (model) => tree, // билдер; JSON — документ + registry
19
+ behavior?, // defineFormBehavior(...)
20
+ validation?, // ValidationDefinition<T> | { rules?, extras?, strategy?, debounce? }
21
+ flow?, // { steps: [{ id, rules? }] } — id = selector узла шага
22
+ seed?, setup?,
23
+ }): FormBundle<T>
24
+ // bundle = { model, form, render, flow, validating, submission, validate, validateStep, dispose }
25
+ // (+ служебное validation: { rules, controller } | undefined — его армирует useFormBundle)
26
+ // await bundle.validate() → ValidationResult (вся форма; замечания показываются)
27
+ // await bundle.validateStep('loan') → ValidationResult (правила шага; неизвестный шаг — исключение)
28
+ // await bundle.submission.submit(handler) → SubmitResult<R> (один прогон; обработчик — только при valid)
29
+ // bundle.submission.submitting → ReadonlySignal<boolean> (работает обработчик)
30
+ // bundle.flow?.next() / previous() / goTo(id) → FlowTransition
31
+ // bundle.render.tree → CompiledSchema (см. 37-compiled-schema.md)
32
+ // bundle.dispose() → снять сборку: поведение, подписки нод, живую проверку;
33
+ // идущие проверки — cancelled; модель остаётся; идемпотентно
34
+ // useFormBundle(() => createForm(…)) снимает сборку сам — при размонтировании компонента
35
+
36
+ // Отправка — отдельный объект сборки (`bundle.submission`)
37
+ interface FormSubmission<T> {
38
+ readonly submitting: ReadonlySignal<boolean>; // работает обработчик; пока идёт проверка — false
39
+ submit<R>(handler: (values: T) => R | Promise<R>): Promise<SubmitResult<R>>;
40
+ }
41
+ // Без сборки (визард в разметке без createForm): check: undefined — отправка без проверки
42
+ createSubmission<T>(dependencies: {
43
+ check: (() => Promise<ActionCheck<T>>) | undefined; // ActionCheck = { result, values, stale }
44
+ values: () => T;
45
+ }): FormSubmission<T>
46
+
47
+ // Низкоуровневая фабрика: форма из готовых модели и дерева, без уровня формы
48
+ createFormFromModel<T>({ model, schema?, behavior? }): FormGroup<T>
49
+ disposeForm<T>(form: FormGroup<T>): void // снять такую форму: поведение и подписки нод; модель остаётся
26
50
  ```
27
51
 
52
+ ### Form nodes
53
+
54
+ Ноды строит сборка; прикладной код работает с ними через интерфейсы. Классов нод в публичном
55
+ API нет — вид ноды узнают гардами `isFormControl` / `isFormGroup` / `isFormArray`.
56
+
57
+ ```typescript
58
+ interface FormNode<T> {
59
+ readonly value: ReadonlySignal<T>;
60
+ readonly valid: ReadonlySignal<boolean>; // нет блокирующих замечаний, не disabled, не pending (у поля, группы и массива одинаково)
61
+ readonly invalid: ReadonlySignal<boolean>;
62
+ readonly pending: ReadonlySignal<boolean>; // идёт async-правило
63
+ readonly disabled: ReadonlySignal<boolean>;
64
+ readonly touched: ReadonlySignal<boolean>;
65
+ readonly dirty: ReadonlySignal<boolean>;
66
+ readonly issues: ReadonlySignal<readonly ValidationIssue[]>; // у группы и массива — свои и детей
67
+ readonly ownIssues: ReadonlySignal<readonly ValidationIssue[]>; // только свои: правила на ручке группы/массива
68
+ getValue(): T;
69
+ setValue(value: T): void; // пишет в модель
70
+ reset(value?: T): void; // значение и состояние — к начальным
71
+ markAsTouched(): void; // у группы — каскадно
72
+ markAsUntouched(): void;
73
+ enable(): void;
74
+ disable(): void;
75
+ setIssues(issues: readonly ValidationIssue[]): void; // ответ сервера, своя проверка
76
+ clearIssues(): void;
77
+ }
78
+
79
+ interface FormControl<T> extends FormNode<T> {
80
+ readonly component: ComponentRef | undefined;
81
+ readonly componentProps: ReadonlySignal<Readonly<Record<string, unknown>>>;
82
+ // invalid && (touched || dirty) — состояние ошибки: aria-invalid, рамка
83
+ readonly shouldShowError: ReadonlySignal<boolean>;
84
+ // есть замечания (ошибки ИЛИ предупреждения) && (touched || dirty) — показ текстов замечаний
85
+ readonly shouldShowIssues: ReadonlySignal<boolean>;
86
+ updateComponentProps(patch: Record<string, unknown>): void;
87
+ }
88
+
89
+ interface FormArray<T extends object> extends FormNode<T[]> {
90
+ readonly length: number; // число; чтение в эффекте/computed подписывает
91
+ at(index: number): FormGroup<T> | undefined;
92
+ map<R>(fn: (row: FormGroup<T>, index: number) => R): R[];
93
+ forEach(fn: (row: FormGroup<T>, index: number) => void): void;
94
+ keyAt(index: number): string; // стабильный ключ строки — React `key`
95
+ push(item?: T): void; // без значения — по шаблону arrayOf(blank)
96
+ insert(index: number, item?: T): void;
97
+ removeAt(index: number): void;
98
+ move(from: number, to: number): void;
99
+ swap(first: number, second: number): void;
100
+ clear(): void;
101
+ }
102
+
103
+ // Группа: члены FormNode + ноды полей по именам + пространство `$` со всеми полями
104
+ type FormGroup<T> = FormNode<T> & /* поля по именам */ & { readonly $: FormFields<T> };
105
+ // form.email — FormControl<string>
106
+ // form.address.city — FormControl<string> (вложенная группа — FormGroup)
107
+ // form.items — FormArray<Item> (массив объектов); form.tags — FormControl<string[]>
108
+ // form.$.value — поле, затенённое членом FormNode (value, issues, reset, …)
109
+ ```
110
+
111
+ Поле затеняют только члены `FormNode`: поле данных `value` берётся через `form.$.value`. Имена
112
+ `id`, `status`, `fields`, `submit` — обычные поля (`form.status`).
113
+
28
114
  ### Validators
29
115
 
30
116
  Валидаторы — **чистые фабрики** из `@reformer/core/validators`: возвращают правило `Rule<T>` =
31
- `(value) => ValidationError | null` (принимают nullable value). Передаются оператору
32
- `validate(sig, [required(), min(50000)])` внутри схемы валидации, а **не** в layout-дерево `createForm`.
117
+ `(value: T) => ValidationIssue | null`. Передаются оператору `validate(sig, [required(), min(50000)])`
118
+ внутри схемы валидации, а **не** в схему формы.
33
119
 
34
120
  ```typescript
35
- required(options?: { message?: string })
36
- min(value: number, options?: { message?: string })
37
- max(value: number, options?: { message?: string })
38
- minLength(length: number, options?: { message?: string })
39
- maxLength(length: number, options?: { message?: string })
40
- email(options?: { message?: string })
41
- pattern(regex: RegExp, options?: { message?: string })
42
- url(options?: { message?: string; requireProtocol?: boolean })
43
- phone(options?: { message?: string; format?: PhoneFormat })
44
- // Number validator factories
45
- isNumber(options?: { message?: string })
46
- integer(options?: { message?: string })
47
- multipleOf(divisor: number, options?: { message?: string })
48
- nonNegative(options?: { message?: string }) // value >= 0
49
- nonZero(options?: { message?: string }) // value !== 0
50
- // Date validator factories
51
- isDate(options?: { message?: string })
52
- minDate(date: Date | string, options?: { message?: string })
53
- maxDate(date: Date | string, options?: { message?: string })
54
- pastDate(options?: { message?: string })
55
- futureDate(options?: { message?: string })
56
- minAge(years: number, options?: { message?: string })
57
- maxAge(years: number, options?: { message?: string })
121
+ required(options?: RuleOptions) // Rule<unknown>; пусто: null, undefined, '', [], NaN, false
122
+ min(value: number, options?: RuleOptions) // Rule<number | null | undefined>
123
+ max(value: number, options?: RuleOptions)
124
+ minLength(length: number, options?: RuleOptions) // Rule<unknown>: строка либо массив
125
+ maxLength(length: number, options?: RuleOptions)
126
+ email(options?: RuleOptions) // Rule<string | null | undefined>
127
+ pattern(regex: RegExp, options?: RuleOptions)
128
+ url(options?: RuleOptions & { requireProtocol?: boolean; allowedProtocols?: string[] })
129
+ // url: разбор new URL; localhost, IP, порт, кириллический домен — да; пробелы и userinfo — нет;
130
+ // протоколы по умолчанию http/https (без учёта регистра), чужой — код url_protocol
131
+ phone(options?: RuleOptions & { format?: PhoneFormat })
132
+ // Number validator factories — Rule<number | null | undefined>
133
+ isNumber(options?: RuleOptions)
134
+ integer(options?: RuleOptions)
135
+ multipleOf(divisor: number, options?: RuleOptions) // делитель 0 / не конечный — RangeError при создании
136
+ nonNegative(options?: RuleOptions) // value >= 0
137
+ nonZero(options?: RuleOptions) // value !== 0
138
+ // Date validator factories — Rule<string | Date | null | undefined>
139
+ // строгий формат: Date либо строка YYYY-MM-DD; другие строки (дата-время, ДД.ММ.ГГГГ) — не даты:
140
+ // isDate даёт date_invalid, остальные правила их пропускают. Рождённый 29 февраля в невисокосный
141
+ // год взрослеет 28 февраля (ГК РФ, ст. 192); дата рождения в будущем — minAge даёт date_future
142
+ isDate(options?: RuleOptions)
143
+ minDate(date: Date, options?: RuleOptions)
144
+ maxDate(date: Date, options?: RuleOptions)
145
+ pastDate(options?: RuleOptions)
146
+ futureDate(options?: RuleOptions)
147
+ minAge(years: number, options?: RuleOptions)
148
+ maxAge(years: number, options?: RuleOptions)
149
+ // File validator factories — Rule<unknown>: файл либо массив файлов
150
+ fileType(accept: string, options?: RuleOptions) // синтаксис accept; «звезда/звезда» — любой файл
151
+ maxFileSize(bytes: number, options?: RuleOptions)
152
+ minFileSize(bytes: number, options?: RuleOptions)
153
+ maxFiles(count: number, options?: RuleOptions)
154
+ minFiles(count: number, options?: RuleOptions) // [] при count > 0 — замечание, как у minLength
155
+ maxTotalFileSize(bytes: number, options?: RuleOptions)
58
156
  ```
59
157
 
60
- Кроме `message`, каждое правило принимает `messageKey` (ключ словаря приложения) и `params`.
61
- Без `message` правило кладёт в ошибку пустую строку, а текст при отображении берётся из словаря
62
- локали по коду (`validation.<code>`): «Обязательное поле» под русской локалью, английская фраза без
63
- провайдера. Явное `message` важнее словаря, но языком не переключается; для локализуемого текста —
64
- `messageKey`. Подробно — [34-i18n.md](./34-i18n.md).
158
+ Тип значения правила сверяется с типом поля: `validate(model.$.age, [email()])` для числового поля
159
+ не компилируется. Набор правил для поля объявляют типом поля — `Rule<LoanType>[]`,
160
+ `Rule<number | null>[]`: фабрики с более широким типом значения в него присваиваются.
161
+
162
+ `RuleOptions` — `{ code?: string; params?: Record<string, unknown> }`. Текста у правила нет:
163
+ замечание несёт только код и параметры (`ValidationIssue = { code, params?, severity? }`), а строку
164
+ по коду при отображении даёт словарь — словарь формы (`defineIssueMessages`), затем словарь локали
165
+ (`validation.<code>`): «Обязательное поле» под русской локалью, английская фраза без провайдера.
166
+ Текст конкретного поля — свой код (`required({ code: 'phone.required' })`) и запись в словаре
167
+ формы. Подробно — [36-issue-messages.md](./36-issue-messages.md).
65
168
 
66
- Использование: layout-дерево `createForm` привязывает поля к сигналам, правила живут отдельной
67
- `ValidationSchema` и гоняются раннером `validateModel` по требованию.
169
+ Использование: схема формы привязывает поля к ручкам модели, правила живут отдельной
170
+ `ValidationDefinition` и уходят в сборку полем `validation`.
68
171
 
69
172
  ```typescript
70
- import { createModel, createForm, createFormFromModel } from '@reformer/core';
71
- import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
173
+ import { createForm, type FormModel, type FormSchemaNode } from '@reformer/core';
174
+ import { defineValidation, validate } from '@reformer/core/validation';
72
175
  import { required, min, max, email } from '@reformer/core/validators';
73
176
 
74
- type Loan = { email: string; age: number | null; amount: number | null };
75
- const model = createModel<Loan>({ email: '', age: null, amount: null });
177
+ interface Loan {
178
+ email: string;
179
+ age: number | null;
180
+ amount: number | null;
181
+ }
76
182
 
77
- // layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
78
- const form = createFormFromModel({
79
- model,
80
- schema: {
81
- email: { value: model.$.email, component: Input },
82
- age: { value: model.$.age, component: InputNumber },
83
- amount: { value: model.$.amount, component: InputNumber },
84
- },
183
+ // схема формы правил не несёт — только привязка поля к ручке модели + компонент
184
+ const loanSchema = (model: FormModel<Loan>): FormSchemaNode => ({
185
+ children: [
186
+ { model: model.$.email, component: Input },
187
+ { model: model.$.age, component: InputNumber },
188
+ { model: model.$.amount, component: InputNumber },
189
+ ],
85
190
  });
86
191
 
87
- // правила — в ОТДЕЛЬНОЙ схеме валидации (не в layout)
88
- const loanValidation = defineValidationSchema<Loan>(({ model }) => {
89
- validate(model.$.email, [required(), email()]);
90
- validate(model.$.age, [required(), min(18)]);
192
+ // правила — в ОТДЕЛЬНОЙ схеме валидации
193
+ const loanValidation = defineValidation<Loan>(({ model }) => {
194
+ validate(model.$.email, [required(), email()]);
195
+ validate(model.$.age, [required(), min(18)]);
91
196
  validate(model.$.amount, [min(0), max(1000)]);
92
197
  });
93
198
 
94
- const ok = await validateModel(model, loanValidation); // Promise<boolean>; ошибки сами доедут до form.<field>.errors
199
+ const loan = createForm<Loan>({
200
+ initial: { email: '', age: null, amount: null },
201
+ schema: loanSchema,
202
+ validation: loanValidation,
203
+ });
204
+
205
+ const result = await loan.validate(); // ValidationResult; замечания сами доедут до loan.form.<field>.issues
206
+ if (result.status === 'valid') send(loan.model.get());
95
207
  ```
96
208
 
97
209
  ### Custom & cross-field validators
98
210
 
99
- Контракт валидации — `@reformer/core/validation`. Схема (`ValidationSchema<T>`) — обычная функция над
100
- (под)моделью; внутри вызываются свободные операторы, которые сами пишут ошибки в ноды текущего прогона
101
- (`getNodeForSignal(sig).setErrors(...)` — автор коллектора не видит).
211
+ Контракт валидации — `@reformer/core/validation`. Схема (`ValidationDefinition<T>`) — обычная функция
212
+ над (под)моделью; внутри вызываются операторы, которые сами пишут замечания в сток текущего прогона —
213
+ автор коллектора не видит.
102
214
 
103
215
  ```typescript
104
- type Rule<T> = (value: T, scope: never, root: never) => ValidationError | null; // scope/root = never (см. ниже)
105
- type AsyncRule<T> = (value: T, ctx: { signal: AbortSignal }) => Promise<ValidationError | null>;
106
- type ValidationSchema<T> = (ctx: { model: FormModel<T> }) => void;
216
+ type Rule<T> = (value: T) => ValidationIssue | null;
217
+ type AsyncRule<T> = (value: T, ctx: { signal: AbortSignal }) => Promise<ValidationIssue | null>;
218
+
219
+ interface ValidationScope<T> {
220
+ readonly model: FormModel<T>;
221
+ // правило над несколькими полями: check получает снимок модели области
222
+ cross(handle: PathAwareSignal<unknown>, check: (snapshot: T) => ValidationIssue | null): void;
223
+ }
224
+ type ValidationDefinition<T> = (scope: ValidationScope<T>) => void;
107
225
  ```
108
226
 
109
- Операторы (ambient — валидны только внутри прогона `validateModel`):
227
+ Операторы (валидны только внутри прогона схемы):
110
228
 
111
229
  | Оператор | Назначение |
112
230
  |---|---|
113
231
  | `validate(sig, rules: Rule<T>[])` | синхронные правила поля |
114
232
  | `validateAsync(sig, rules: AsyncRule<T>[])` | асинхронные правила (раннер дожидается, прокидывает `AbortSignal`) |
115
233
  | `validateWhen(cond: () => boolean, cb: () => void)` | условная ветка: правила внутри активны/гасятся по `cond` (не трогает enable — это поведение) |
116
- | `cross(sig, fn: (form) => err \| null)` | cross-field; `fn` получает снапшот модели текущего scope (`model.get()`) |
117
- | `each(arr, itemFn: (im: FormModel<U>) => void)` | per-item по элементам массива модели |
118
- | `apply(...schemas: ValidationSchema<T>[])` | композиция под-схем над той же моделью |
119
- | `defineValidationSchema<T>(fn): ValidationSchema<T>` | тонкая identity-обёртка (типизация/discoverability) |
120
- | `validateModel<T>(model, schema): Promise<boolean>` | внешний раннер (warning не блокирует, устаревший прогон отменяется) |
234
+ | `cross(sig, check)` из аргумента схемы | правило над несколькими полями; `check` получает снимок модели области, тип снимка выведен |
235
+ | `apply(model.$.group, schema)` | правила подформы: схема над типом группы подключается к ручке группы |
236
+ | `applyEach(model.$.items, schema)` | правила строк массива: схема над типом строки |
237
+ | `apply(...schemas: ValidationDefinition<T>[])` | композиция схем над той же моделью |
238
+ | `defineValidation<T>(fn): ValidationDefinition<T>` | тонкая identity-обёртка (типизация/discoverability) |
239
+
240
+ Прогон. В сборке — уровень формы (`bundle.validate()`, `bundle.validateStep(id)`,
241
+ `bundle.submission.submit(handler)`, см. [38-form-flow.md](./38-form-flow.md)). Без сборки — раннер
242
+ либо контроллер стратегии:
243
+
244
+ | Функция | Что делает |
245
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
246
+ | `validateModel(model, schema, { touch }?)` | сбор + разнос; `true` только при статусе `valid` |
247
+ | `runValidation(model, schema)` | только сбор: `Promise<ValidationResult>`, ноды не трогает |
248
+ | `applyValidationResult(result, { touch }?)` | разнос собранного результата по нодам формы |
249
+ | `createFormValidation(model, schema, options?)` | контроллер стратегии: `validate()`, `run(options?)`, `start()`, `dispose()` — см. [33-validation-strategy.md](./33-validation-strategy.md) |
250
+
251
+ ```typescript
252
+ // Контроллер стратегии (@reformer/core/validation)
253
+ controller.validate(): Promise<boolean>; // нейтральная проверка: проверенные поля touched, фаза не меняется
254
+ controller.run(options?: FormValidationRunOptions): Promise<ValidationResult>;
255
+ interface FormValidationRunOptions {
256
+ touch?: true | 'all'; // 'all' — при отказе (invalid, error) тронуть всю форму
257
+ submit?: boolean; // попытка отправки: переводит afterFirstSubmit в живую фазу
258
+ }
121
259
 
122
- > `Rule<T>` намеренно несёт `scope: never, root: never` — так value-only фабрики (`required()` = `(value, model, root)`)
123
- > и inline `(value) => err` ОБА присваиваются в `Rule<T>[]`, сохраняя проверку типа поля
124
- > (`validate(model.$.age, [email()])` подсветится). Ментальная модель автора — `(value) => error`.
260
+ // Хук без сборки (@reformer/core/react)
261
+ useFormValidation({ model, schema, strategy?, debounce?, liveAfterSubmit? }): {
262
+ submit: () => Promise<boolean>; // попытка отправки: при отказе раскрыта вся форма
263
+ validate: () => Promise<boolean>; // проверка без отправки: фаза afterFirstSubmit не меняется
264
+ isValidating: boolean;
265
+ }
266
+ ```
267
+
268
+ ```typescript
269
+ type ValidationStatus = 'valid' | 'invalid' | 'cancelled' | 'error';
270
+
271
+ interface ValidationResult {
272
+ readonly status: ValidationStatus;
273
+ /** Замечания по ручке поля `model.$.…`; путь — `handle.__path`. */
274
+ readonly issues: ReadonlyMap<PathAwareSignal<unknown>, readonly ValidationIssue[]>;
275
+ /** Правила, которые не вернули результат (сеть, исключение). */
276
+ readonly failures: readonly { handle: PathAwareSignal<unknown>; error: unknown }[];
277
+ }
278
+ ```
279
+
280
+ - `valid` — блокирующих замечаний нет (`severity: 'warning'` не блокирует);
281
+ - `invalid` — есть блокирующие замечания;
282
+ - `error` — async-правило не вернуло результат: сбой **блокирует**, на поле ставится замечание
283
+ `{ code: 'ruleFailed' }` («Не удалось проверить поле. Повторите попытку»), причина — в `failures`;
284
+ - `cancelled` — прогон вытеснен более новым по той же паре (модель, схема); такой результат не
285
+ разносится.
286
+
287
+ `validateModel` отвечает `false` на всё, кроме `valid`. Различить замечания, сбой и отмену —
288
+ `bundle.validate()` сборки либо `runValidation`:
289
+
290
+ ```typescript
291
+ const result = await bundle.validate();
292
+ if (result.status === 'error') showToast('Не удалось проверить форму, повторите попытку');
293
+ ```
294
+
295
+ Пока идут async-правила поля, его нода в состоянии `pending` (`form.email.pending.value`).
125
296
 
126
297
  Кастомные правила, cross-field и async — в схеме:
127
298
 
128
299
  ```typescript
129
- import { type ValidationError } from '@reformer/core';
300
+ import { type ValidationIssue } from '@reformer/core';
130
301
  import {
131
- defineValidationSchema, validate, validateAsync, cross,
302
+ defineValidation, validate, validateAsync,
132
303
  type Rule, type AsyncRule,
133
304
  } from '@reformer/core/validation';
134
305
  import { required } from '@reformer/core/validators';
135
306
 
136
- type Signup = { password: string; confirm: string; email: string };
307
+ interface Signup {
308
+ password: string;
309
+ confirm: string;
310
+ email: string;
311
+ }
137
312
 
138
- // Value-only правило (Rule<T>)
313
+ // Правило значения (Rule<T>)
139
314
  const strongPassword: Rule<string> = (value) =>
140
- !value || value.length < 8 ? { code: 'too-short', message: 'Минимум 8 символов' } : null;
315
+ value.length < 8 ? { code: 'password.weak', params: { minLength: 8 } } : null;
141
316
 
142
- // Cross-field: обычная функция над снапшотом модели (model.get()), вешается через cross(sig, fn)
143
- const passwordsMatch = (f: Signup): ValidationError | null =>
144
- f.confirm && f.password && f.confirm !== f.password
145
- ? { code: 'mismatch', message: 'Пароли не совпадают' }
317
+ // Правило над несколькими полями: обычная функция над снимком модели
318
+ const passwordsMatch = (signup: Signup): ValidationIssue | null =>
319
+ signup.confirm && signup.password && signup.confirm !== signup.password
320
+ ? { code: 'confirm.mismatch' }
146
321
  : null;
147
322
 
148
- // Async (AsyncRule<T>): получает { signal }; сетевой сбой → null (submit не блокируется)
323
+ // Async (AsyncRule<T>): получает { signal }. Отклонённый промис — сбой правила: прогон получает
324
+ // статус `error`, поле — замечание `ruleFailed`. Если сетевой сбой не должен блокировать отправку,
325
+ // перехвати его в правиле и верни `null`.
149
326
  const emailUnique: AsyncRule<string> = async (value, { signal }) => {
150
327
  if (!value) return null;
151
- try {
152
- const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
153
- return (await res.json()).available ? null : { code: 'taken', message: 'Email уже зарегистрирован' };
154
- } catch {
155
- return null;
156
- }
328
+ const response = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
329
+ return (await response.json()).available
330
+ ? null
331
+ : { code: 'email.taken' };
157
332
  };
158
333
 
159
- const signupValidation = defineValidationSchema<Signup>(({ model }) => {
334
+ const signupValidation = defineValidation<Signup>(({ model, cross }) => {
160
335
  validate(model.$.password, [required(), strongPassword]);
161
336
  validate(model.$.confirm, [required()]);
162
- cross(model.$.confirm, passwordsMatch); // ошибка вешается на confirm
337
+ cross(model.$.confirm, passwordsMatch); // замечание вешается на confirm
163
338
  validateAsync(model.$.email, [emailUnique]);
164
339
  });
340
+ // Тексты кодов 'password.weak', 'confirm.mismatch', 'email.taken' — в словаре формы
341
+ // (defineIssueMessages), см. 36-issue-messages.md
165
342
  ```
166
343
 
344
+ > `cross` берётся только из аргумента схемы: из `@reformer/core/validation` он не импортируется.
345
+
167
346
  ### Conditional & array validation
168
347
 
169
- Условные ветки и массивы — операторы `validateWhen` / `each` / `apply` внутри схемы (не узлы дерева):
348
+ Условные ветки, подформы и массивы — операторы внутри схемы (не узлы дерева):
170
349
 
171
350
  - **условная ветка** `validateWhen(() => cond, () => { validate(...) })` — правила внутри активны только
172
- при истинном условии; при ложном ошибки полей ветки гасятся (`setErrors([])`). Enable/сброс поля — дело поведения;
173
- - **массив** `each(model.arr, (im) => { validate(im.$.field, [...]) })` — правила применяются к каждому
174
- элементу; `im` — под-модель элемента. Для cross-field над элементом захватывайте снапшот в замыкание (`const item = im.get()`);
175
- - **композиция** `apply(...schemas)` — объединяет под-схемы над той же моделью (напр. полную схему из per-step).
351
+ при истинном условии; при ложном замечания полей ветки гасятся. Enable/сброс поля — дело поведения;
352
+ - **подформа** `apply(model.$.group, groupRules)` — схема над типом группы; принимает и массив ручек;
353
+ - **массив** `applyEach(model.$.items, rowRules)` — схема над типом строки применяется к каждой строке;
354
+ - **композиция** `apply(...schemas)` — объединяет схемы над той же моделью.
355
+
356
+ У схемы, подключённой через `apply(ручка, схема)` / `applyEach`, своя область: `model` — под-модель,
357
+ `cross` получает её снимок.
176
358
 
177
359
  ```typescript
178
360
  import {
179
- defineValidationSchema, validate, validateWhen, cross, each, apply, validateModel,
361
+ apply, applyEach, defineValidation, validate, validateWhen,
180
362
  } from '@reformer/core/validation';
181
363
  import { required, min } from '@reformer/core/validators';
182
364
 
183
365
  // Условная валидация — ипотечная ветка активна только при loanType === 'mortgage'
184
- const step1 = defineValidationSchema<LoanForm>(({ model }) => {
366
+ const loanRules = defineValidation<LoanForm>(({ model }) => {
185
367
  validate(model.$.loanType, [required()]);
186
368
  validateWhen(
187
369
  () => model.loanType === 'mortgage',
@@ -192,64 +374,46 @@ const step1 = defineValidationSchema<LoanForm>(({ model }) => {
192
374
  );
193
375
  });
194
376
 
195
- // Per-item валидация элементов массива (each над ModelArray)
196
- const step5 = defineValidationSchema<LoanForm>(({ model }) => {
197
- each(model.coBorrowers, (im) => {
198
- const item = im.get(); // снапшот элемента для cross
199
- validate(im.$.income, [required(), min(10_000)]);
200
- cross(im.$.income, () =>
201
- item.income < item.loanShare ? { code: 'tooLow', message: 'Доход ниже доли' } : null);
202
- });
377
+ // Правила строки массива — отдельная схема над типом строки
378
+ const coBorrowerRules = defineValidation<CoBorrower>(({ model, cross }) => {
379
+ validate(model.$.income, [required(), min(10_000)]);
380
+ cross(model.$.income, (coBorrower) =>
381
+ coBorrower.income < coBorrower.loanShare ? { code: 'income.tooLow' } : null);
203
382
  });
204
383
 
205
- // Полная схема формы = композиция шагов (apply над той же моделью)
206
- const formValidation = defineValidationSchema<LoanForm>(() => apply(step1, step5));
384
+ const additionalRules = defineValidation<LoanForm>(({ model }) => {
385
+ applyEach(model.$.coBorrowers, coBorrowerRules);
386
+ });
207
387
  ```
208
388
 
209
- Wizard-конфиг (per-step + полная валидация) — поверх того же раннера `validateModel`:
389
+ Пошаговая и полная валидация — поток шагов сборки: `flow.steps[].id` — `selector` шага в схеме,
390
+ `validation.extras` — правила вне шагов.
210
391
 
211
392
  ```typescript
212
- const STEP_SCHEMAS = [step1, /* step2, … */ step5] as const;
393
+ const loan = createForm<LoanForm>({
394
+ initial,
395
+ schema: loanSchema,
396
+ validation: { extras: crossStepRules }, // правила всей формы — проверяются только целиком
397
+ flow: {
398
+ steps: [
399
+ { id: 'loan', rules: loanRules }, // id = selector узла шага
400
+ { id: 'additional', rules: additionalRules },
401
+ ],
402
+ },
403
+ });
213
404
 
214
- function makeValidationConfig(model: FormModel<LoanForm>) {
215
- return {
216
- validateStep: (n: number) => validateModel(model, STEP_SCHEMAS[n - 1]),
217
- validateAll: () => validateModel(model, formValidation), // formValidation = apply(...STEP_SCHEMAS, extras)
218
- };
219
- }
405
+ await loan.validateStep('loan'); // правила одного шага → ValidationResult
406
+ await loan.validate(); // все шаги + extras → ValidationResult
407
+ await loan.flow?.next(); // проверить текущий шаг и перейти → FlowTransition
408
+ loan.validating.value; // идёт ли проверка — полная либо шага
220
409
  ```
221
410
 
222
- Операторы `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply` экспортируются напрямую из
223
- `@reformer/core/validation` — это публичный API, а не локальный сахар примера.
411
+ Подробно — [13-multi-step.md](./13-multi-step.md) и [38-form-flow.md](./38-form-flow.md).
224
412
 
225
413
  ### Behaviors
226
414
 
227
- Два способа. **Примитивы из `@reformer/core`** (принимают сигналы, возвращают cleanup):
228
-
229
- ```typescript
230
- computeFrom(sources: ReadonlySignal[], target: Signal, fn: (...vals) => R, options?: { when?: (...vals) => boolean }): () => void
231
- copyFrom(source: ReadonlySignal, target: Signal, options?: { when?: () => boolean; transform?: (v) => v }): () => void
232
- watchField(source: ReadonlySignal, cb: (value) => void, options?: { immediate?: boolean }): () => void
233
- enableWhen(target: ReadonlySignal, condition: () => boolean, options?: { resetOnDisable?: boolean }): () => void
234
- disableWhen(target: ReadonlySignal, condition: () => boolean, options?: { resetOnDisable?: boolean }): () => void
235
- transformValue(target: Signal, transformer: (value) => value): () => void
236
- resetWhen(target: Signal, condition: () => boolean, options?: { resetValue?: T }): () => void
237
- syncFields(a: Signal, b: Signal, options?: { transform?: (v) => v }): () => void
238
- revalidateWhen(deps: ReadonlySignal[], revalidate: () => void): () => void
239
- ```
240
-
241
- ```typescript
242
- import { computeFrom, enableWhen, copyFrom } from '@reformer/core';
243
-
244
- const cleanups = [
245
- computeFrom([model.$.price, model.$.quantity], model.$.total, (p, q) => p * q),
246
- enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true }),
247
- copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
248
- ];
249
- // при teardown: cleanups.forEach((c) => c());
250
- ```
251
-
252
- **Декларативный DSL из `@reformer/core/behaviors`** (регистрирует cleanup сам, передаётся в `createForm({ behavior })`):
415
+ **Поведение формы — декларативный DSL из `@reformer/core/behaviors`** (регистрирует cleanup сам,
416
+ передаётся в `createForm({ behavior })`):
253
417
 
254
418
  ```typescript
255
419
  import { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';
@@ -262,15 +426,66 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
262
426
  });
263
427
  });
264
428
 
265
- const form = createFormFromModel({ model, schema, behavior });
429
+ const { form } = createForm<MyForm>({ initial, schema, behavior });
266
430
  ```
267
431
 
432
+ Область поведения — `{ model, form, schema, flow }`: `schema` адресует узлы своей области
433
+ (`schema.node(selector)`), `flow` — поток шагов сборки (без `flow` в конфиге — `undefined`).
434
+
268
435
  DSL-операторы: `compute` (auto-tracking, без явного списка источников), `computeFrom`, `copyFrom`,
269
436
  `onChange` (реакция на изменение; `{ debounce, immediate }`, 2-й аргумент колбэка — `{ signal }` AbortSignal),
270
437
  `enableWhen`/`disableWhen`, `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen`,
271
- `apply` (под-схема для группы), `applyEach` (per-item для массива), `exclusiveFlag`, `aggregateInto`.
438
+ `apply` (поведение подформы), `applyEach` (поведение строки массива), `exclusiveFlag`, `aggregateInto`;
439
+ операторы узлов схемы — `hideWhen`, `onComponentEvent`, `onMount`, `onUnmount`, `onInit`, `renderEffect`.
272
440
  См. `20-compute-vs-watch.md`.
273
441
 
442
+ - `copyFrom` групп разной формы — как `set` группы модели: поля цели, которых нет в значении
443
+ источника, остаются прежними; в dev — одно предупреждение на оператор. Значение цели целиком
444
+ собирает `transform`.
445
+ - `exclusiveFlag`: выбор строки (флаг стал `true`) снимает флаг у остальных. Строка, пришедшая с
446
+ `true` (начальные данные, добавленная), прежний выбор не снимает — флаг сбрасывается у неё; из
447
+ нескольких пришедших сразу остаётся первая.
448
+ - Ручка не того вида — в dev ошибка с путём: `applyEach` / `exclusiveFlag` / `aggregateInto` ждут
449
+ массив, `apply` — группу. `enableWhen` / `disableWhen` на поле без ноды формы ничего не делают;
450
+ в dev — предупреждение.
451
+
452
+ **Примитивы над сигналами — `@reformer/core/model`** (принимают сигналы, возвращают cleanup; в корне
453
+ `@reformer/core` их нет). Нужны вне формы — там, где нет области поведения:
454
+
455
+ ```typescript
456
+ computeFrom(sources: ReadonlySignal[], target: Signal, fn: (...vals) => R, options?: { when?: (...vals) => boolean }): () => void
457
+ copyFrom(source: ReadonlySignal, target: Signal, options?: { when?: () => boolean; transform?: (v) => v }): () => void
458
+ watchField(source: ReadonlySignal, cb: (value) => void, options?: { immediate?: boolean }): () => void
459
+ transformValue(target: Signal, transformer: (value) => value): () => void
460
+ resetWhen(target: Signal, condition: () => boolean, options?: { resetValue?: T }): () => void // resetValue обязателен, если null не в типе поля
461
+ syncFields(a: Signal, b: Signal, options?: { transform?: (v) => v }): () => void
462
+ revalidateWhen(deps: ReadonlySignal[], revalidate: () => void): () => void
463
+ ```
464
+
465
+ Утилиты значений там же, в `@reformer/core/model`:
466
+
467
+ ```typescript
468
+ // сравнение значений модели: объекты и массивы — по содержимому, даты — по времени, NaN равен себе;
469
+ // на нём стоят isDirty и сверка «модель изменилась, пока шла проверка» при отправке и переходе шага
470
+ deepEqual(first: unknown, second: unknown): boolean
471
+ // дождаться отложенных записей поведения текущего тика (onChange, resetOnDisable);
472
+ // undefined — ждать нечего
473
+ deferredWritesSettled(): Promise<void> | undefined
474
+ ```
475
+
476
+ ```typescript
477
+ import { computeFrom, copyFrom } from '@reformer/core/model';
478
+
479
+ const cleanups = [
480
+ computeFrom([model.$.price, model.$.quantity], model.$.total, (price, quantity) => price * quantity),
481
+ copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
482
+ ];
483
+ // при teardown: cleanups.forEach((cleanup) => cleanup());
484
+ ```
485
+
486
+ `enableWhen` / `disableWhen` работают с состоянием ноды формы и существуют только как операторы
487
+ поведения (`@reformer/core/behaviors`).
488
+
274
489
  Поведение **не владеет** валидацией — это отдельный слой. Мост «поведение инициирует валидацию» — через
275
490
  `revalidateWhen`, который просто вызывает внешний раннер валидации:
276
491