@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
  ## 7. FORM TYPE DEFINITION
2
2
 
3
- Form-shape объявляй через `type`-alias (не `interface` — см. `30-type-safety-recipes.md`).
4
- Тип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без узлов/сигналов.
3
+ Форму данных объявляй через `interface` или `type`-alias — оба варианта работают одинаково (см.
4
+ `30-type-safety-recipes.md`). Тип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без
5
+ узлов/сигналов.
5
6
 
6
7
  ```typescript
7
8
  // CORRECT form type definition
8
- type MyForm = {
9
+ interface MyForm {
9
10
  // Required fields
10
11
  name: string;
11
12
  email: string;
@@ -28,7 +29,7 @@ type MyForm = {
28
29
  id: string;
29
30
  name: string;
30
31
  }>;
31
- };
32
+ }
32
33
 
33
34
  // Модель создаётся из initial-значений этого типа
34
35
  const model = createModel<MyForm>({
@@ -45,3 +46,9 @@ const model = createModel<MyForm>({
45
46
  `number | null` / `string | null` работают со встроенными валидаторами напрямую —
46
47
  `min`/`max`/`minLength`/`minDate`/`minAge` пропускают пустые значения внутри, guard `if (v != null)`
47
48
  не нужен.
49
+
50
+ Какой ноде формы соответствует поле, решает вид значения: объект — `FormGroup`
51
+ (`form.address.city`), массив объектов — `FormArray` (`form.items.at(0)`), примитив, массив
52
+ примитивов, `Date`, `File` — `FormControl`. Вычисляемые поля (цели `compute`) удобно держать
53
+ отдельным типом и объявлять форму пересечением: `type MyFormFull = MyForm & MyFormDerived` — тогда
54
+ на сервер уходит только ввод (`MyForm`), а вычисляемые ключи отрезаются перед отправкой.
@@ -1,143 +1,191 @@
1
1
  ## 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)
2
2
 
3
- Под M1 схема **привязывает поле к сигналу модели** (`value: model.$.field`) и держит
4
- UI-конфиг (`component`/`componentProps`). Значения принадлежат модели. Валидаторов в
5
- layout-схеме НЕТ — правила живут в отдельной схеме `defineValidationSchema` из
6
- `@reformer/core/validation` и запускаются раннером `validateModel(model, schema)`.
3
+ Схема — **дерево узлов**. Узел привязан к части модели ключом `model` и держит UI-конфиг
4
+ (`component` / `componentProps`). Значения принадлежат модели. Правил валидации в схеме НЕТ — они
5
+ живут в отдельной схеме `defineValidation` из `@reformer/core/validation`.
7
6
 
8
- ### Field node
7
+ ### Четыре вида узла
8
+
9
+ Тип узла — `FormSchemaNode`, закрытый union. Вид определяют ключи `model`, `item`, `part` и
10
+ `children`:
11
+
12
+ | Вид | Ключи | Пример |
13
+ | --------------- | --------------------------------- | --------------------------------------------------- |
14
+ | поле | `model` — лист или массив целиком | `{ model: model.$.email, component: Input }` |
15
+ | массив под-форм | `model` — массив + `item` | `{ model: model.$.phones, item: phoneRow }` |
16
+ | подформа | `model` — группа + `part` | `{ model: model.$.address, part: address }` |
17
+ | контейнер | `children` | `{ component: Section, children: [/* узлы */] }` |
18
+
19
+ Общие ключи любого узла: `component`, `componentProps`, `selector`. У поля ещё `disabled`, у
20
+ массива под-форм — `initialValue` (запасной шаблон новой строки).
9
21
 
10
22
  ```typescript
23
+ // Поле
11
24
  {
12
- value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
25
+ model: model.$.fieldName, // ручка значения из model.$ — обязательна
13
26
  component: Input, // React-компонент
14
27
  componentProps?: object, // пропсы (label, placeholder, options, type, ...)
15
- disabled?: boolean,
16
- updateOn?: 'change' | 'blur' | 'submit',
17
- debounce?: number,
28
+ disabled?: boolean, // постоянная блокировка поля (вычисляемые total, fullName)
29
+ selector?: string, // имя узла для поведения: schema.node('…')
18
30
  }
