@reformer/core 11.0.0-beta.1 → 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 (283) 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 -24
  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 -85
  42. package/dist/form/types/field-config.d.ts +27 -0
  43. package/dist/form/types/index.d.ts +5 -57
  44. package/dist/form/types/nodes.d.ts +159 -0
  45. package/dist/form/types/schema-node.d.ts +147 -46
  46. package/dist/form/types/validation-schema.d.ts +25 -17
  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 +17 -5
  97. package/dist/index.js +942 -2601
  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 +58 -36
  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 +1 -2
  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 -1
  192. package/dist/validators.js +86 -108
  193. package/docs/llms/01-api-reference.md +60 -34
  194. package/docs/llms/02-quick-start.md +112 -74
  195. package/docs/llms/03-api-signatures.md +369 -148
  196. package/docs/llms/04-common-patterns.md +58 -56
  197. package/docs/llms/05-common-mistakes.md +69 -44
  198. package/docs/llms/06-troubleshooting.md +14 -9
  199. package/docs/llms/07-complete-import.md +74 -43
  200. package/docs/llms/08-form-types.md +11 -4
  201. package/docs/llms/09-formschema.md +132 -84
  202. package/docs/llms/10-arrays.md +141 -61
  203. package/docs/llms/11-async-watchfield.md +2 -2
  204. package/docs/llms/13-multi-step.md +134 -63
  205. package/docs/llms/14-extended-mistakes.md +20 -17
  206. package/docs/llms/15-project-structure.md +224 -70
  207. package/docs/llms/16-ui-components.md +86 -46
  208. package/docs/llms/17-nonexistent-api.md +217 -32
  209. package/docs/llms/18-conditional-fields.md +71 -32
  210. package/docs/llms/19-reading-values.md +45 -4
  211. package/docs/llms/20-compute-vs-watch.md +25 -10
  212. package/docs/llms/21-array-operations.md +76 -26
  213. package/docs/llms/22-cycle-detection.md +1 -1
  214. package/docs/llms/23-copy-from.md +2 -2
  215. package/docs/llms/24-sync-fields.md +6 -5
  216. package/docs/llms/25-reset-when.md +14 -7
  217. package/docs/llms/26-transform-value.md +4 -4
  218. package/docs/llms/27-revalidate-when.md +16 -15
  219. package/docs/llms/28-submit-and-reset.md +278 -128
  220. package/docs/llms/29-async-preload.md +41 -31
  221. package/docs/llms/30-type-safety-recipes.md +105 -51
  222. package/docs/llms/31-async-validator-debounce.md +96 -53
  223. package/docs/llms/33-validation-strategy.md +178 -66
  224. package/docs/llms/34-i18n.md +298 -0
  225. package/docs/llms/35-unified-contract.md +309 -0
  226. package/docs/llms/36-issue-messages.md +284 -0
  227. package/docs/llms/37-compiled-schema.md +332 -0
  228. package/docs/llms/38-form-flow.md +396 -0
  229. package/llms-index.json +1 -0
  230. package/llms.txt +10690 -3089
  231. package/package.json +48 -9
  232. package/dist/behaviors-node-DGEFMTQu.js +0 -18
  233. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  234. package/dist/date-utils-dTT_x_Be.js +0 -31
  235. package/dist/derived-registry-CluZCQ7U.js +0 -16
  236. package/dist/form/behaviors.d.ts +0 -133
  237. package/dist/form/error-handler.d.ts +0 -166
  238. package/dist/form/factories/index.d.ts +0 -6
  239. package/dist/form/factories/node-factory.d.ts +0 -188
  240. package/dist/form/form-submitter.d.ts +0 -121
  241. package/dist/form/nodes/array-node.d.ts +0 -276
  242. package/dist/form/nodes/field-node.d.ts +0 -294
  243. package/dist/form/nodes/form-node.d.ts +0 -326
  244. package/dist/form/nodes/group-node.d.ts +0 -284
  245. package/dist/form/nodes/model-array-node.d.ts +0 -121
  246. package/dist/form/status-machine.d.ts +0 -115
  247. package/dist/form/types/deep-schema.d.ts +0 -107
  248. package/dist/form/types/form-proxy.d.ts +0 -141
  249. package/dist/form/unique-id.d.ts +0 -45
  250. package/dist/form/validation/validators/date-utils.d.ts +0 -30
  251. package/dist/form/validation/validators/email.d.ts +0 -23
  252. package/dist/form/validation/validators/future-date.d.ts +0 -23
  253. package/dist/form/validation/validators/integer.d.ts +0 -22
  254. package/dist/form/validation/validators/is-date.d.ts +0 -23
  255. package/dist/form/validation/validators/is-number.d.ts +0 -24
  256. package/dist/form/validation/validators/max-age.d.ts +0 -25
  257. package/dist/form/validation/validators/max-date.d.ts +0 -25
  258. package/dist/form/validation/validators/max-files.d.ts +0 -26
  259. package/dist/form/validation/validators/max-length.d.ts +0 -27
  260. package/dist/form/validation/validators/max-total-file-size.d.ts +0 -26
  261. package/dist/form/validation/validators/max.d.ts +0 -25
  262. package/dist/form/validation/validators/min-age.d.ts +0 -25
  263. package/dist/form/validation/validators/min-date.d.ts +0 -25
  264. package/dist/form/validation/validators/min-files.d.ts +0 -26
  265. package/dist/form/validation/validators/min-length.d.ts +0 -27
  266. package/dist/form/validation/validators/min.d.ts +0 -25
  267. package/dist/form/validation/validators/multiple-of.d.ts +0 -24
  268. package/dist/form/validation/validators/non-negative.d.ts +0 -22
  269. package/dist/form/validation/validators/non-zero.d.ts +0 -22
  270. package/dist/form/validation/validators/past-date.d.ts +0 -23
  271. package/dist/form/validation/validators/pattern.d.ts +0 -33
  272. package/dist/form/validation/validators/required.d.ts +0 -32
  273. package/dist/form/validation/validators/url.d.ts +0 -43
  274. package/dist/form/validation-schema.d.ts +0 -106
  275. package/dist/form/validation-strategy.d.ts +0 -55
  276. package/dist/form/validators.d.ts +0 -11
  277. package/dist/form-model-DYCzGGKF.js +0 -286
  278. package/dist/state/index.d.ts +0 -31
  279. package/dist/state/subscription-manager.d.ts +0 -180
  280. package/dist/state/types.d.ts +0 -144
  281. package/dist/state.d.ts +0 -2
  282. package/dist/state.js +0 -21
  283. package/dist/subscription-manager-705bls97.js +0 -171
