@reformer/core 10.2.0 → 11.0.0-beta.10

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 (285) hide show
  1. package/README.md +90 -68
  2. package/dist/behaviors-value-D05z3my6.js +109 -0
  3. package/dist/behaviors.d.ts +1 -1
  4. package/dist/behaviors.js +376 -211
  5. package/dist/create-model-DPSG3I0O.js +30 -0
  6. package/dist/date-utils-90-qZbse.js +43 -0
  7. package/dist/en-HP8XEych.js +44 -0
  8. package/dist/form/aggregate-signals.d.ts +21 -63
  9. package/dist/form/behaviors/collections.d.ts +84 -0
  10. package/dist/form/behaviors/context.d.ts +56 -0
  11. package/dist/form/behaviors/index.d.ts +30 -0
  12. package/dist/form/behaviors/internals.d.ts +41 -0
  13. package/dist/form/{behaviors-node.d.ts → behaviors/node.d.ts} +28 -5
  14. package/dist/form/behaviors/operators.d.ts +59 -0
  15. package/dist/form/behaviors/schema-operators.d.ts +83 -0
  16. package/dist/form/behaviors/types.d.ts +37 -0
  17. package/dist/form/create-form.d.ts +59 -70
  18. package/dist/form/flow/flow.d.ts +87 -0
  19. package/dist/form/form-bundle.d.ts +153 -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 +23 -27
  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 +159 -0
  37. package/dist/form/schema-subtree.d.ts +28 -0
  38. package/dist/form/signal-node-registry.d.ts +27 -22
  39. package/dist/form/submission.d.ts +93 -0
  40. package/dist/form/type-guards.d.ts +14 -73
  41. package/dist/form/types/contracts.d.ts +21 -91
  42. package/dist/form/types/field-config.d.ts +27 -0
  43. package/dist/form/types/index.d.ts +5 -91
  44. package/dist/form/types/nodes.d.ts +159 -0
  45. package/dist/form/types/schema-node.d.ts +148 -50
  46. package/dist/form/types/validation-schema.d.ts +24 -41
  47. package/dist/form/validation/context.d.ts +67 -0
  48. package/dist/form/validation/index.d.ts +24 -28
  49. package/dist/form/validation/operators.d.ts +101 -0
  50. package/dist/form/validation/run.d.ts +120 -0
  51. package/dist/form/validation/strategy.d.ts +89 -0
  52. package/dist/form/validation/types.d.ts +83 -0
  53. package/dist/form/validators/date-utils.d.ts +42 -0
  54. package/dist/form/validators/email.d.ts +23 -0
  55. package/dist/form/{validation/validators → validators}/file-type.d.ts +10 -10
  56. package/dist/form/{validation/validators → validators}/file-utils.d.ts +3 -2
  57. package/dist/form/validators/future-date.d.ts +23 -0
  58. package/dist/form/{validation/validators → validators}/index.d.ts +7 -4
  59. package/dist/form/validators/integer.d.ts +22 -0
  60. package/dist/form/validators/is-date.d.ts +24 -0
  61. package/dist/form/validators/is-number.d.ts +25 -0
  62. package/dist/form/validators/max-age.d.ts +28 -0
  63. package/dist/form/validators/max-date.d.ts +25 -0
  64. package/dist/form/{validation/validators → validators}/max-file-size.d.ts +10 -10
  65. package/dist/form/validators/max-files.d.ts +26 -0
  66. package/dist/form/validators/max-length.d.ts +33 -0
  67. package/dist/form/validators/max-total-file-size.d.ts +26 -0
  68. package/dist/form/validators/max.d.ts +26 -0
  69. package/dist/form/validators/min-age.d.ts +27 -0
  70. package/dist/form/validators/min-date.d.ts +25 -0
  71. package/dist/form/{validation/validators → validators}/min-file-size.d.ts +10 -10
  72. package/dist/form/validators/min-files.d.ts +27 -0
  73. package/dist/form/validators/min-length.d.ts +33 -0
  74. package/dist/form/validators/min.d.ts +26 -0
  75. package/dist/form/validators/multiple-of.d.ts +25 -0
  76. package/dist/form/validators/non-negative.d.ts +22 -0
  77. package/dist/form/validators/non-zero.d.ts +22 -0
  78. package/dist/form/validators/past-date.d.ts +23 -0
  79. package/dist/form/validators/pattern.d.ts +26 -0
  80. package/dist/form/{validation/validators → validators}/phone.d.ts +11 -11
  81. package/dist/form/validators/required.d.ts +29 -0
  82. package/dist/form/validators/url.d.ts +45 -0
  83. package/dist/form/validators/validation-issue.d.ts +8 -0
  84. package/dist/i18n/descriptor.d.ts +54 -0
  85. package/dist/i18n/en.json.d.ts +49 -0
  86. package/dist/i18n/loader.d.ts +71 -0
  87. package/dist/i18n/locale-store.d.ts +46 -0
  88. package/dist/i18n/locale.d.ts +90 -0
  89. package/dist/i18n/message-format.d.ts +136 -0
  90. package/dist/i18n/resolve-localized.d.ts +17 -0
  91. package/dist/i18n/ru.json.d.ts +49 -0
  92. package/dist/i18n/translator.d.ts +61 -0
  93. package/dist/i18n/validation-message.d.ts +53 -0
  94. package/dist/i18n.d.ts +26 -0
  95. package/dist/i18n.js +86 -0
  96. package/dist/index.d.ts +18 -6
  97. package/dist/index.js +932 -2779
  98. package/dist/loader-BWkigpVe.js +52 -0
  99. package/dist/locale/en.d.ts +2 -0
  100. package/dist/locale/en.js +5 -0
  101. package/dist/locale/en.json +45 -0
  102. package/dist/locale/index.d.ts +18 -0
  103. package/dist/locale/ru.d.ts +2 -0
  104. package/dist/locale/ru.js +44 -0
  105. package/dist/locale/ru.json +45 -0
  106. package/dist/locale-CG54AU2Q.js +219 -0
  107. package/dist/locale.d.ts +2 -0
  108. package/dist/locale.js +9 -0
  109. package/dist/{state → model}/behaviors-value.d.ts +24 -6
  110. package/dist/{state/form-model.d.ts → model/create-model.d.ts} +18 -4
  111. package/dist/{state → model}/derived-registry.d.ts +1 -1
  112. package/dist/model/index.d.ts +42 -0
  113. package/dist/model/model-nodes.d.ts +190 -0
  114. package/dist/model/model-signals-proxy.d.ts +63 -0
  115. package/dist/model/model-value-proxy.d.ts +84 -0
  116. package/dist/{state → model}/safe-effect.d.ts +50 -2
  117. package/dist/model/types.d.ts +239 -0
  118. package/dist/model.d.ts +2 -0
  119. package/dist/model.js +27 -0
  120. package/dist/node-kind-C-r6ZMdo.js +15 -0
  121. package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
  122. package/dist/platforms/react/field-binding/field-handle.d.ts +53 -0
  123. package/dist/platforms/react/form-bundle-context.d.ts +27 -0
  124. package/dist/{form → platforms/react}/hooks/types.d.ts +60 -38
  125. package/dist/platforms/react/hooks/use-form-bundle.d.ts +41 -0
  126. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  127. package/dist/{form → platforms/react}/hooks/use-form-validation.d.ts +13 -7
  128. package/dist/{form → platforms/react}/hooks/useArrayLength.d.ts +6 -6
  129. package/dist/{form → platforms/react}/hooks/useFormControl.d.ts +7 -9
  130. package/dist/{form → platforms/react}/hooks/useFormControlValue.d.ts +18 -18
  131. package/dist/{form → platforms/react}/hooks/useSignalSubscription.d.ts +9 -1
  132. package/dist/platforms/react/i18n/context.d.ts +18 -0
  133. package/dist/platforms/react/i18n/hooks.d.ts +65 -0
  134. package/dist/platforms/react/i18n/provider.d.ts +59 -0
  135. package/dist/platforms/react/index.d.ts +30 -0
  136. package/dist/react.d.ts +22 -0
  137. package/dist/react.js +505 -0
  138. package/dist/runtime-token.d.ts +22 -0
  139. package/dist/safe-effect-BazLOv1g.js +669 -0
  140. package/dist/schema-controller-CpiO2JoK.js +143 -0
  141. package/dist/signals.d.ts +15 -9
  142. package/dist/signals.js +11 -6
  143. package/dist/strategy-DzvnsWLh.js +375 -0
  144. package/dist/type-guards-D-N_5Ddh.js +19 -0
  145. package/dist/validation-issue-B9mjI8jz.js +7 -0
  146. package/dist/validation-message-Ced9MGxs.js +194 -0
  147. package/dist/validation.d.ts +1 -1
  148. package/dist/validation.js +11 -165
  149. package/dist/validators/email.d.ts +1 -1
  150. package/dist/validators/email.js +5 -8
  151. package/dist/validators/future-date.d.ts +1 -1
  152. package/dist/validators/future-date.js +10 -13
  153. package/dist/validators/integer.d.ts +1 -1
  154. package/dist/validators/integer.js +4 -7
  155. package/dist/validators/is-date.d.ts +1 -1
  156. package/dist/validators/is-date.js +5 -8
  157. package/dist/validators/is-number.d.ts +1 -1
  158. package/dist/validators/is-number.js +4 -7
  159. package/dist/validators/max-age.d.ts +1 -1
  160. package/dist/validators/max-age.js +8 -12
  161. package/dist/validators/max-date.d.ts +1 -1
  162. package/dist/validators/max-date.js +10 -13
  163. package/dist/validators/max-length.d.ts +1 -1
  164. package/dist/validators/max-length.js +7 -10
  165. package/dist/validators/max.d.ts +1 -1
  166. package/dist/validators/max.js +4 -7
  167. package/dist/validators/min-age.d.ts +1 -1
  168. package/dist/validators/min-age.js +10 -11
  169. package/dist/validators/min-date.d.ts +1 -1
  170. package/dist/validators/min-date.js +8 -11
  171. package/dist/validators/min-length.d.ts +1 -1
  172. package/dist/validators/min-length.js +4 -7
  173. package/dist/validators/min.d.ts +1 -1
  174. package/dist/validators/min.js +4 -7
  175. package/dist/validators/multiple-of.d.ts +1 -1
  176. package/dist/validators/multiple-of.js +11 -10
  177. package/dist/validators/non-negative.d.ts +1 -1
  178. package/dist/validators/non-negative.js +2 -5
  179. package/dist/validators/non-zero.d.ts +1 -1
  180. package/dist/validators/non-zero.js +4 -7
  181. package/dist/validators/past-date.d.ts +1 -1
  182. package/dist/validators/past-date.js +10 -13
  183. package/dist/validators/pattern.d.ts +1 -1
  184. package/dist/validators/pattern.js +5 -8
  185. package/dist/validators/phone.d.ts +1 -1
  186. package/dist/validators/phone.js +10 -11
  187. package/dist/validators/required.d.ts +1 -1
  188. package/dist/validators/required.js +4 -7
  189. package/dist/validators/url.d.ts +1 -1
  190. package/dist/validators/url.js +48 -17
  191. package/dist/validators.d.ts +1 -5
  192. package/dist/validators.js +98 -154
  193. package/docs/llms/01-api-reference.md +89 -0
  194. package/docs/llms/02-quick-start.md +181 -0
  195. package/docs/llms/03-api-signatures.md +494 -0
  196. package/docs/llms/04-common-patterns.md +204 -0
  197. package/docs/llms/05-common-mistakes.md +192 -0
  198. package/docs/llms/06-troubleshooting.md +16 -0
  199. package/docs/llms/07-complete-import.md +130 -0
  200. package/docs/llms/08-form-types.md +54 -0
  201. package/docs/llms/09-formschema.md +191 -0
  202. package/docs/llms/10-arrays.md +241 -0
  203. package/docs/llms/11-async-watchfield.md +53 -0
  204. package/docs/llms/12-array-cleanup.md +31 -0
  205. package/docs/llms/13-multi-step.md +162 -0
  206. package/docs/llms/14-extended-mistakes.md +101 -0
  207. package/docs/llms/15-project-structure.md +279 -0
  208. package/docs/llms/16-ui-components.md +229 -0
  209. package/docs/llms/17-nonexistent-api.md +278 -0
  210. package/docs/llms/18-conditional-fields.md +222 -0
  211. package/docs/llms/19-reading-values.md +100 -0
  212. package/docs/llms/20-compute-vs-watch.md +153 -0
  213. package/docs/llms/21-array-operations.md +163 -0
  214. package/docs/llms/22-cycle-detection.md +86 -0
  215. package/docs/llms/23-copy-from.md +141 -0
  216. package/docs/llms/24-sync-fields.md +115 -0
  217. package/docs/llms/25-reset-when.md +122 -0
  218. package/docs/llms/26-transform-value.md +136 -0
  219. package/docs/llms/27-revalidate-when.md +133 -0
  220. package/docs/llms/28-submit-and-reset.md +409 -0
  221. package/docs/llms/29-async-preload.md +203 -0
  222. package/docs/llms/30-type-safety-recipes.md +213 -0
  223. package/docs/llms/31-async-validator-debounce.md +178 -0
  224. package/docs/llms/32-async-options-loading.md +108 -0
  225. package/docs/llms/33-validation-strategy.md +305 -0
  226. package/docs/llms/34-i18n.md +298 -0
  227. package/docs/llms/35-unified-contract.md +309 -0
  228. package/docs/llms/36-issue-messages.md +284 -0
  229. package/docs/llms/37-compiled-schema.md +332 -0
  230. package/docs/llms/38-form-flow.md +396 -0
  231. package/llms-index.json +1 -0
  232. package/llms.txt +10014 -3826
  233. package/package.json +77 -15
  234. package/dist/behaviors-node-DGEFMTQu.js +0 -18
  235. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  236. package/dist/date-utils-dTT_x_Be.js +0 -31
  237. package/dist/derived-registry-CluZCQ7U.js +0 -16
  238. package/dist/form/behaviors.d.ts +0 -133
  239. package/dist/form/error-handler.d.ts +0 -166
  240. package/dist/form/factories/index.d.ts +0 -6
  241. package/dist/form/factories/node-factory.d.ts +0 -188
  242. package/dist/form/form-submitter.d.ts +0 -125
  243. package/dist/form/nodes/array-node.d.ts +0 -360
  244. package/dist/form/nodes/field-node.d.ts +0 -294
  245. package/dist/form/nodes/form-node.d.ts +0 -340
  246. package/dist/form/nodes/group-node.d.ts +0 -288
  247. package/dist/form/nodes/model-array-node.d.ts +0 -121
  248. package/dist/form/status-machine.d.ts +0 -153
  249. package/dist/form/types/deep-schema.d.ts +0 -116
  250. package/dist/form/types/form-proxy.d.ts +0 -141
  251. package/dist/form/unique-id.d.ts +0 -53
  252. package/dist/form/validation/validators/date-utils.d.ts +0 -30
  253. package/dist/form/validation/validators/email.d.ts +0 -23
  254. package/dist/form/validation/validators/future-date.d.ts +0 -23
  255. package/dist/form/validation/validators/integer.d.ts +0 -22
  256. package/dist/form/validation/validators/is-date.d.ts +0 -23
  257. package/dist/form/validation/validators/is-number.d.ts +0 -24
  258. package/dist/form/validation/validators/max-age.d.ts +0 -25
  259. package/dist/form/validation/validators/max-date.d.ts +0 -25
  260. package/dist/form/validation/validators/max-files.d.ts +0 -26
  261. package/dist/form/validation/validators/max-length.d.ts +0 -27
  262. package/dist/form/validation/validators/max-total-file-size.d.ts +0 -26
  263. package/dist/form/validation/validators/max.d.ts +0 -25
  264. package/dist/form/validation/validators/min-age.d.ts +0 -25
  265. package/dist/form/validation/validators/min-date.d.ts +0 -25
  266. package/dist/form/validation/validators/min-files.d.ts +0 -26
  267. package/dist/form/validation/validators/min-length.d.ts +0 -27
  268. package/dist/form/validation/validators/min.d.ts +0 -25
  269. package/dist/form/validation/validators/multiple-of.d.ts +0 -24
  270. package/dist/form/validation/validators/non-negative.d.ts +0 -22
  271. package/dist/form/validation/validators/non-zero.d.ts +0 -22
  272. package/dist/form/validation/validators/past-date.d.ts +0 -23
  273. package/dist/form/validation/validators/pattern.d.ts +0 -33
  274. package/dist/form/validation/validators/required.d.ts +0 -32
  275. package/dist/form/validation/validators/url.d.ts +0 -43
  276. package/dist/form/validation-schema.d.ts +0 -106
  277. package/dist/form/validation-strategy.d.ts +0 -55
  278. package/dist/form/validators.d.ts +0 -6
  279. package/dist/form-model-DYCzGGKF.js +0 -286
  280. package/dist/state/index.d.ts +0 -31
  281. package/dist/state/subscription-manager.d.ts +0 -180
  282. package/dist/state/types.d.ts +0 -144
  283. package/dist/state.d.ts +0 -2
  284. package/dist/state.js +0 -21
  285. package/dist/subscription-manager-705bls97.js +0 -171