19
31
  ```
20
32
 
21
- > `disabled` — **top-level поле узла** (начальное состояние); дальше управляется методами
22
- > `form.field.disable()`/`enable()` и оператором `enableWhen`. `componentProps.disabled` — это
23
- > UI-проп и на состояние узла **не влияет** (частая причина «computed-поле остаётся редактируемым»).
33
+ > **Вложенные узлы читаются только из `children` и из поддерева `part`.** В `componentProps` и под
34
+ > произвольными ключами сборка не заглядывает: узел, положенный туда, полем не станет. В dev
35
+ > сборка предупреждает о ключах узла, которые она не читает.
36
+
37
+ > **Тип закрыт — ошибки ловит компилятор:** опечатка в ключе (`componnet`), группа без `part`,
38
+ > `item` не на массиве, привязка value-фасадом (`model: model.email` вместо `model.$.email`) и
39
+ > узлы под произвольными ключами (`{ email: { model, component } }`) не компилируются.
40
+
41
+ > `disabled` — **ключ узла**, постоянная блокировка: `enable()` группы, массива или формы её не
42
+ > снимает (цикл «выключить форму на время отправки → включить» вычисляемые поля не разблокирует).
43
+ > Снимают её явный `form.field.enable()` и оператор `enableWhen` на этом поле. `componentProps.disabled` — это
44
+ > UI-проп и на состояние поля **не влияет** (частая причина «computed-поле остаётся редактируемым»).
24
45
 
25
46
  ### Primitive Fields
26
47
 
27
48
  ```typescript
28
- import { createModel, createForm } from '@reformer/core';
29
- import { defineValidationSchema, validate } from '@reformer/core/validation';
49
+ import { createModel, createFormFromModel, type FormSchemaNode } from '@reformer/core';
50
+ import { defineValidation, validate } from '@reformer/core/validation';
30
51
  import { required } from '@reformer/core/validators';
31
- import { Input, Select, Checkbox } from '@reformer/ui-kit';
52
+ import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
32
53
 
33
54
  const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
34
55
 
35
56
  // Валидация — отдельным слоем (запуск: await validateModel(model, myValidation))