@@ -1,11 +1,12 @@
1
1
  ## 17. COMPUTE vs ONCHANGE
2
2
 
3
- Под M1 behaviors работают на сигналах модели. Есть два способа их писать:
3
+ Поведение работает на сигналах модели. Писать его можно двумя способами:
4
4
 
5
- - **примитивы из `@reformer/core`** — принимают сигналы, возвращают cleanup, вызываются
6
- императивно (например, в `useEffect`), cleanup складывается в массив;
7
5
  - **декларативный DSL из `@reformer/core/behaviors`** — `defineFormBehavior(...)` + операторы,
8
- cleanup управляется формой, передаётся в `createForm({ behavior })`.
6
+ cleanup управляется формой, передаётся в `createForm({ behavior })`. Это способ для формы;
7
+ - **примитивы из `@reformer/core/model`** — принимают сигналы, возвращают cleanup, вызываются
8
+ императивно (например, в `useEffect`), cleanup складывается в массив. Нужны вне формы; в корне
9
+ `@reformer/core` их нет.
9
10
 
10
11
  Для производных значений — `compute`/`computeFrom`. Для side-эффектов на изменение (async,
11
12
  обновление componentProps) — `onChange` (DSL) или примитив `watchField`.
@@ -14,7 +15,9 @@
14
15
 
15
16
  `compute(target, read)` подписывается на сигналы, прочитанные внутри `read()`, и пишет
16
17
  результат в `target`. Цель не входит в источники → цикла нет; запись идемпотентна (peek-guard).
17
- Кросс-уровневые вычисления работают так же — читай любые поля модели.
18
+ Кросс-уровневые вычисления работают так же — читай любые поля модели. Тип результата задаёт поле:
19
+ `compute(model.$.total, () => model.amount)` при `amount: number | null` и `total: number` не
20
+ компилируется — вычисление писало бы `null` в число (то же у `computeFrom`).
18
21
 