@@ -0,0 +1,222 @@
1
+ # Условные поля — видимость, доступность и валидация
2
+
3
+ ## Purpose
4
+
5
+ «Показать поле X только если Y» решается тремя независимыми механизмами — выбирай по тому, что
6
+ именно должно быть условным:
7
+
8
+ 1. **Видимость / доступность** (поле остаётся в модели, но выключается/включается и не валидируется)
9
+ — behaviors: `enableWhen`, а для условной записи значения — `compute` / `copyFrom` с опцией `{ when }`.
10
+ 2. **Условная валидация** (правила применяются только в истинной ветке) — оператор
11
+ `validateWhen(() => cond, () => {…})` внутри схемы `defineValidation`; прогон — уровнем формы
12
+ (`bundle.validate()` / `bundle.submission.submit`) либо раннером `validateModel`.
13
+ 3. **Скрытие из разметки** (узел вообще не рендерится) — в схеме под рендерером — оператор
14
+ `hideWhen(schema.node(selector), cond)`; при разметке в JSX — `useFormControlValue` + условный
15
+ рендер.
16
+
17
+ > Условная **валидация** — оператор `validateWhen` из `@reformer/core/validation`; старого branch-узла
18
+ > `{ when, children }` и хелпера `applyWhen` больше нет — см.
19
+ > [Anti-patterns удалённого контракта валидации](#anti-patterns-удалённого-контракта-валидации) и `17-nonexistent-api.md`.
20
+
21
+ ## Видимость и доступность
22
+
23
+ `enableWhen(target, () => condition, { resetOnDisable })` из `@reformer/core/behaviors`
24
+ включает/выключает поле реактивно. При `disabled` поле не участвует в валидации; с
25
+ `resetOnDisable: true` его значение сбрасывается при выключении (иначе — сохраняется).
26
+
27
+ ```typescript
28
+ import { defineFormBehavior, enableWhen, compute, copyFrom } from '@reformer/core/behaviors';
29
+
30
+ type CreditForm = {
31
+ loanType: 'mortgage' | 'car' | 'consumer';
32
+ propertyValue: number | null;
33
+ initialPayment: number | null;
34
+ sameEmail: boolean;
35
+ email: string;
36
+ emailAdditional: string;
37
+ };
38
+
39
+ export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
40
+ // один или несколько таргетов; условие читает model.* реактивно
41
+ enableWhen(
42
+ [model.$.propertyValue, model.$.initialPayment],
43
+ () => model.loanType === 'mortgage',
44
+ { resetOnDisable: true },
45
+ );
46
+
47
+ // условная ЗАПИСЬ производного значения — compute с { when }
48
+ compute(model.$.initialPayment, () => Math.round((model.propertyValue ?? 0) * 0.2), {
49
+ when: () => model.loanType === 'mortgage',
50
+ });
51
+
52
+ // условное КОПИРОВАНИЕ из другого поля — copyFrom с { when }
53
+ copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true });
54
+ });
55
+ ```
56
+
57
+ Требования:
58
+
59
+ - **Все поля должны быть материализованы в модели** (`createModel`), даже те, что показываются
60
+ только под условием. `enableWhen`/`compute`/`copyFrom` принимают сигналы `model.$.field`, а не
61
+ строковые пути; сигнала для несуществующего поля нет.
62
+ - Поведение подключается к форме через `createForm({ …, behavior })` (или
63
+ `createFormFromModel({ model, schema, behavior })`) — форма владеет жизненным циклом,
64
+ DSL-операторы регистрируют свой cleanup сами (ручной массив cleanup'ов не нужен).
65
+ - Таргетом может быть и **группа**: `enableWhen(model.$.residenceAddress, () => model.sameAsRegistration === false)`
66
+ проходит по поддереву; без `resetOnDisable` значение группы сохраняется (удобно, когда оно копируется
67
+ из другого источника).
68
+
69
+ > `enableWhen` / `disableWhen` существуют только как операторы поведения
70
+ > (`@reformer/core/behaviors`): в корне `@reformer/core` и в `@reformer/core/model` их нет.
71
+
72
+ Скрыть узел разметки, когда форму рисует рендерер, — оператор узла схемы в том же поведении:
73
+
74
+ ```typescript
75
+ import { defineFormBehavior, enableWhen, hideWhen } from '@reformer/core/behaviors';
76
+
77
+ export const mortgageBehavior = defineFormBehavior<CreditForm>(({ model, schema }) => {
78
+ const isMortgage = () => model.loanType === 'mortgage';
79
+ enableWhen([model.$.propertyValue, model.$.initialPayment], isMortgage, { resetOnDisable: true });
80
+ hideWhen(schema.node('mortgage'), () => !isMortgage()); // узел с selector: 'mortgage'
81
+ });
82
+ ```
83
+
84
+ `hideWhen` только записывает правило — исполняет его рендерер; при разметке в JSX он ничего не
85
+ делает (см. раздел «Скрытие в JSX»).
86
+
87
+ **Скрытие — только видимость.** Поля скрытого узла (`hideWhen` или
88
+ `schema.node(sel).setHidden(true)`) остаются включёнными, и правила их проверяют: шаг с
89
+ незаполненным скрытым полем не пройдёт, а замечаний на экране не будет. Поэтому узел с полями
90
+ скрывают вместе с `enableWhen` по тому же условию — как в примере выше.
91
+
92
+ ## Условная валидация
93
+
94
+ Правила, действующие только при выполнении условия, оборачиваются оператором
95
+ `validateWhen(() => cond, () => {…})` внутри схемы `defineValidation<T>(({ model }) => …)`.
96
+ Условие `cond` читает модель напрямую (`model.field` — снимок на момент прогона): при истинном условии
97
+ раннер применяет вложенные `validate`/`cross`/…, при ложном — правила ветки **гасятся** (поля,
98
+ затронутые ранее, получают `setIssues([])`). Прогон — уровнем формы (`createForm({ validation })`,
99
+ затем `bundle.validate()` / `bundle.submission.submit(handler)`) либо раннером
100
+ `validateModel(model, schema): Promise<boolean>`; замечания сами доезжают до нод формы.
101
+
102
+ ```typescript
103
+ import {
104
+ validate,
105
+ validateWhen,
106
+ defineValidation,
107
+ validateModel,
108
+ } from '@reformer/core/validation';
109
+ import { required, min } from '@reformer/core/validators';
110
+
111
+ const schema = defineValidation<CreditForm>(({ model }) => {
112
+ validate(model.$.loanType, [required()]);
113
+
114
+ // ветка применяется только для ипотеки
115
+ validateWhen(
116
+ () => model.loanType === 'mortgage',
117
+ () => {
118
+ validate(model.$.propertyValue, [required(), min(1_000_000)]);
119
+ validate(model.$.initialPayment, [required()]);
120
+ },
121
+ );
122
+ });
123
+
124
+ const ok: boolean = await validateModel(model, schema);
125
+ ```
126
+
127
+ Переключение `loanType` с `mortgage` на другой тип автоматически снимает ошибки `required` с
128
+ `propertyValue`/`initialPayment` — при ложном условии ветка `validateWhen` гасит их (`setIssues([])`).
129
+ `validateWhen` управляет только **валидацией**; чтобы поля ещё и выпадали из состояния (сброс
130
+ значения), держи их под `enableWhen` из раздела выше — слои независимы.
131
+
132
+ Выключенное поле (`enableWhen`, `disable()`) в проверке **уровня формы** не участвует:
133
+ `bundle.validate`, `validateStep`, `submission.submit`, `flow.next()` и живая проверка гасят его
134
+ замечания, и статус прогона совпадает с `valid` формы. Значение выключенного поля при этом остаётся
135
+ в модели и уходит в обработчик отправки — модель передаётся целиком; убрать его из отправки —
136
+ забота обработчика либо `enableWhen(…, { resetOnDisable: true })`. `validateWhen` нужен, когда
137
+ поле остаётся доступным, но правило по условию не действует, — и в независимом раннере
138
+ `validateModel(model, rules)`, который о нодах не знает.
139
+
140
+ ## Скрытие в JSX
141
+
142
+ Чтобы условный блок вообще не рендерился, читай значение-триггер хуком `useFormControlValue(form.field)`
143
+ и делай ранний `return null`. Хук возвращает **только значение** (без деструктуризации `{ value }`).
144
+
145
+ ```tsx
146
+ import type { FormGroup } from '@reformer/core';
147
+ import { useFormControlValue } from '@reformer/core/react';
148
+
149
+ function MortgageFields({ form }: { form: FormGroup<CreditForm> }) {
150
+ const loanType = useFormControlValue(form.loanType);
151
+ if (loanType !== 'mortgage') return null;
152
+
153
+ return (
154
+ <>
155
+ <PropertyValueField form={form} />
156
+ <InitialPaymentField form={form} />
157
+ </>
158
+ );
159
+ }
160
+ ```
161
+
162
+ Скрытие в JSX — чисто визуальное: если поле должно ещё и **выпадать из валидации/состояния**,
163
+ сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или `validateWhen` (валидация).
164
+
165
+ ## Anti-patterns удалённого контракта валидации
166
+
167
+ Условную валидацию раньше собирали иначе; эти формы удалены при переходе на `@reformer/core/validation`.
168
+ Ниже — **как НЕ надо** (чтобы не регрессировать); рабочий вариант — `validateWhen` из раздела выше.
169
+
170
+ - **Нет branch-узла `{ when, children }` в дереве схемы.** Старая схема была деревом
171
+ `{ value, validators: [...] }` с ветками `{ when: (scope, root) => boolean, children: [...] }`,
172
+ которое обходил `validateFormModel`. Теперь схема — функция `({ model, cross }) => void`, а условие —
173
+ оператор `validateWhen(() => cond, () => {…})`; отдельного объекта-узла нет.
174
+
175
+ ```typescript
176
+ // УДАЛЕНО — так больше нельзя
177
+ const schema = {
178
+ children: [
179
+ { value: model.$.loanType, validators: [required()] },
180
+ { when: (_s, root) => root.loanType === 'mortgage', children: [ /* … */ ] },
181
+ ],
182
+ };
183
+ await validateFormModel(model, schema); // символа нет
184
+ ```
185
+
186
+ - **`applyWhen` не существует** — ни как экспорт, ни как локальный сахар: он эмитил branch-узел,
187
+ которого больше нет. Оборачивай условные правила оператором `validateWhen`.
188
+
189
+ - **`ValidationSchemaFn` удалён** (тип старой path-схемы). Тип схемы — `ValidationDefinition<T>`
190
+ (`(scope: { model: FormModel<T>; cross }) => void`), фабрика-обёртка — `defineValidation<T>(fn)`.
191
+
192
+ - **Cross-field — обычная функция над снимком, а не `ModelValidator (value, scope, root)`.**
193
+ Правило значения — `Rule<T> = (value: T) => error | null`, параметров `scope` / `root` у него нет.
194
+ Соседние поля читаются из снимка модели, а правило вешается оператором `cross(sig, check)` из
195
+ аргумента схемы. Каноничное использование — каталог `validation/` формы `complex-multy-step-form`.
196
+
197
+ ```typescript
198
+ import { defineValidation } from '@reformer/core/validation';
199
+ import type { ValidationIssue } from '@reformer/core';
200
+
201
+ type Passwords = { password: string; confirmPassword: string };
202
+
203
+ // (snapshot) => error — сравнение с соседним полем
204
+ const passwordsMatch = (passwords: Passwords): ValidationIssue | null =>
205
+ passwords.confirmPassword &&
206
+ passwords.password &&
207
+ passwords.confirmPassword !== passwords.password
208
+ ? { code: 'confirmPassword.mismatch' } // текст кода — в словаре формы
209
+ : null;
210
+
211
+ // cross — из аргумента схемы; check получает снимок модели области:
212
+ const passwordRules = defineValidation<Passwords>(({ model, cross }) => {
213
+ cross(model.$.confirmPassword, passwordsMatch);
214
+ });
215
+ ```
216
+
217
+ ## See also
218
+
219
+ - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom` (поведение) и `validate`/`validateWhen`/`cross`/`validateModel` (валидация)
220
+ - [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, `validateFormModel`, …)
221
+ - [25-reset-when.md](./25-reset-when.md) — `resetWhen` как альтернатива, когда поле остаётся enabled
222
+ - [19-reading-values.md](./19-reading-values.md) — `useFormControlValue` и чтение значений в React
@@ -0,0 +1,100 @@
1
+ ## 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
2
+
3
+ Значения живут в модели. Есть три контекста чтения: value-доступ модели, сигналы, и React-хуки
4
+ (`@reformer/core/react`).
5
+
6
+ ### В React-компоненте — хуки
7
+
8
+ ```typescript
9
+ // Полное состояние поля (объект); текст замечания — useIssueMessage(messages)(issues[0])
10
+ // shouldShowIssues — показывать ли тексты замечаний; shouldShowError — aria-invalid
11
+ const { value, issues, disabled, touched, shouldShowIssues } = useFormControl(control.email);
12
+
13
+ // Только значение (напрямую, БЕЗ деструктуризации!)
14
+ const email = useFormControlValue(control.email);
15
+
16
+ // Реактивная длина массива
17
+ const count = useArrayLength(control.items);
18
+ ```
19
+
20
+ ### Вне React — нода формы
21
+
22
+ ```typescript
23
+ form.email.value.value; // сигнал значения ноды (реактивно в effect/computed)
24
+ form.email.getValue(); // значение без подписки
25
+ form.email.issues.value; // замечания: ValidationIssue[] — код и параметры, без текста
26
+ form.items.length; // число строк массива (число; в эффекте подписывает)
27
+ form.getValue(); // значение группы целиком
28
+ ```
29
+
30
+ ### Вне React — модель
31
+
32
+ ```typescript
33
+ // value-доступ (реактивно внутри effect/computed, запись присваиванием)
34
+ model.email; // читать
35
+ model.email = 'a@b.c'; // писать
36
+ model.address.city; // вложенное поле (model.address — под-модель FormModel<Address>)
37
+
38
+ // через сигнал (escape-hatch)
39
+ model.$.email.value; // реактивное чтение/запись
40
+ model.$.email.peek(); // нереактивный снимок
41
+ model.$.address.city.value; // сигнал вложенного поля (≡ model.address.$.city у под-модели)
42
+
43
+ // весь объект
44
+ model.get(); // НЕреактивный снимок { email, address: { city }, ... } — для submit
45
+ ```
46
+
47
+ > ⚠️ `model.get()` и `model.isDirty()` читают через `peek()` — внутри `effect`/`computed` они НЕ
48
+ > создают зависимостей. `computed(() => model.get())` никогда не пересчитается. Для реактивного
49
+ > чтения всего объекта — узел дерева `$` (ниже).
50
+
51
+ ### Подписка на группу и на модель целиком
52
+
53
+ Каждый узел дерева `$` — сигнал: лист отдаёт `PathAwareSignal`, а корень, вложенные объекты-группы
54
+ и массивы — `ReadonlySignal` агрегированного значения поддерева. Поэтому подписаться можно на любом
55
+ уровне, а не только на конкретном поле.
56
+
57
+ ```typescript
58
+ model.$.subscribe((all) => autosave(all)); // любое изменение модели; all: T целиком
59
+ model.$.address.subscribe((addr) => ...); // только поддерево address
60
+ model.$.address.value; // реактивный снимок группы
61
+ model.$.address.peek(); // нереактивный снимок группы
62
+ model.$.items.subscribe((rows) => ...); // массив: и правка элемента, и push/removeAt/move
63
+
64
+ // узел — обычный ReadonlySignal, поэтому принимается операциями слоя данных
65
+ watchField(model.$.address, (addr) => geocode(addr));
66
+ ```
67
+
68
+ Дети узла доступны как раньше — `model.$.address.city` по-прежнему сигнал поля. Подписчик вызывается
69
+ сразу с текущим значением (семантика `Signal.subscribe`), а `model.set(...)`/`model.reset()`
70
+ уведомляют один раз, а не по разу на поле.
71
+
72
+ > ⚠️ Доступ к полю выигрывает у свойства сигнала: если в форме есть поле с именем `value`, `peek`,
73
+ > `subscribe`, `valueOf`, `toString`, `toJSON` или `brand`, то `model.$.<группа>.<это имя>` вернёт
74
+ > сигнал поля. `subscribe` при этом продолжает работать.
75
+
76
+ ### В behaviors — читаем model напрямую
77
+
78
+ `compute`/`onChange`/условия `when` читают значения из value-модели (`model.field`) —
79
+ подписка на сигналы происходит автоматически внутри реактивного эффекта.
80
+
81
+ ```typescript
82
+ import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';
83
+
84
+ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
85
+ // читаем несколько полей — compute сам подпишется на прочитанные сигналы
86
+ compute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
87
+
88
+ // onChange: 1-й аргумент — новое значение; остальные поля берём из model
89
+ onChange(model.$.loanAmount, (amount) => {
90
+ const term = model.loanTerm;
91
+ if (amount && term) {
92
+ form.monthlyPayment.updateComponentProps({ hint: `≈ ${amount / term}` });
93
+ }
94
+ });
95
+ });
96
+ ```
97
+
98
+ > Cross-field ЗАПИСЬ производного значения делай через `compute` (цель не входит в источники →
99
+ > цикла нет). Для side-эффектов (загрузка опций, обновление componentProps) — `onChange`.
100
+ > `computeFrom`/`copyFrom`/`enableWhen` принимают сигналы (`model.$.x`), НЕ строковые пути.
@@ -0,0 +1,153 @@
1
+ ## 17. COMPUTE vs ONCHANGE
2
+
3
+ Поведение работает на сигналах модели. Писать его можно двумя способами:
4
+
5
+ - **декларативный DSL из `@reformer/core/behaviors`** — `defineFormBehavior(...)` + операторы,
6
+ cleanup управляется формой, передаётся в `createForm({ behavior })`. Это способ для формы;
7
+ - **примитивы из `@reformer/core/model`** — принимают сигналы, возвращают cleanup, вызываются
8
+ императивно (например, в `useEffect`), cleanup складывается в массив. Нужны вне формы; в корне
9
+ `@reformer/core` их нет.
10
+
11
+ Для производных значений — `compute`/`computeFrom`. Для side-эффектов на изменение (async,
12
+ обновление componentProps) — `onChange` (DSL) или примитив `watchField`.
13
+
14
+ ### compute — auto-tracking (DSL)
15
+
16
+ `compute(target, read)` подписывается на сигналы, прочитанные внутри `read()`, и пишет
17
+ результат в `target`. Цель не входит в источники → цикла нет; запись идемпотентна (peek-guard).
18
+ Кросс-уровневые вычисления работают так же — читай любые поля модели. Тип результата задаёт поле:
19
+ `compute(model.$.total, () => model.amount)` при `amount: number | null` и `total: number` не
20
+ компилируется — вычисление писало бы `null` в число (то же у `computeFrom`).
21
+
22
+ ```typescript
23
+ import { defineFormBehavior, compute } from '@reformer/core/behaviors';
24
+
25
+ const behavior = defineFormBehavior<MyForm>(({ model }) => {
26
+ // same-level
27
+ compute(model.$.total, () => (model.price ?? 0) * (model.quantity ?? 0));
28
+
29
+ // nested-to-nested / cross-level — просто читаем нужные поля
30
+ compute(model.$.fullName, () =>
31
+ [model.personalData.firstName, model.personalData.lastName].filter(Boolean).join(' ')
32
+ );
33
+
34
+ // условный пересчёт
35
+ compute(model.$.initialPayment, () => model.propertyValue * 0.2, {
36
+ when: () => model.loanType === 'mortgage',
37
+ });
38
+ });
39
+ ```
40
+
41
+ > **Не читай `model.get()` внутри `compute`/`when`** — это нереактивный снимок, зависимость не
42
+ > отследится и пересчёта не будет. Читай поля по отдельности (`model.field` или `model.$.field.value`).
43
+ > `model.get()` — только вне реактивного контекста (в `onChange`, обработчиках событий).
44
+
45
+ ### Computed sum over a FieldArray
46
+
47
+ Агрегат по массиву (сумма доходов созаёмщиков и т.п.) считается **реактивно** через value-proxy
48
+ массива — `model.<array>.map(...)`:
49
+
50
+ ```typescript
51
+ compute(model.$.coBorrowersIncome, () =>
52
+ model.coBorrowers.map((cb) => cb.monthlyIncome ?? 0).reduce((s, v) => s + v, 0)
53
+ );
54
+ ```
55
+
56
+ Почему реактивно: `.map` читает сигнал самого массива (трекает `push`/`removeAt`/reorder) **и**
57
+ внутри колбэка читает каждый `cb.<field>` (трекает правки элементов) → пересчёт срабатывает на оба
58
+ вида изменений.
59
+
60
+ Ключевой нюанс — **на каком proxy** живёт `.map`/`.reduce`(-via-`.map`):
61
+
62
+ - **value-proxy `model.coBorrowers`** — есть `.map`/`.forEach`/`.at`/индексы (обходит элементы как
63
+ значения). Именно его читай в `compute`.
64
+ - **signals-proxy `model.$.coBorrowers`** — только индексный доступ и `.length`, без `.map`. Для
65
+ агрегата не подходит.
66
+
67
+ Если нужен только **счётчик** (пересчёт лишь на изменение длины, без чтения полей элементов) —
68
+ `model.<array>.map(() => null)` (длина трекается, значения — нет):
69
+
70
+ ```typescript
71
+ // зависит только от КОЛИЧЕСТВА элементов, не от их полей
72
+ compute(model.$.interestRate, () => computeRate(model.properties.map(() => null).length));
73
+ ```
74
+
75
+ > **Не агрегируй через `model.get()` / `model.<array>.peek()`** в `compute` — это нереактивный
76
+ > снапшот массива, зависимость не отследится и сумма не пересчитается. Читай массив только через
77
+ > value-proxy `model.<array>.map(...)`.
78
+
79
+ ### computeFrom — явный список источников
80
+
81
+ Когда нужен явный контроль зависимостей — `computeFrom(sources, target, fn)`. Значения
82
+ источников приходят в `fn` позиционно.
83
+
84
+ ```typescript
85
+ import { computeFrom } from '@reformer/core/behaviors'; // примитив — из '@reformer/core/model'
86
+
87
+ computeFrom(
88
+ [model.$.loanAmount, model.$.loanTerm, model.$.interestRate],
89
+ model.$.monthlyPayment,
90
+ (amount, term, rate) => annuityMonthly(amount ?? 0, term ?? 0, rate ?? 0)
91
+ );
92
+ ```
93
+
94
+ > Примитив `computeFrom` из `@reformer/core/model` имеет ту же сигнатуру и возвращает cleanup-функцию,
95
+ > но цель вычисляемой НЕ помечает: `model.set` / `patch` / `reset` её перезаписывают, `isDirty()` её
96
+ > учитывает — до следующей смены источников. Оператор из `@reformer/core/behaviors` цель помечает
97
+ > (`markDerived`), и массовая запись её не трогает. В поведении формы — оператор.
98
+
99
+ ### onChange — реакция на изменение (async, side-effects)
100
+
101
+ `onChange(source, cb, { debounce, immediate })` вызывает `cb(value, { signal })` при изменении.
102
+ Колбэк выполняется ВНЕ effect-контекста — можно писать сигналы/ноды. `signal` (AbortSignal)
103
+ аннулируется при следующей смене значения.
104
+
105
+ ```typescript
106
+ import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
107
+
108
+ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
109
+ onChange(
110
+ model.$.country,
111
+ async (country, { signal }) => {
112
+ const cities = await fetchCities(country, { signal });
113
+ form.city.updateComponentProps({ options: cities });
114
+ },
115
+ { debounce: 300 }
116
+ );
117
+ });
118
+ ```
119
+
120
+ ### Примитив watchField
121
+
122
+ Низкоуровневая подписка из `@reformer/core/model` (без debounce/AbortSignal). `onChange` построен
123
+ поверх неё. Для простых синхронных реакций:
124
+
125
+ ```typescript
126
+ import { watchField } from '@reformer/core/model';
127
+ const stop = watchField(model.$.country, () => { model.city = ''; });
128
+ ```
129
+
130
+ Источником может быть не только поле: узлы-контейнеры дерева `$` (корень, группы, массивы) — тоже
131
+ `ReadonlySignal`, поэтому за поддеревом целиком следят без перечисления полей.
132
+
133
+ ```typescript
134
+ watchField(model.$.address, (addr) => geocode(addr)); // любое поле address
135
+ watchField(model.$, (all) => autosave(all)); // любое поле формы
136
+ ```
137
+
138
+ ### Rule of Thumb
139
+
140
+ | Scenario | Use |
141
+ |----------|-----|
142
+ | Производное значение (любой уровень) | `compute` (auto-tracking) |
143
+ | Производное с явными зависимостями | `computeFrom` |
144
+ | Async-реакция, обновление componentProps | `onChange` (debounce + AbortSignal) |
145
+ | Простая синхронная реакция (примитив) | `watchField` |
146
+ | Реакция на любое изменение группы/формы | `watchField(model.$.<группа>)` или `.subscribe` |
147
+
148
+ ### Chained computeds
149
+
150
+ Если несколько вычислений зависят друг от друга (`interestRate` → `monthlyPayment` →
151
+ `paymentToIncomeRatio`), объяви каждое отдельным `compute` — они выстроятся в правильном
152
+ порядке через реактивный граф (цель одного = источник другого). Расходящиеся взаимные
153
+ `compute` без стабилизации бросят понятную ошибку (см. `22-cycle-detection.md`).
@@ -0,0 +1,163 @@
1
+ ## 18. ARRAY OPERATIONS
2
+
3
+ Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`) либо через
4
+ ноду формы `FormArray` (`form.items`) — она данными не владеет и передаёт мутацию в массив модели;
5
+ рендер — через ноду формы (`form.items.map` / `.at` / `.keyAt`).
6
+
7
+ ### Array Access
8
+
9
+ ```typescript
10
+ // Через ModelArray (данные)
11
+ model.items.at(0); // FormModel<Item> | undefined (под-модель элемента)
12
+ model.items.map((item, i) => item.name); // item — FormModel<Item>
13
+ model.items.length; // реактивная длина
14
+ model.items.toArray(); // снимок значений
15
+
16
+ // Через ноду формы (рендер / доступ к нодам полей)
17
+ form.items.at(0); // FormGroup<Item> | undefined
18
+ form.items.map((item, i) => …); // item — FormGroup<Item>
19
+ form.items.length; // число строк (число, не сигнал; в эффекте подписывает)
20
+ form.items.keyAt(0); // стабильный ключ строки — не меняется при move/swap
21
+ ```
22
+
23
+ ### Array Methods
24
+
25
+ ```typescript
26
+ // на модели
27
+ model.items.push({ name: '', price: 0 }); // добавить в конец (ПЛОСКИЕ значения!)
28
+ model.items.push(); // по шаблону arrayOf(blank)
29
+ model.items.insertAt(0, { name: '', price: 0 }); // вставить по индексу 0…length; за концом — RangeError
30
+ model.items.removeAt(index); // удалить по индексу; нет такого — ничего
31
+ model.items.move(fromIndex, toIndex); // переместить; индекс за концом — RangeError
32
+ model.items.swap(a, b); // поменять местами
33
+ model.items.clear(); // очистить
34
+
35
+ // на ноде формы — те же мутации (вставка называется insert)
36
+ form.items.push();
37
+ form.items.insert(0, { name: '', price: 0 });
38
+ form.items.removeAt(index);
39
+ form.items.move(fromIndex, toIndex);
40
+ form.items.swap(a, b);
41
+ form.items.clear();
42
+ ```
43
+
44
+ Формы строк держатся за под-модели элементов: после `move` / `swap` строка остаётся той же нодой
45
+ со своими `touched` и замечаниями.
46
+
47
+ Разница между путями — в `dirty`. Мутация через ноду, которая поменяла состав строк (в том числе
48
+ перестановка), делает массив и форму изменёнными (`form.items.dirty`), снимает это `reset`. Запись в
49
+ модель — нет, как и у поля: загрузка данных форму не «пачкает». Кнопки добавления и удаления в
50
+ интерфейсе — через ноду.
51
+
52
+ > **push принимает ПЛОСКИЕ значения** (`{ name, price }`), а НЕ FieldConfig-шаблон
53
+ > (`{ value, component }`). У массива с шаблоном `arrayOf` переданный объект накладывается на
54
+ > шаблон: недостающие ключи берутся из него (так `add({ type })` из cdk даёт полный элемент). Component/componentProps берутся из `item`-фабрики схемы.
55
+ > Передача FieldConfig-объектов сломает рендер (`[object Object]` в инпутах).
56
+
57
+ ### Rendering Arrays
58
+
59
+ ```tsx
60
+ import { useArrayLength } from '@reformer/core/react';
61
+
62
+ function ItemsList({ form }: { form: FormGroup<MyForm> }) {
63
+ const length = useArrayLength(form.items);
64
+
65
+ return (
66
+ <div>
67
+ {form.items.map((item, index) => (
68
+ <div key={form.items.keyAt(index)}>
69
+ <FormField control={item.name} />
70
+ <FormField control={item.price} />
71
+ <button onClick={() => form.items.removeAt(index)}>Remove</button>
72
+ </div>
73
+ ))}
74
+ {length === 0 && <p>No items yet</p>}
75
+ <button onClick={() => form.items.push({ name: '', price: 0 })}>Add Item</button>
76
+ </div>
77
+ );
78
+ }
79
+ ```
80
+
81
+ > В монорепо используется `FieldArraySection` из `@reformer/ui-kit`: `control={form.items}`,
82
+ > `itemComponent`, add/remove/reorder из коробки; шаблон новой строки — `arrayOf(blank)` в модели.
83
+ > См. `find_recipe(topic="field-array")`.
84
+
85
+ ### Per-item behavior — applyEach
86
+
87
+ Чтобы применить поведение к КАЖДОМУ элементу (реагируя на add/remove), используй `applyEach`:
88
+
89
+ ```typescript
90
+ import { defineFormBehavior, applyEach, compute } from '@reformer/core/behaviors';
91
+
92
+ const behavior = defineFormBehavior<MyForm>(({ model }) => {
93
+ applyEach(
94
+ model.$.items,
95
+ defineFormBehavior<Item>(({ model: row }) => {
96
+ compute(row.$.lineTotal, () => row.qty * row.price); // per-row value-op
97
+ })
98
+ );
99
+ });
100
+ ```
101
+
102
+ ### Вложенные области — массив в группе и в строке массива
103
+
104
+ Узел `{ model: model.$.<массив>, item }` и операторы `applyEach` / `apply` / `enableWhen` работают на любой глубине:
105
+ цель оператора — **ручка** `model.$.…`, под-модель и нода находятся по её идентичности, а не по
106
+ пути.
107
+
108
+ ```typescript
109
+ const phoneBehavior = defineFormBehavior<Phone>(({ model: phone }) => {
110
+ enableWhen(phone.$.number, () => phone.primary);
111
+ });
112
+
113
+ const contactBehavior = defineFormBehavior<Contact>(({ model: contact }) => {
114
+ applyEach(contact.$.phones, phoneBehavior); // массив внутри строки
115
+ enableWhen(contact.$.address, () => contact.quantity > 0); // группа внутри строки
116
+ apply(contact.$.address, addressBehavior); // под-схема получает настоящую под-модель
117
+ });
118
+
119
+ const behavior = defineFormBehavior<MyForm>(({ model }) => {
120
+ applyEach(model.$.contacts, contactBehavior);
121
+ });
122
+ ```
123
+
124
+ Ручка → value-фасад: `modelOf(model.$.address)` — под-модель (`get` / `set` / `patch`),
125
+ `modelOf(model.$.phones)` — фасад массива (`push` / `removeAt` / `at`). Нода формы по ручке —
126
+ `getNodeForSignal(model.$.address)`.
127
+
128
+ ### Aggregate write — aggregateInto
129
+
130
+ Агрегатная запись в строки (например, «последняя строка = 100 − Σ остальных»):
131
+
132
+ ```typescript
133
+ import { aggregateInto } from '@reformer/core/behaviors';
134
+
135
+ aggregateInto(model.$.rows, (rows) => {
136
+ const n = rows.length;
137
+ if (n === 0) return [];
138
+ const others = rows.slice(0, n - 1).reduce((s, r) => s + r.percent, 0);
139
+ return [{ index: n - 1, patch: { percent: 100 - others } }]; // derive должна сходиться
140
+ });
141
+ ```
142
+
143
+ ### Array Cross-Validation
144
+
145
+ Правило над всем массивом — оператор `cross(sig, check)` из аргумента схемы валидации: `check`
146
+ получает снимок модели, ошибка вешается на скалярное поле-носитель `sig`. Правила строк —
147
+ `applyEach(model.$.<массив>, схема строки)`.
148
+
149
+ ```typescript
150
+ import { defineValidation } from '@reformer/core/validation';
151
+ import type { ValidationIssue } from '@reformer/core';
152
+
153
+ const percentagesSumTo100 = (form: MyForm): ValidationIssue | null => {
154
+ const total = form.items.reduce((sum, item) => sum + (item.percentage || 0), 0);
155
+ return Math.abs(total - 100) > 0.01
156
+ ? { code: 'items.percentTotal', params: { total } } // текст кода — в словаре формы
157
+ : null;
158
+ };
159
+
160
+ const itemsRules = defineValidation<MyForm>(({ model, cross }) => {
161
+ cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
162
+ });
163
+ ```