36
- const myValidation = defineValidationSchema<MyForm>(({ model }) => {
57
+ const myValidation = defineValidation<MyForm>(({ model }) => {
37
58
  validate(model.$.name, [required()]);
38
59
  });
39
60
 
40
- const schema = {
41
- name: {
42
- value: model.$.name,
43
- component: Input,
44
- componentProps: { label: 'Name', placeholder: 'Enter name' },
45
- },
46
- age: {
47
- value: model.$.age,
48
- component: Input,
49
- componentProps: { type: 'number', label: 'Age' },
50
- },
51
- agree: {
52
- value: model.$.agree,
53
- component: Checkbox,
54
- componentProps: { label: 'I agree to terms' },
55
- },
56
- status: {
57
- value: model.$.status,
58
- component: Select,
59
- componentProps: {
60
- label: 'Status',
61
- options: [
62
- { value: 'active', label: 'Active' },
63
- { value: 'inactive', label: 'Inactive' },
64
- ],
61
+ const schema: FormSchemaNode = {
62
+ children: [
63
+ {
64
+ model: model.$.name,
65
+ component: Input,
66
+ componentProps: { label: 'Name', placeholder: 'Enter name' },
67
+ },
68
+ {
69
+ model: model.$.age,
70
+ component: InputNumber,
71
+ componentProps: { label: 'Age' },
72
+ },
73
+ {
74
+ model: model.$.agree,
75
+ component: CheckboxWithLabel,
76
+ componentProps: { label: 'I agree to terms' },
65
77
  },
66
- },
78
+ {
79
+ model: model.$.status,
80
+ component: SelectAsync,
81
+ componentProps: {
82
+ label: 'Status',
83
+ options: [
84
+ { value: 'active', label: 'Active' },
85
+ { value: 'inactive', label: 'Inactive' },
86
+ ],
87
+ },
88
+ },
89
+ ],
67
90
  };
68
91
 
69
- const form = createForm<MyForm>({ model, schema });
92
+ const form = createFormFromModel<MyForm>({ model, schema });
70
93
  ```
71
94
 
72
- ### Nested Objects
95
+ Поле данных может называться как угодно — `value`, `schema`, `form`, `model`, `children`: вид ноды
96
+ определяет узел модели, а не имя.
73
97
 
74
- Вложенная группа модели — это сама по себе под-модель `FormModel<Sub>` (доступна как `model.<group>`,
75
- с `.$`/API). Удобно вынести в builder, принимающий `FormModel<Sub>` — симметрично builder'у элемента
76
- массива. Сигналы под-модели идентичны корневым: `model.address.$.city === model.$.address.city`.
98
+ ### Nested Objects — подформа `{ model, part }`
77
99
 
78
- ```typescript
79
- import type { FormModel } from '@reformer/core';
100
+ Вложенная группа модели — под-модель `FormModel<Sub>`. Её разметку объявляют один раз функцией от
101
+ под-модели и подключают к группе узлом-подформой. Привязки внутри части идут через `$` полученной
102
+ под-модели:
80
103
 
81
- const addressNodes = (m: FormModel<Address>) => ({
82
- street: { value: m.$.street, component: Input, componentProps: { label: 'Street' } },
83
- city: { value: m.$.city, component: Input, componentProps: { label: 'City' } },
84
- zip: { value: m.$.zip, component: Input, componentProps: { label: 'ZIP' } },
104
+ ```typescript
105
+ import type { FormModel, FormSchemaNode } from '@reformer/core';
106
+
107
+ const address = (model: FormModel<Address>): FormSchemaNode => ({
108
+ children: [
109
+ { model: model.$.street, component: Input, componentProps: { label: 'Street' } },
110
+ { model: model.$.city, component: Input, componentProps: { label: 'City' } },
111
+ { model: model.$.zip, component: Input, componentProps: { label: 'ZIP' } },
112
+ ],
85
113
  });
86
114
 
87
- const schema = {
88
- address: addressNodes(model.address),
115
+ const schema: FormSchemaNode = {
116
+ children: [
117
+ // Одна часть подключается к любой группе той же формы данных — сколько угодно раз.
118
+ { model: model.$.registrationAddress, part: address },
119
+ { model: model.$.residenceAddress, part: address },
120
+ ],
89
121
  };
90
122
  ```
91
123
 
92
- ### Arrays — `{ array, item }` node
124
+ Поля группы можно привязать и напрямую, без подформы:
125
+ `{ model: model.$.address.city, component: Input }`.
93
126
 
94
- Массив объектов объявляется узлом `{ array: model.<path>, item: (itemModel) => subSchema }`.
95
- `item` строит под-схему из под-модели элемента (`FormModel<Item>`):
127
+ ### Arrays — массив под-форм `{ model, item }`
128
+
129
+ `item` строит разметку строки из под-модели строки (`FormModel<Item>`):
96
130
 
97
131
  ```typescript
98
- import type { FormModel } from '@reformer/core';
132
+ import type { FormModel, FormSchemaNode } from '@reformer/core';
99
133
 
100
- const itemSchema = (item: FormModel<Item>) => ({
101
- id: { value: item.$.id, component: Input, componentProps: { label: 'ID' } },
102
- name: { value: item.$.name, component: Input, componentProps: { label: 'Name' } },
134
+ const itemRow = (item: FormModel<Item>): FormSchemaNode => ({
135
+ children: [
136
+ { model: item.$.id, component: Input, componentProps: { label: 'ID' } },
137
+ { model: item.$.name, component: Input, componentProps: { label: 'Name' } },
138
+ ],
103
139
  });
104
140
 
105
- const schema = {
106
- items: { array: model.items, item: itemSchema },
141
+ const schema: FormSchemaNode = {
142
+ children: [{ model: model.$.items, component: FieldArray, item: itemRow }],
107
143
  };
108
144
  ```
109
145
 
146
+ Массив **без** `item` и с `component` — одно значение поля (мультивыбор, теги, список файлов):
147
+ `{ model: model.$.tags, component: SelectMulti }`.
148
+
110
149
  ### createForm API
111
150
 
112
151
  ```typescript
113
- // M1: данные из модели + схема (+ опциональный декларативный behavior)
114
- const form = createForm<MyForm>({
115
- model, // FormModel<MyForm> — обязателен
116
- schema, // дерево узлов, привязанных к сигналам
117
- behavior: myBehavior, // опционально: defineFormBehavior(...) из @reformer/core/behaviors
152
+ // Сборка одним вызовом: модель, форма, уровень формы и дерево для рендера
153
+ const bundle = createForm<MyForm>({
154
+ initial, // либо готовая модель: model
155
+ schema: (model) => tree, // билдер дерева: узлы держат ручки модели
156
+ behavior: myBehavior, // опционально: defineFormBehavior(...) из @reformer/core/behaviors
157
+ validation: myRules, // опционально: схема либо { rules?, extras?, strategy? }
158
+ flow: { steps: [{ id: 'loan', rules: loanRules }] }, // опционально: шаги, id = selector шага
118
159
  });
160
+ bundle.render.tree; // CompiledSchema — то же дерево после компиляции
161
+
162
+ // Низкоуровневая фабрика: готовые модель и дерево
163
+ const form = createFormFromModel<MyForm>({ model, schema, behavior: myBehavior });
119
164
 
120
- // Доступ к нодам через Proxy
165
+ // Доступ к нодам
121
166
  form.name.setValue('John');
122
- form.address.city.value.value; // текущее значение (через сигнал)
123
- model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели
167
+ form.address.city.value.value; // текущее значение (через сигнал)
168
+ model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели (или form.items.push)
124
169
  ```
125
170
 
126
- > **Тип `schema` в M1 — `unknown`, не `FormSchema<T>`.** Это осознанно: интерпретатор обходит
127
- > произвольную структуру и собирает листья `{ value: signal, component?, ... }`, поэтому обёртки
128
- > (`{ children: [...] }`, wizard/steps) компилируются. Строгий `FormSchema<T>` (keyed-map по полям
129
- > `T`) применяется только на legacy-пути `createForm(schema)` без модели.
171
+ Сборка компилирует дерево один раз (`compileSchema`): у каждого узла — вид (`field | array | part |
172
+ container | text`), идентификатор (`selector` либо позиционный путь `#/0/2`) и область (`#`,
173
+ `#/address`, `#/phones[<ключ>]`). Повтор `selector` в одной области (корень, строка массива,
174
+ подформа) — исключение с путями обоих узлов. В dev правило поведения на узел, которого нет в
175
+ дереве, и шаг `flow.steps`, для которого нет узла с таким `selector`, дают предупреждение в
176
+ консоли. Подробно — [37-compiled-schema.md](./37-compiled-schema.md).
130
177
 
131
- ### createForm Returns a Proxy
178
+ ### Ноды формы — типы и гарды
132
179
 
133
180
  ```typescript
134
- const form = createForm<MyForm>({ model, schema });
181
+ const form = createFormFromModel<MyForm>({ model, schema });
135
182
 
136
- form.email; // FieldNode<string> — TypeScript знает тип
137
- form.address.city; // FieldNode<string> — вложенный доступ
138
- form.items.at(0); // FormProxy<ItemType> — элемент массива
183
+ form.email; // FormControl<string> — TypeScript знает тип
184
+ form.address.city; // FormControl<string> — вложенный доступ (form.address — FormGroup)
185
+ form.items.at(0); // FormGroup<ItemType> | undefined — форма строки массива
186
+ form.$.value; // поле с именем члена ноды (value, issues, reset, …) — через `$`
139
187
 
140
- // IMPORTANT: Proxy не проходит instanceof! Используй type guards:
141
- import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
142
- if (isFieldNode(node)) { /* ... */ }
188
+ // FormControl / FormGroup / FormArray — интерфейсы, instanceof не работает. Вид ноды — гардами:
189
+ import { isFormControl, isFormGroup, isFormArray } from '@reformer/core';
190
+ if (isFormControl(node)) { /* ... */ }
143
191
  ```
@@ -2,75 +2,143 @@
2
2
 
3
3
  Массивы объектов — **model-owned**: данные принадлежат модели (`model.arrayField` — это
4
4
  `ModelArray<Item>` с реактивными `push`/`removeAt`/`length`). В схеме массив объявляется узлом
5
- `{ array: model.<path>, item: (itemModel) => subSchema }`, где `item` строит под-схему одного
6
- элемента из его под-модели (`FormModel<Item>`).
5
+ `{ model: model.$.<path>, item: (itemModel) => узел }`, где `item` строит разметку одной строки
6
+ из её под-модели (`FormModel<Item>`).
7
7
 
8
8
  ```typescript
9
- import { createModel, createForm, type FormModel } from '@reformer/core';
10
- import { Input } from '@reformer/ui-kit';
9
+ import {
10
+ arrayOf,
11
+ createModel,
12
+ createFormFromModel,
13
+ type FormModel,
14
+ type FormSchemaNode,
15
+ } from '@reformer/core';
16
+ import { FieldArray, Input, InputNumber } from '@reformer/ui-kit';
11
17
 
12
18
  type Item = { id: string; name: string; price: number };
13
19
  type MyForm = { items: Item[] };
14
20
 
15
- const model = createModel<MyForm>({ items: [] });
16
-
17
- // под-схема одного элемента (item.$.field — сигнал под-модели)
18
- const itemSchema = (item: FormModel<Item>) => ({
19
- id: { value: item.$.id, component: Input },
20
- name: { value: item.$.name, component: Input },
21
- price: { value: item.$.price, component: Input, componentProps: { type: 'number' } },
21
+ // arrayOf(blank) — пустой список и шаблон строки для кнопки «Добавить»
22
+ const blankItem = (): Item => ({ id: '', name: '', price: 0 });
23
+ const model = createModel<MyForm>({ items: arrayOf(blankItem) });
24
+
25
+ // разметка одной строки (item.$.field — ручка под-модели строки)
26
+ const itemRow = (item: FormModel<Item>): FormSchemaNode => ({
27
+ children: [
28
+ { model: item.$.id, component: Input },
29
+ { model: item.$.name, component: Input },
30
+ { model: item.$.price, component: InputNumber },
31
+ ],
22
32
  });
23
33
 
24
- const schema = {
25
- items: { array: model.items, item: itemSchema },
34
+ const schema: FormSchemaNode = {
35
+ children: [{ model: model.$.items, component: FieldArray, item: itemRow }],
36
+ };
37
+
38
+ const form = createFormFromModel<MyForm>({ model, schema });
39
+ form.items; // FormArray<Item> — форма каждой строки: form.items.at(i)
40
+ ```
41
+
42
+ > Привязка массива — **ручка** `model.$.items`, как у поля и подформы. `item` на привязке, которая
43
+ > массивом не является (лист, группа), — ошибка компиляции.
44
+
45
+ ### Массив как ОДНО значение поля
46
+
47
+ Массив бывает не набором под-форм, а одним значением: мультивыбор, теги, список файлов. Чем он
48
+ является, решает **схема**, а не данные: `{ model, item }` — набор под-форм, а узел поля
49
+ `{ model: model.$.<путь>, component }` без `item` — одно значение. Массив, к которому в схеме
50
+ ничего не привязано, в форму не попадает.
51
+
52
+ ```typescript
53
+ type MyForm = { tags: string[] };
54
+
55
+ const model = createModel<MyForm>({ tags: [] });
56
+
57
+ const schema: FormSchemaNode = {
58
+ children: [{ model: model.$.tags, component: SelectMulti }], // поле над массивом целиком
26
59
  };
60
+ const form = createFormFromModel<MyForm>({ model, schema });
61
+
62
+ form.tags.setValue(['a', 'b']); // FormControl<string[]>
63
+ model.$.tags.value = ['c']; // ручка значения: заменяет массив целиком
64
+ validate(model.$.tags, [required(), maxLength(3)]); // правило получает массив
65
+ ```
66
+
67
+ - `model.$.<массив>` — записываемая ручка значения (`PathAwareSignal`): подходит для `model:` в
68
+ схеме, `validate`/`cross`, `enableWhen`/`copyFrom` и для `model.signalAt(path)`.
69
+ - Узел-массив хранит только массив: запись `null`/`undefined` даёт `[]`. Нужно отличать «не
70
+ выбирали» от «выбрали ничего» — объявляй поле `T[] | null` с начальным `null`: в рантайме это
71
+ лист, и привязывается он так же.
72
+ - **Контейнер с `null` в типе — значение как есть.** `model.tags` у поля `string[] | null` — это
73
+ `string[] | null`, а не фасад `ModelArray`; `model.$.tags` — лист `PathAwareSignal` без `length`
74
+ и индексов; группа `Address | null` — `Address | null` без под-модели. Такое поле начинают с
75
+ `null`: контейнер, созданный из `[]` или `{}`, в рантайме стал бы фасадом, а тип его не видит.
76
+ - Запись в узел-ГРУППУ (`model.$.<группа>.value = …`) — ошибка: группа пишется по полям или через
77
+ `model.patch(...)`.
78
+
79
+ ### Массив в группе и в строке другого массива
80
+
81
+ Узел `{ model, item }` стоит на любой глубине: массив в группе получает ноду `form.<группа>.<массив>`,
82
+ массив в строке — `form.<массив>.at(i).<массив>`. Разметка строки объявляет свои массивы так же.
83
+
84
+ ```typescript
85
+ const contactRow = (contact: FormModel<Contact>): FormSchemaNode => ({
86
+ children: [
87
+ { model: contact.$.name, component: Input },
88
+ { model: contact.$.phones, item: phoneRow }, // массив в строке массива
89
+ ],
90
+ });
27
91
 
28
- const form = createForm<MyForm>({ model, schema });
92
+ const schema: FormSchemaNode = {
93
+ children: [
94
+ { model: model.$.details.hotlines, item: phoneRow }, // массив в группе
95
+ { model: model.$.contacts, item: contactRow },
96
+ ],
97
+ };
29
98
  ```
30
99
 
31
- > **Type constraint:** тип элемента `Item` объявляй через `type`-alias (не `interface`) — иначе
32
- > он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
33
- > См. `30-type-safety-recipes.md`.
100
+ Поведение строк вложенного массива — `applyEach` внутри схемы строки, см. `21-array-operations.md`.
34
101
 
35
102
  ### Один массив — три слоя (три разных движка)
36
103
 
37
104
  Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
38
105
  перепутать, но каждая корректна только в своём контексте:
39
106
 
40
- 1. **Layout-схема `createForm`** — единственная форма, которую ест `createForm`:
41
- `{ array: model.<path>, item: (itemModel) => subSchema }`. Массив связывается через
42
- **value-proxy** `model.properties` (он несёт `__path`), а **не** через сигнальный
43
- `model.$.properties`.
107
+ 1. **Схема формы** — узел `{ model: model.$.<path>, item: (itemModel) => узел }` среди `children`
108
+ дерева:
44
109
 
45
110
  ```typescript
46
- // узел схемы для createForm({ model, schema })
47
- properties: { array: model.properties, item: propertyItem },
111
+ // узел схемы для createForm / createFormFromModel
112
+ { model: model.$.properties, component: FieldArray, item: propertyRow },
48
113
  ```
49
114
 
50
- 2. **Validation-схема (`@reformer/core/validation`)** — per-item правила пишутся оператором
51
- `each(arr, (im) => {...})` внутри `defineValidationSchema`; `im` — под-модель элемента
52
- (`im.$.field` — его сигналы). Запуск — внешним `validateModel(model, schema)`.
115
+ 2. **Схема валидации (`@reformer/core/validation`)** — правила строки объявляются отдельной схемой
116
+ над типом строки и подключаются оператором `applyEach(model.$.<массив>, схема)`. У схемы строки
117
+ своя область: её `cross` получает снимок строки. Запуск — `validateModel(model, schema)`.
53
118
 
54
119
  ```typescript
55
- // внутри defineValidationSchema<MyForm>(({ model }) => { ... })
56
- each(model.properties, (im) => {
57
- validate(im.$.description, [required()]);
58
- validate(im.$.estimatedValue, [required(), min(1)]);
120
+ const propertyRules = defineValidation<Property>(({ model }) => {
121
+ validate(model.$.description, [required()]);
122
+ validate(model.$.estimatedValue, [required(), min(1)]);
59
123
  });
124
+
125
+ // внутри defineValidation<MyForm>(({ model }) => { ... })
126
+ applyEach(model.$.properties, propertyRules);
60
127
  ```
61
128
 
62
- 3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`ModelArrayNode`),
63
- а не со схемой: `<FormArray.Root control={form.properties}>` (CDK) или `FormArraySection` из
129
+ 3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`FormArray`),
130
+ а не со схемой: `<FieldArray.Root control={form.properties}>` (CDK) или `FieldArraySection` из
64
131
  `@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).
65
132
 
66
- > **Не путай форму по движку.** `createForm` принимает **только** `{ array, item }`;
67
- > per-item валидация — это `each(model.<array>, (im) => ...)` в validation-схеме; `FormArray.Root
68
- > control={form.x}` (или `FormArraySection`) — рендер. `each` в layout-схеме `createForm`
69
- > не подхватится, а `{ array, item }` в validation-схеме не обходится.
133
+ > **Не путай запись по движку.** Схема формы — узел `{ model, item }`; правила строк —
134
+ > `applyEach(model.$.<массив>, схема)` в схеме валидации; `FieldArray.Root control={form.x}` (или
135
+ > `FieldArraySection`) — рендер. `applyEach` в схеме формы не подхватится, а узел `{ model, item }`
136
+ > схема валидации не обходит.
70
137
 
71
- ### Array operations — на модели
138
+ ### Array operations — на модели или на ноде
72
139
 
73
- Мутации массива делаются через `ModelArray` (`model.items`), а не через ноду формы:
140
+ Мутации массива делаются через `ModelArray` (`model.items`) либо через ноду формы `FormArray`
141
+ (`form.items`) — нода данными не владеет и передаёт мутацию в тот же массив модели:
74
142
 
75
143
  ```typescript
76
144
  model.items.push({ id: '1', name: '', price: 0 }); // добавить в конец (плоские значения!)
@@ -82,8 +150,18 @@ model.items.clear();
82
150
  model.items.length; // реактивная длина
83
151
  model.items.at(0); // под-модель элемента (FormModel<Item>)
84
152
  model.items.map((item, i) => item.name); // item — FormModel<Item>
153
+
154
+ // та же мутация через ноду формы (у ноды вставка — insert, у модели — insertAt)
155
+ form.items.push(); // строка по шаблону arrayOf(blank)
156
+ form.items.insert(0, { id: '3', name: '', price: 0 });
157
+ form.items.removeAt(index);
158
+ form.items.length; // число строк (число, не сигнал)
159
+ form.items.keyAt(index); // стабильный ключ строки
85
160
  ```
86
161
 
162
+ Формы строк держатся за под-модели элементов и при `move` / `swap` не пересоздаются; ключ строки
163
+ (`keyAt`) не меняется при перестановке — его и берут для React `key`.
164
+
87
165
  > **Плоские значения при push.** В `push`/`insertAt` передавай payload из **плоских значений**
88
166
  > (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
89
167
  > берутся из `item`-фабрики схемы автоматически.
@@ -100,27 +178,27 @@ model.items.map((item, i) => item.name); // item — FormModel<Item>
100
178
 
101
179
  ### Rendering Arrays
102
180
 
103
- Каждый элемент массива — под-форма (`FormProxy<Item>`). Итерируй через `form.items.map`:
181
+ Каждый элемент массива — под-форма (`FormGroup<Item>`). Итерируй через `form.items.map`:
104
182
 
105
183
  ```tsx
106
- import { useArrayLength } from '@reformer/core';
184
+ import { useArrayLength } from '@reformer/core/react';
107
185
 
108
- function ItemsList({ form }: { form: FormProxy<MyForm> }) {
186
+ function ItemsList({ form }: { form: FormGroup<MyForm> }) {
109
187
  const length = useArrayLength(form.items);
110
188
 
111
189
  return (
112
190
  <div>
113
191
  {form.items.map((item, index) => (
114
- <div key={index}>
192
+ <div key={form.items.keyAt(index)}>
115
193
  <FormField control={item.name} />
116
194
  <FormField control={item.price} />
117
- <button onClick={() => model.items.removeAt(index)}>Remove</button>
195
+ <button onClick={() => form.items.removeAt(index)}>Remove</button>
118
196
  </div>
119
197
  ))}
120
198
 
121
199
  {length === 0 && <p>No items yet</p>}
122
200
 
123
- <button onClick={() => model.items.push({ id: crypto.randomUUID(), name: '', price: 0 })}>
201
+ <button onClick={() => form.items.push({ id: crypto.randomUUID(), name: '', price: 0 })}>
124
202
  Add Item
125
203
  </button>
126
204
  </div>
@@ -128,34 +206,36 @@ function ItemsList({ form }: { form: FormProxy<MyForm> }) {
128
206
  }
129
207
  ```
130
208
 
131
- > В монорепо для массивов используется готовый `FormArraySection` из `@reformer/ui-kit`
132
- > (`control={form.items}`, `itemComponent`, `initialValue`, add/remove/reorder из коробки).
133
- > См. `find_recipe(topic="form-array")`.
209
+ > В монорепо для массивов используется готовый `FieldArraySection` из `@reformer/ui-kit`
210
+ > (`control={form.items}`, `itemComponent`, add/remove/reorder из коробки; шаблон новой строки —
211
+ > `arrayOf(blank)` в модели, проп `initialValue` — запасной путь). В схеме массив под-форм рисует
212
+ > компонент `FieldArray` узла `{ model, component: FieldArray, item }`.
213
+ > См. `find_recipe(topic="field-array")`.
134
214
 
135
215
  ### Array Cross-Validation
136
216
 
137
- Whole-array правило пишется оператором `cross(sig, (f) => ...)`: `f` — снапшот `model.get()`
138
- (плоские значения), ошибка вешается на поле-носитель `sig`. Per-item правила — `each`
139
- (см. выше, оба — из `@reformer/core/validation`).
217
+ Правило над всем массивом пишется оператором `cross` из аргумента схемы: `check` получает снимок
218
+ модели области (`model.get()`, плоские значения), ошибка вешается на поле-носитель. Тип снимка
219
+ выведен из схемы. Правила строк — `applyEach` (см. выше).
140
220
 
141
221
  Носитель — любое **скалярное** поле формы (флаг `hasItems`, итоговая сумма и т.п.), у которого
142
- есть сигнал `model.$.<field>` и нода в форме:
222
+ есть ручка `model.$.<field>` и нода в форме:
143
223
 
144
224
  ```typescript
145
- import { cross } from '@reformer/core/validation';
146
-
147
225
  type MyForm = { hasItems: boolean; items: Item[] };
148
226
 
149
- // внутри defineValidationSchema<MyForm>(({ model }) => { ... })
150
- cross(model.$.hasItems, (f: MyForm) => {
151
- const names = f.items.map((i) => i.name);
152
- return names.length !== new Set(names).size
153
- ? { code: 'duplicate', message: 'Item names must be unique' }
154
- : null;
227
+ defineValidation<MyForm>(({ model, cross }) => {
228
+ cross(model.$.hasItems, (form) => {
229
+ const names = form.items.map((item) => item.name);
230
+ return names.length !== new Set(names).size
231
+ ? { code: 'items.duplicate' }
232
+ : null;
233
+ });
155
234
  });
156
235
  ```
157
236
 
158
237
  ## See also
159
238
 
160
- - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item валидация `each`
161
- - CDK / ui-kit form-array (`FormArray.Root`, `FormArraySection`) — `find_recipe(topic="form-array")`
239
+ - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, правила строк `applyEach`
240
+ - [37-compiled-schema.md](./37-compiled-schema.md) — область строки `#/items[<ключ>]` и `keyAt`
241
+ - CDK / ui-kit field-array (`FieldArray.Root`, `FieldArraySection`) — `find_recipe(topic="field-array")`
@@ -39,11 +39,11 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
39
39
 
40
40
  ### Низкоуровневый примитив watchField
41
41
 
42
- `watchField` из `@reformer/core` — базовая подписка на изменение сигнала (без debounce и
42
+ `watchField` из `@reformer/core/model` — базовая подписка на изменение сигнала (без debounce и
43
43
  AbortSignal). `onChange` построен поверх него. Для простых синхронных реакций:
44
44
 
45
45
  ```typescript
46
- import { watchField } from '@reformer/core';
46
+ import { watchField } from '@reformer/core/model';
47
47
 
48
48
  // вызывается при каждом изменении (по умолчанию НЕ на инициализации)
49
49
  const stop = watchField(model.$.country, (country) => {