19
22
  ```typescript
20
23
  import { defineFormBehavior, compute } from '@reformer/core/behaviors';
@@ -39,7 +42,7 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
39
42
  > отследится и пересчёта не будет. Читай поля по отдельности (`model.field` или `model.$.field.value`).
40
43
  > `model.get()` — только вне реактивного контекста (в `onChange`, обработчиках событий).
41
44
 
42
- ### Computed sum over a FormArray
45
+ ### Computed sum over a FieldArray
43
46
 
44
47
  Агрегат по массиву (сумма доходов созаёмщиков и т.п.) считается **реактивно** через value-proxy
45
48
  массива — `model.<array>.map(...)`:
@@ -79,7 +82,7 @@ compute(model.$.interestRate, () => computeRate(model.properties.map(() => null)
79
82
  источников приходят в `fn` позиционно.
80
83
 
81
84
  ```typescript
82
- import { computeFrom } from '@reformer/core/behaviors'; // или из '@reformer/core' как примитив
85
+ import { computeFrom } from '@reformer/core/behaviors'; // примитив — из '@reformer/core/model'
83
86
 
84
87
  computeFrom(
85
88
  [model.$.loanAmount, model.$.loanTerm, model.$.interestRate],
@@ -88,7 +91,10 @@ computeFrom(
88
91
  );
89
92
  ```
90
93
 
91
- > Примитив `computeFrom` из `@reformer/core` имеет ту же сигнатуру и возвращает cleanup-функцию.
94
+ > Примитив `computeFrom` из `@reformer/core/model` имеет ту же сигнатуру и возвращает cleanup-функцию,
95
+ > но цель вычисляемой НЕ помечает: `model.set` / `patch` / `reset` её перезаписывают, `isDirty()` её
96
+ > учитывает — до следующей смены источников. Оператор из `@reformer/core/behaviors` цель помечает
97
+ > (`markDerived`), и массовая запись её не трогает. В поведении формы — оператор.
92
98
 
93
99
  ### onChange — реакция на изменение (async, side-effects)
94
100
 
@@ -113,14 +119,22 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
113
119
 
114
120
  ### Примитив watchField
115
121
 
116
- Низкоуровневая подписка из `@reformer/core` (без debounce/AbortSignal). `onChange` построен
122
+ Низкоуровневая подписка из `@reformer/core/model` (без debounce/AbortSignal). `onChange` построен
117
123
  поверх неё. Для простых синхронных реакций:
118
124
 
119
125
  ```typescript
120
- import { watchField } from '@reformer/core';
126
+ import { watchField } from '@reformer/core/model';
121
127
  const stop = watchField(model.$.country, () => { model.city = ''; });
122
128
  ```
123
129
 
130
+ Источником может быть не только поле: узлы-контейнеры дерева `$` (корень, группы, массивы) — тоже
131
+ `ReadonlySignal`, поэтому за поддеревом целиком следят без перечисления полей.
132
+
133
+ ```typescript
134
+ watchField(model.$.address, (addr) => geocode(addr)); // любое поле address
135
+ watchField(model.$, (all) => autosave(all)); // любое поле формы
136
+ ```
137
+
124
138
  ### Rule of Thumb
125
139
 
126
140
  | Scenario | Use |
@@ -129,6 +143,7 @@ const stop = watchField(model.$.country, () => { model.city = ''; });
129
143
  | Производное с явными зависимостями | `computeFrom` |
130
144
  | Async-реакция, обновление componentProps | `onChange` (debounce + AbortSignal) |
131
145
  | Простая синхронная реакция (примитив) | `watchField` |
146
+ | Реакция на любое изменение группы/формы | `watchField(model.$.<группа>)` или `.subscribe` |
132
147
 
133
148
  ### Chained computeds
134
149
 
@@ -1,7 +1,8 @@
1
1
  ## 18. ARRAY OPERATIONS
2
2
 
3
- Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);
4
- рендер — через ноду формы (`form.items.map` / `.at`).
3
+ Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`) либо через
4
+ ноду формы `FormArray` (`form.items`) — она данными не владеет и передаёт мутацию в массив модели;
5
+ рендер — через ноду формы (`form.items.map` / `.at` / `.keyAt`).
5
6
 
6
7
  ### Array Access
7
8
 
@@ -13,51 +14,73 @@ model.items.length; // реактивная длина
13
14
  model.items.toArray(); // снимок значений
14
15
 
15
16
  // Через ноду формы (рендер / доступ к нодам полей)
