@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,191 @@
1
+ ## 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)
2
+
3
+ Схема — **дерево узлов**. Узел привязан к части модели ключом `model` и держит UI-конфиг
4
+ (`component` / `componentProps`). Значения принадлежат модели. Правил валидации в схеме НЕТ — они
5
+ живут в отдельной схеме `defineValidation` из `@reformer/core/validation`.
6
+
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` (запасной шаблон новой строки).
21
+
22
+ ```typescript
23
+ // Поле
24
+ {
25
+ model: model.$.fieldName, // ручка значения из model.$ — обязательна
26
+ component: Input, // React-компонент
27
+ componentProps?: object, // пропсы (label, placeholder, options, type, ...)
28
+ disabled?: boolean, // постоянная блокировка поля (вычисляемые total, fullName)
29
+ selector?: string, // имя узла для поведения: schema.node('…')
30
+ }
31
+ ```
32
+
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-поле остаётся редактируемым»).
45
+
46
+ ### Primitive Fields
47
+
48
+ ```typescript
49
+ import { createModel, createFormFromModel, type FormSchemaNode } from '@reformer/core';
50
+ import { defineValidation, validate } from '@reformer/core/validation';
51
+ import { required } from '@reformer/core/validators';
52
+ import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
53
+
54
+ const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
55
+
56
+ // Валидация — отдельным слоем (запуск: await validateModel(model, myValidation))
57
+ const myValidation = defineValidation<MyForm>(({ model }) => {
58
+ validate(model.$.name, [required()]);
59
+ });
60
+
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' },
77
+ },
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
+ ],
90
+ };
91
+
92
+ const form = createFormFromModel<MyForm>({ model, schema });
93
+ ```
94
+
95
+ Поле данных может называться как угодно — `value`, `schema`, `form`, `model`, `children`: вид ноды
96
+ определяет узел модели, а не имя.
97
+
98
+ ### Nested Objects — подформа `{ model, part }`
99
+
100
+ Вложенная группа модели — под-модель `FormModel<Sub>`. Её разметку объявляют один раз функцией от
101
+ под-модели и подключают к группе узлом-подформой. Привязки внутри части идут через `$` полученной
102
+ под-модели:
103
+
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
+ ],
113
+ });
114
+
115
+ const schema: FormSchemaNode = {
116
+ children: [
117
+ // Одна часть подключается к любой группе той же формы данных — сколько угодно раз.
118
+ { model: model.$.registrationAddress, part: address },
119
+ { model: model.$.residenceAddress, part: address },
120
+ ],
121
+ };
122
+ ```
123
+
124
+ Поля группы можно привязать и напрямую, без подформы:
125
+ `{ model: model.$.address.city, component: Input }`.
126
+
127
+ ### Arrays — массив под-форм `{ model, item }`
128
+
129
+ `item` строит разметку строки из под-модели строки (`FormModel<Item>`):
130
+
131
+ ```typescript
132
+ import type { FormModel, FormSchemaNode } from '@reformer/core';
133
+
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
+ ],
139
+ });
140
+
141
+ const schema: FormSchemaNode = {
142
+ children: [{ model: model.$.items, component: FieldArray, item: itemRow }],
143
+ };
144
+ ```
145
+
146
+ Массив **без** `item` и с `component` — одно значение поля (мультивыбор, теги, список файлов):
147
+ `{ model: model.$.tags, component: SelectMulti }`.
148
+
149
+ ### createForm API
150
+
151
+ ```typescript
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 шага
159
+ });
160
+ bundle.render.tree; // CompiledSchema — то же дерево после компиляции
161
+
162
+ // Низкоуровневая фабрика: готовые модель и дерево
163
+ const form = createFormFromModel<MyForm>({ model, schema, behavior: myBehavior });
164
+
165
+ // Доступ к нодам
166
+ form.name.setValue('John');
167
+ form.address.city.value.value; // текущее значение (через сигнал)
168
+ model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели (или form.items.push)
169
+ ```
170
+
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).
177
+
178
+ ### Ноды формы — типы и гарды
179
+
180
+ ```typescript
181
+ const form = createFormFromModel<MyForm>({ model, schema });
182
+
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, …) — через `$`
187
+
188
+ // FormControl / FormGroup / FormArray — интерфейсы, instanceof не работает. Вид ноды — гардами:
189
+ import { isFormControl, isFormGroup, isFormArray } from '@reformer/core';
190
+ if (isFormControl(node)) { /* ... */ }
191
+ ```
@@ -0,0 +1,241 @@
1
+ ## 9. ARRAY SCHEMA FORMAT
2
+
3
+ Массивы объектов — **model-owned**: данные принадлежат модели (`model.arrayField` — это
4
+ `ModelArray<Item>` с реактивными `push`/`removeAt`/`length`). В схеме массив объявляется узлом
5
+ `{ model: model.$.<path>, item: (itemModel) => узел }`, где `item` строит разметку одной строки
6
+ из её под-модели (`FormModel<Item>`).
7
+
8
+ ```typescript
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';
17
+
18
+ type Item = { id: string; name: string; price: number };
19
+ type MyForm = { items: Item[] };
20
+
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
+ ],
32
+ });
33
+
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 }], // поле над массивом целиком
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
+ });
91
+
92
+ const schema: FormSchemaNode = {
93
+ children: [
94
+ { model: model.$.details.hotlines, item: phoneRow }, // массив в группе
95
+ { model: model.$.contacts, item: contactRow },
96
+ ],
97
+ };
98
+ ```
99
+
100
+ Поведение строк вложенного массива — `applyEach` внутри схемы строки, см. `21-array-operations.md`.
101
+
102
+ ### Один массив — три слоя (три разных движка)
103
+
104
+ Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
105
+ перепутать, но каждая корректна только в своём контексте:
106
+
107
+ 1. **Схема формы** — узел `{ model: model.$.<path>, item: (itemModel) => узел }` среди `children`
108
+ дерева:
109
+
110
+ ```typescript
111
+ // узел схемы для createForm / createFormFromModel
112
+ { model: model.$.properties, component: FieldArray, item: propertyRow },
113
+ ```
114
+
115
+ 2. **Схема валидации (`@reformer/core/validation`)** — правила строки объявляются отдельной схемой
116
+ над типом строки и подключаются оператором `applyEach(model.$.<массив>, схема)`. У схемы строки
117
+ своя область: её `cross` получает снимок строки. Запуск — `validateModel(model, schema)`.
118
+
119
+ ```typescript
120
+ const propertyRules = defineValidation<Property>(({ model }) => {
121
+ validate(model.$.description, [required()]);
122
+ validate(model.$.estimatedValue, [required(), min(1)]);
123
+ });
124
+
125
+ // внутри defineValidation<MyForm>(({ model }) => { ... })
126
+ applyEach(model.$.properties, propertyRules);
127
+ ```
128
+
129
+ 3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`FormArray`),
130
+ а не со схемой: `<FieldArray.Root control={form.properties}>` (CDK) или `FieldArraySection` из
131
+ `@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).
132
+
133
+ > **Не путай запись по движку.** Схема формы — узел `{ model, item }`; правила строк —
134
+ > `applyEach(model.$.<массив>, схема)` в схеме валидации; `FieldArray.Root control={form.x}` (или
135
+ > `FieldArraySection`) — рендер. `applyEach` в схеме формы не подхватится, а узел `{ model, item }`
136
+ > схема валидации не обходит.
137
+
138
+ ### Array operations — на модели или на ноде
139
+
140
+ Мутации массива делаются через `ModelArray` (`model.items`) либо через ноду формы `FormArray`
141
+ (`form.items`) — нода данными не владеет и передаёт мутацию в тот же массив модели:
142
+
143
+ ```typescript
144
+ model.items.push({ id: '1', name: '', price: 0 }); // добавить в конец (плоские значения!)
145
+ model.items.insertAt(0, { id: '2', name: '', price: 0 });
146
+ model.items.removeAt(index);
147
+ model.items.move(from, to);
148
+ model.items.swap(a, b);
149
+ model.items.clear();
150
+ model.items.length; // реактивная длина
151
+ model.items.at(0); // под-модель элемента (FormModel<Item>)
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); // стабильный ключ строки
160
+ ```
161
+
162
+ Формы строк держатся за под-модели элементов и при `move` / `swap` не пересоздаются; ключ строки
163
+ (`keyAt`) не меняется при перестановке — его и берут для React `key`.
164
+
165
+ > **Плоские значения при push.** В `push`/`insertAt` передавай payload из **плоских значений**
166
+ > (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
167
+ > берутся из `item`-фабрики схемы автоматически.
168
+
169
+ > **Очистка массива в behavior.** Тот же `model.<array>.clear()` (см. список операций выше) —
170
+ > способ очистить коллекцию **вне React**, из behavior: он мутирует модель напрямую, минуя ноды
171
+ > формы. Типичный случай — сбросить массив при выключении флага:
172
+ >
173
+ > ```typescript
174
+ > onChange(model.$.hasProperty, (on) => {
175
+ > if (!on) model.properties.clear();
176
+ > });
177
+ > ```
178
+
179
+ ### Rendering Arrays
180
+
181
+ Каждый элемент массива — под-форма (`FormGroup<Item>`). Итерируй через `form.items.map`:
182
+
183
+ ```tsx
184
+ import { useArrayLength } from '@reformer/core/react';
185
+
186
+ function ItemsList({ form }: { form: FormGroup<MyForm> }) {
187
+ const length = useArrayLength(form.items);
188
+
189
+ return (
190
+ <div>
191
+ {form.items.map((item, index) => (
192
+ <div key={form.items.keyAt(index)}>
193
+ <FormField control={item.name} />
194
+ <FormField control={item.price} />
195
+ <button onClick={() => form.items.removeAt(index)}>Remove</button>
196
+ </div>
197
+ ))}
198
+
199
+ {length === 0 && <p>No items yet</p>}
200
+
201
+ <button onClick={() => form.items.push({ id: crypto.randomUUID(), name: '', price: 0 })}>
202
+ Add Item
203
+ </button>
204
+ </div>
205
+ );
206
+ }
207
+ ```
208
+
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")`.
214
+
215
+ ### Array Cross-Validation
216
+
217
+ Правило над всем массивом пишется оператором `cross` из аргумента схемы: `check` получает снимок
218
+ модели области (`model.get()`, плоские значения), ошибка вешается на поле-носитель. Тип снимка
219
+ выведен из схемы. Правила строк — `applyEach` (см. выше).
220
+
221
+ Носитель — любое **скалярное** поле формы (флаг `hasItems`, итоговая сумма и т.п.), у которого
222
+ есть ручка `model.$.<field>` и нода в форме:
223
+
224
+ ```typescript
225
+ type MyForm = { hasItems: boolean; items: Item[] };
226
+
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
+ });
234
+ });
235
+ ```
236
+
237
+ ## See also
238
+
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")`
@@ -0,0 +1,53 @@
1
+ ## 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)
2
+
3
+ Для async-реакции на изменение поля (загрузка зависимых опций, справочников) используй
4
+ `onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно
5
+ безопасно писать сигналы/ноды), а 2-й аргумент — `{ signal }` (AbortSignal): при следующей
6
+ смене значения предыдущий вызов аннулируется — передавай `signal` в `fetch`.
7
+
8
+ ```typescript
9
+ import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
10
+
11
+ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
12
+ // CORRECT — async onChange со всеми safeguards
13
+ onChange(
14
+ model.$.parentField,
15
+ async (value, { signal }) => {
16
+ if (!value) {
17
+ form.dependentField.updateComponentProps({ options: [] });
18
+ return;
19
+ }
20
+ try {
21
+ const data = await fetchData(value, { signal }); // отмена устаревших запросов
22
+ form.dependentField.updateComponentProps({ options: data });
23
+ } catch (error) {
24
+ if ((error as Error).name === 'AbortError') return;
25
+ form.dependentField.updateComponentProps({ options: [] });
26
+ }
27
+ },
28
+ { debounce: 300 } // не fetch на каждое нажатие
29
+ );
30
+ });
31
+ ```
32
+
33
+ ### Опции onChange
34
+
35
+ - `debounce: 300` — не дёргать сеть на каждый keystroke (300–500 мс рекомендуется).
36
+ - `immediate: true` — вызвать колбэк сразу при регистрации (по умолчанию `false`).
37
+ - Guard-clause — пропустить пустое значение.
38
+ - try/catch + проверка `AbortError` — обработка отмены и ошибок.
39
+
40
+ ### Низкоуровневый примитив watchField
41
+
42
+ `watchField` из `@reformer/core/model` — базовая подписка на изменение сигнала (без debounce и
43
+ AbortSignal). `onChange` построен поверх него. Для простых синхронных реакций:
44
+
45
+ ```typescript
46
+ import { watchField } from '@reformer/core/model';
47
+
48
+ // вызывается при каждом изменении (по умолчанию НЕ на инициализации)
49
+ const stop = watchField(model.$.country, (country) => {
50
+ model.city = ''; // сброс зависимого поля
51
+ });
52
+ // stop() — отписаться
53
+ ```
@@ -0,0 +1,31 @@
1
+ ## 11. ARRAY CLEANUP PATTERN
2
+
3
+ Очистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на
4
+ модели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.
5
+
6
+ ```typescript
7
+ import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
8
+
9
+ const behavior = defineFormBehavior<MyForm>(({ model }) => {
10
+ // при снятии флага — очистить массив
11
+ onChange(model.$.hasItems, (hasItems) => {
12
+ if (!hasItems) model.items.clear();
13
+ });
14
+ });
15
+ ```
16
+
17
+ Переиспользуемый оператор (как в монорепо):
18
+
19
+ ```typescript
20
+ import { onChange } from '@reformer/core/behaviors';
21
+ import type { ReadonlySignal } from '@reformer/core/behaviors';
22
+
23
+ function clearWhenOff(flag: ReadonlySignal<boolean>, array: { clear(): void }): void {
24
+ onChange(flag, (on) => {
25
+ if (!on) array.clear();
26
+ });
27
+ }
28
+
29
+ // в схеме поведения:
30
+ clearWhenOff(model.$.hasProperty, model.properties);
31
+ ```