16
- form.items.at(0); // FormProxy<Item> | undefined
17
- form.items.map((item, i) => …); // item — FormProxy<Item>
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
18
21
  ```
19
22
 
20
- ### Array Methods (на модели)
23
+ ### Array Methods
21
24
 
22
25
  ```typescript
26
+ // на модели
23
27
  model.items.push({ name: '', price: 0 }); // добавить в конец (ПЛОСКИЕ значения!)
24
- model.items.insertAt(0, { name: '', price: 0 }); // вставить по индексу
25
- model.items.removeAt(index); // удалить по индексу
26
- model.items.move(fromIndex, toIndex); // переместить
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
27
32
  model.items.swap(a, b); // поменять местами
28
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();
29
42
  ```
30
43
 
44
+ Формы строк держатся за под-модели элементов: после `move` / `swap` строка остаётся той же нодой
45
+ со своими `touched` и замечаниями.
46
+
47
+ Разница между путями — в `dirty`. Мутация через ноду, которая поменяла состав строк (в том числе
48
+ перестановка), делает массив и форму изменёнными (`form.items.dirty`), снимает это `reset`. Запись в
49
+ модель — нет, как и у поля: загрузка данных форму не «пачкает». Кнопки добавления и удаления в
50
+ интерфейсе — через ноду.
51
+
31
52
  > **push принимает ПЛОСКИЕ значения** (`{ name, price }`), а НЕ FieldConfig-шаблон
32
- > (`{ value, component }`). Component/componentProps берутся из `item`-фабрики схемы.
53
+ > (`{ value, component }`). У массива с шаблоном `arrayOf` переданный объект накладывается на
54
+ > шаблон: недостающие ключи берутся из него (так `add({ type })` из cdk даёт полный элемент). Component/componentProps берутся из `item`-фабрики схемы.
33
55
  > Передача FieldConfig-объектов сломает рендер (`[object Object]` в инпутах).
34
56
 
35
57
  ### Rendering Arrays
36
58
 
37
59
  ```tsx
38
- import { useArrayLength } from '@reformer/core';
60
+ import { useArrayLength } from '@reformer/core/react';
39
61
 
40
- function ItemsList({ form }: { form: FormProxy<MyForm> }) {
62
+ function ItemsList({ form }: { form: FormGroup<MyForm> }) {
41
63
  const length = useArrayLength(form.items);
42
64
 
43
65
  return (
44
66
  <div>
45
67
  {form.items.map((item, index) => (
46
- <div key={index}>
68
+ <div key={form.items.keyAt(index)}>
47
69
  <FormField control={item.name} />
48
70
  <FormField control={item.price} />
49
- <button onClick={() => model.items.removeAt(index)}>Remove</button>
71
+ <button onClick={() => form.items.removeAt(index)}>Remove</button>
50
72
  </div>
51
73
  ))}
52
74
  {length === 0 && <p>No items yet</p>}
53
- <button onClick={() => model.items.push({ name: '', price: 0 })}>Add Item</button>
75
+ <button onClick={() => form.items.push({ name: '', price: 0 })}>Add Item</button>
54
76
  </div>
55
77
  );
56
78
  }
57
79
  ```
58
80
 
59
- > В монорепо используется `FormArraySection` из `@reformer/ui-kit`: `control={form.items}`,
60
- > `itemComponent`, `initialValue`, add/remove/reorder из коробки. См. `find_recipe(topic="form-array")`.
81
+ > В монорепо используется `FieldArraySection` из `@reformer/ui-kit`: `control={form.items}`,
82
+ > `itemComponent`, add/remove/reorder из коробки; шаблон новой строки — `arrayOf(blank)` в модели.
83
+ > См. `find_recipe(topic="field-array")`.
61
84
 
62
85
  ### Per-item behavior — applyEach
63
86
 
@@ -76,6 +99,32 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
76
99
  });
77
100
  ```
78
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
+
79
128
  ### Aggregate write — aggregateInto
80
129
 
81
130
  Агрегатная запись в строки (например, «последняя строка = 100 − Σ остальных»):
@@ -93,21 +142,22 @@ aggregateInto(model.$.rows, (rows) => {
93
142
 
94
143
  ### Array Cross-Validation
95
144
 
96
- Whole-array правило — оператор `cross(sig, (f) => ...)` из `@reformer/core/validation`:
97
- `f` — снапшот `model.get()`, ошибка вешается на скалярное поле-носитель `sig`. Per-item
98
- правила — `each(model.<array>, (im) => ...)`.
145
+ Правило над всем массивом — оператор `cross(sig, check)` из аргумента схемы валидации: `check`
146
+ получает снимок модели, ошибка вешается на скалярное поле-носитель `sig`. Правила строк —
147
+ `applyEach(model.$.<массив>, схема строки)`.
99
148
 
100
149
  ```typescript
101
- import { cross } from '@reformer/core/validation';
102
- import type { ValidationError } from '@reformer/core';
150
+ import { defineValidation } from '@reformer/core/validation';
151
+ import type { ValidationIssue } from '@reformer/core';
103
152
 
104
- const percentagesSumTo100 = (f: MyForm): ValidationError | null => {
105
- const total = f.items.reduce((sum, i) => sum + (i.percentage || 0), 0);
153
+ const percentagesSumTo100 = (form: MyForm): ValidationIssue | null => {
154
+ const total = form.items.reduce((sum, item) => sum + (item.percentage || 0), 0);
106
155
  return Math.abs(total - 100) > 0.01
107
- ? { code: 'invalid_total', message: 'Percentages must sum to 100%' }
156
+ ? { code: 'items.percentTotal', params: { total } } // текст кода — в словаре формы
108
157
  : null;
109
158
  };
110
159
 
111
- // внутри defineValidationSchema<MyForm>(({ model }) => { ... })
112
- cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
160
+ const itemsRules = defineValidation<MyForm>(({ model, cross }) => {
161
+ cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
162
+ });
113
163
  ```
@@ -1,6 +1,6 @@
1
1
  # Cycle Detection — предотвращение «Cycle detected»
2
2
 
3
- ## Как устроено под M1
3
+ ## Как устроено
4
4
 
5
5
  Behaviors на сигналах уже защищены от типичных циклов:
6
6
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## API
11
11
 
12
- Есть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):
12
+ Есть две формы. **Примитив из `@reformer/core/model`** (сигнал → сигнал):
13
13
 
14
14
  ```typescript
15
15
  function copyFrom<T>(
@@ -92,7 +92,7 @@ export const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {
92
92
  ### Как примитив (вне defineFormBehavior)
93
93
 
94
94
  ```typescript
95
- import { copyFrom } from '@reformer/core';
95
+ import { copyFrom } from '@reformer/core/model';
96
96
 
97
97
  const cleanups = [
98
98
  copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
@@ -10,17 +10,18 @@
10
10
 
11
11
  ## API
12
12
 
13
- Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
13
+ Одинаково в примитиве (`@reformer/core/model`) и DSL (`@reformer/core/behaviors`):
14
14
 
15
15
  ```typescript
16
16
  // примитив: возвращает cleanup
17
- function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): () => void;
17
+ function syncFields<T>(a: Signal<T>, b: Signal<NoInfer<T>>, options?: { transform?: (value: T) => T }): () => void;
18
18
 
19
19
  // DSL: cleanup управляется формой
20
- function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): void;
20
+ function syncFields<T>(a: Signal<T>, b: Signal<NoInfer<T>>, options?: { transform?: (value: T) => T }): void;
21
21
  ```
22
22
 
23
- `a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:
23
+ `a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. Тип пары задаёт `a`: поле `number | null`
24
+ вторым аргументом к `number` не компилируется — синхронизация писала бы `null` в `number`. `transform` **асимметричен**:
24
25
  применяется только при движении значения `a → b`. Опции `debounce`/`when` нет.
25
26
 
26
27
  ## Examples
@@ -57,7 +58,7 @@ export const codeBehavior = defineFormBehavior<DisplayForm>(({ model }) => {
57
58
  ### Как примитив (вне defineFormBehavior)
58
59
 
59
60
  ```typescript
60
- import { syncFields } from '@reformer/core';
61
+ import { syncFields } from '@reformer/core/model';
61
62
  const stop = syncFields(model.$.syncField1, model.$.syncField2);
62
63
  // stop() — отписаться
63
64
  ```
@@ -5,22 +5,29 @@
5
5
  `resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это
6
6
  альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но
7
7
  содержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но
8
- поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение.
8
+ поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение
9
+ и **обязателен по типу**, если `null` не входит в тип поля.
9
10
 
10
11
  ## API
11
12
 
12
- Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
13
+ Одинаково в примитиве (`@reformer/core/model`) и DSL (`@reformer/core/behaviors`):
13
14
 
14
15
  ```typescript
16
+ // опции: поле с null — необязательны; поле без null — resetValue обязателен
17
+ type ResetWhenOptionsArg<T> = null extends T
18
+ ? [options?: { resetValue?: NoInfer<T> }]
19
+ : [options: { resetValue: NoInfer<T> }];
20
+
15
21
  // примитив: возвращает cleanup
16
- function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): () => void;
22
+ function resetWhen<T>(target: Signal<T>, condition: () => boolean, ...options: ResetWhenOptionsArg<T>): () => void;
17
23
 
18
24
  // DSL: cleanup управляется формой
19
- function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): void;
25
+ function resetWhen<T>(target: Signal<T>, condition: () => boolean, ...options: ResetWhenOptionsArg<T>): void;
20
26
  ```
21
27
 
22
28
  `target` — сигнал (`model.$.field`). `condition` — реактивное условие (читает `model.*`).
23
- `resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
29
+ `resetValue` по умолчанию `null` — поэтому полю без `null` в типе (`string`, `number`) его нужно
30
+ задать явно, иначе код не компилируется. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
24
31
  поля не трогаются оператором (значения принадлежат модели).
25
32
 
26
33
  ## Examples
@@ -55,7 +62,7 @@ export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) =>
55
62
  ### Как примитив (вне defineFormBehavior)
56
63
 
57
64
  ```typescript
58
- import { resetWhen } from '@reformer/core';
65
+ import { resetWhen } from '@reformer/core/model';
59
66
  const stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
60
67
  ```
61
68
 
@@ -79,7 +86,7 @@ resetWhen(model.$.amount, () => model.skipPayment, { resetValue: 0 });
79
86
  ```
80
87
 
81
88
  ```typescript
82
- // ❌ Для строкового поля без resetValue прилетит null, а Input ждёт string
89
+ // ❌ Для строкового поля без resetValue прилетел бы null, а Input ждёт string — не компилируется
83
90
  resetWhen(model.$.cardNumber, () => model.paymentType !== 'card');
84
91
 
85
92
  // ✅ Явный resetValue для строк
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## API
13
13
 
14
- Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
14
+ Одинаково в примитиве (`@reformer/core/model`) и DSL (`@reformer/core/behaviors`):
15
15
 
16
16
  ```typescript
17
17
  // примитив: возвращает cleanup
@@ -22,7 +22,7 @@ function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): voi
22
22
  ```
23
23
 
24
24
  `target` — сигнал (`model.$.field`). `transformer` — чистая идемпотентная функция значения.
25
- Дополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора
25
+ Дополнительных опций (`debounce`, `onUserChangeOnly`) и готового набора
26
26
  `transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.
27
27
 
28
28
  ## Examples
@@ -70,7 +70,7 @@ export const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {
70
70
  ### Как примитив (вне defineFormBehavior)
71
71
 
72
72
  ```typescript
73
- import { transformValue } from '@reformer/core';
73
+ import { transformValue } from '@reformer/core/model';
74
74
  const stop = transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
75
75
  ```
76
76
 
@@ -122,7 +122,7 @@ A: 99% — неидемпотентный transformer. Проверь `transform
122
122
 
123
123
  **Q: Трансформация не применяется.**
124
124
  A: Проверь, что behavior зарегистрирован (`createForm({ behavior })`) / примитив не отписан,
125
- и что форма не пересоздаётся на каждый рендер (используй `useMemo`).
125
+ и что форма не пересоздаётся на каждый рендер (сборка — через `useFormBundle(() => createForm(…))`).
126
126
 
127
127
  **Q: Каретка input прыгает при наборе.**
128
128
  A: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## API
13
13
 
14
- `revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core`).
14
+ `revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core/model`).
15
15
  Его сигнатура НЕ зависит от контракта валидации:
16
16
 
17
17
  ```typescript
@@ -23,7 +23,7 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
23
23
  ```
24
24
 
25
25
  `deps` — массив сигналов-триггеров (`model.$.field`). `revalidate` — колбэк (обычно
26
- `() => void validateModel(model, schema)`, где `schema` — `defineValidationSchema<T>(({ model }) => …)`).
26
+ `() => void validateModel(model, schema)`, где `schema` — `defineValidation<T>(({ model }) => …)`).
27
27
  Вызывается при изменении любой зависимости (НЕ на инициализации), вне effect-контекста.
28
28
 
29
29
  ## Examples
@@ -43,7 +43,8 @@ export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
43
43
 
44
44
  Пример: `revalidateWhen([model.$.maxAmount], () => void validateModel(model, paymentSchema))`.
45
45
  Схема `paymentSchema` — стабильный module-level `const` (важно для отмены устаревших прогонов
46
- в `validateModel`), где правило amount навешано через `cross(model.$.amount, amountVsMax)`.
46
+ в `validateModel`), где правило amount навешано оператором `cross` области схемы:
47
+ `cross(model.$.amount, amountVsMax)`.
47
48
 
48
49
  ### Несколько триггеров
49
50
 
@@ -66,7 +67,7 @@ import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
66
67
  import { validateModel } from '@reformer/core/validation';
67
68
 
68
69
  // правило совпадения — cross-field в схеме: cross(model.$.confirmPassword, passwordsMatch),
69
- // где passwordsMatch читает снапшот формы (см. 03-api-signatures.md)
70
+ // где passwordsMatch получает снимок формы (см. 03-api-signatures.md)
70
71
  export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ model }) => {
71
72
  // при смене password перевалидируем схему (confirm перепроверится)
72
73
  revalidateWhen([model.$.password], () => {
@@ -78,7 +79,7 @@ export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ mode
78
79
  ### Как примитив (вне defineFormBehavior)
79
80
 
80
81
  ```typescript
81
- import { revalidateWhen } from '@reformer/core';
82
+ import { revalidateWhen } from '@reformer/core/model';
82
83
  import { validateModel } from '@reformer/core/validation';
83
84
 
84
85
  const stop = revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
@@ -100,13 +101,13 @@ revalidateWhen([model.$.maxAmount, model.$.discount], () => validateModel(model,
100
101
  revalidateWhen([model.$.maxAmount], () => validateModel(model, schema));
101
102
  // а валидатор amount — статичный max(1000), не читает maxAmount
102
103
 
103
- // ✅ Сначала cross-field правило (читает снапшот формы) в схеме валидации, потом revalidateWhen
104
- const amountVsMax = (f: Form): ValidationError | null =>
105
- f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
106
- ? { code: 'tooBig', message: '...' }
104
+ // ✅ Сначала cross-field правило (получает снимок формы) в схеме валидации, потом revalidateWhen
105
+ const amountVsMax = (form: Form): ValidationIssue | null =>
106
+ form.amount != null && form.maxAmount != null && form.amount > form.maxAmount
107
+ ? { code: 'tooBig' }
107
108
  : null;
108
109
 
109
- const schema = defineValidationSchema<Form>(({ model }) => {
110
+ const schema = defineValidation<Form>(({ model, cross }) => {
110
111
  cross(model.$.amount, amountVsMax); // навешиваем правило на поле amount
111
112
  });
112
113
  revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
@@ -115,18 +116,18 @@ revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
115
116
  ## Troubleshooting
116
117
 
117
118
  **Q: Ошибка target не пропадает после изменения триггера.**
118
- A: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, fn)`,
119
- где `fn` берёт снапшот формы (`model.get()` / `f`); (2) в `deps` передан именно сигнал (`model.$.trigger`);
120
- (3) колбэк вызывает `validateModel` (роутит ошибки в ноды через `getNodeForSignal(sig).setErrors`).
119
+ A: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, check)`,
120
+ где `check` получает снимок формы; (2) в `deps` передан именно сигнал (`model.$.trigger`);
121
+ (3) колбэк вызывает `validateModel` (он разносит ошибки по нодам формы).
121
122
 
122
123
  **Q: Как перевалидировать только одно поле, а не всю схему?**
123
124
  A: Схема — обычная функция, поэтому опиши стабильную под-схему только для этого поля и прогони её:
124
125
  `validateModel(model, oneFieldSchema)`, где
125
- `const oneFieldSchema = defineValidationSchema<Form>(({ model }) => cross(model.$.amount, amountVsMax))`.
126
+ `const oneFieldSchema = defineValidation<Form>(({ model, cross }) => cross(model.$.amount, amountVsMax))`.
126
127
  Раннер выставит/погасит ошибки только затронутых полей (гашение накапливается на пару `(model, schema)`).
127
128
 
128
129
  ## See also
129
130
 
130
- - [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross(sig, fn)` и раннер `validateModel`
131
+ - [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross` области схемы и раннер `validateModel`
131
132
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
132
133
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения