@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,162 @@
1
+ ## 12. MULTI-STEP FORM VALIDATION
2
+
3
+ Многошаговая форма — **поток шагов** сборки: `createForm({ flow: { steps: [{ id, rules }] } })`.
4
+ Каждый шаг — идентификатор и своя `ValidationDefinition<Form>` (обычная функция
5
+ `({ model, cross }) => void`, обёрнутая `defineValidation`). Идентификатор шага — `selector` узла
6
+ шага в схеме. Правила вне шагов (межшаговые) — `validation: { extras }`: они проверяются только
7
+ целиком, при отправке. Валидация живёт ОТДЕЛЬНО от схемы формы: узел схемы (TS или JSON) правил не
8
+ несёт.
9
+
10
+ Сборка отдаёт поток `bundle.flow` (`next` / `previous` / `goTo`), проверку шага
11
+ `bundle.validateStep(id)`, полную проверку `bundle.validate()` и отправку
12
+ `bundle.submission.submit(handler)`. Замечания разносятся по нодам сами — UI подсветит поля
13
+ текущего шага. Полный справочник — [38-form-flow.md](./38-form-flow.md).
14
+
15
+ ### Поток шагов сборки
16
+
17
+ ```typescript
18
+ import { createForm } from '@reformer/core';
19
+ import { useFormBundle } from '@reformer/core/react';
20
+
21
+ const createLoanForm = () =>
22
+ createForm<Form>({
23
+ initial,
24
+ schema: formSchema, // узлы шагов несут selector: 'loan', 'applicant', 'confirm'
25
+ validation: { extras: crossStepRules }, // правила всей формы — только целиком
26
+ flow: {
27
+ steps: [
28
+ { id: 'loan', rules: loanRules },
29
+ { id: 'applicant', rules: applicantRules },
30
+ { id: 'confirm' }, // шаг без правил
31
+ ],
32
+ },
33
+ });
34
+
35
+ // в компоненте: фабрика вызывается один раз, сборка стабильна между рендерами
36
+ const loan = useFormBundle(createLoanForm);
37
+
38
+ // в обработчиках событий
39
+ await loan.flow?.next(); // проверить правила текущего шага и перейти → FlowTransition
40
+ await loan.validateStep('loan'); // правила одного шага → ValidationResult
41
+ await loan.validate(); // все шаги + extras → ValidationResult
42
+ await loan.submission.submit(sendLoan); // один прогон всех правил; обработчик — только при valid
43
+ loan.validating.value; // идёт ли проверка — полная либо шага
44
+ ```
45
+
46
+ - Полный набор правил — правила всех шагов и `extras`; свой полный набор задаёт
47
+ `validation: { rules }` (тогда правила шагов работают только для `validateStep` / `next`).
48
+ - `next()` на шаге с замечаниями остаётся на месте: `{ status: 'stayed', reason: 'invalid',
49
+ validation }`; поля шага помечаются тронутыми. Переход — `{ status: 'moved', from, to }`.
50
+ - Поле правили, пока шла проверка шага (async-правило), — `next()` остаётся на шаге с
51
+ `reason: 'cancelled'`: правила этого значения не видели. Следующий `next()` проверит новое.
52
+ - Стратегия `afterFirstSubmit` переходит в живую фазу только от отправки
53
+ (`bundle.submission.submit`): `next()`, `validateStep` и `validate` её не переключают.
54
+ - `goTo(id)` открывает первый шаг и шаги после пройденных; прыжок вперёд через непройденный шаг —
55
+ `reason: 'locked'`. Назад — всегда.
56
+ - В dev сборка предупреждает о шаге `flow.steps`, для которого в схеме нет узла с таким `selector`.
57
+ - Правила (`loanRules`, `extras`) — стабильные ссылки: константы модуля через `defineValidation`.
58
+
59
+ В большой форме шаг объявляют один раз — в списке шагов (`flow/`): из него строятся и узлы шагов
60
+ схемы, и `flow.steps` сборки. См. `15-project-structure.md`.
61
+
62
+ ### Визард — адаптер потока
63
+
64
+ `FormWizard` из `@reformer/cdk` и `@reformer/ui-kit` рисует поток и зовёт его переходы:
65
+
66
+ ```tsx
67
+ import { FormWizard } from '@reformer/ui-kit';
68
+
69
+ function LoanWizard() {
70
+ const loan = useFormBundle(createLoanForm);
71
+ const send = async () => {
72
+ const outcome = await loan.submission.submit(sendLoan);
73
+ if (outcome.status !== 'submitted') showHint('Проверьте форму');
74
+ };
75
+ return <FormWizard form={loan.form} flow={loan.flow} steps={STEPS} onSubmit={send} />;
76
+ }
77
+ ```
78
+
79
+ - В JSX — `form`, `flow={bundle.flow}` и `steps` пропсами; отправку проверяет приложение через
80
+ `bundle.submission.submit(handler)`.
81
+ - В схеме формы — узел `{ selector: 'wizard', component: FormWizard, children: [шаги] }`, шаг —
82
+ `{ selector: 'loan', component: Step, componentProps: { title }, children: [...] }`. Внутри
83
+ рендерера визард берёт форму и поток из сборки сам, а его `onSubmit` зовётся через
84
+ `bundle.submission.submit` — только после успешной проверки всей формы.
85
+ - Без `flow` и без сборки в контексте визард строит позиционный поток **без правил**.
86
+ - Навигация снаружи визарда — через сам поток: `bundle.flow?.goTo('loan')`; в поведении — `flow` из
87
+ области (`defineFormBehavior(({ model, flow }) => …)`).
88
+
89
+ ### Шаги без визарда — `useFormFlow`
90
+
91
+ ```tsx
92
+ import { useFormFlow } from '@reformer/core/react';
93
+
94
+ function MultiStepForm() {
95
+ const loan = useFormBundle(createLoanForm);
96
+ const flow = loan.flow!;
97
+ const { current, isFirst, isLast, validating } = useFormFlow(flow);
98
+
99
+ return (
100
+ <div>
101
+ {current === 'loan' && <LoanFields form={loan.form} />}
102
+ {current === 'applicant' && <ApplicantFields form={loan.form} />}
103
+ {current === 'confirm' && <Summary form={loan.form} />}
104
+
105
+ <button onClick={() => flow.previous()} disabled={isFirst}>
106
+ Back
107
+ </button>
108
+ {isLast ? (
109
+ <button onClick={() => void loan.submission.submit(sendLoan)}>Submit</button>
110
+ ) : (
111
+ <button onClick={() => void flow.next()} disabled={validating}>
112
+ Next
113
+ </button>
114
+ )}
115
+ </div>
116
+ );
117
+ }
118
+ ```
119
+
120
+ ### Без сборки — `validateModel` по схеме шага
121
+
122
+ Тот же результат без `createForm`: переход проверяется `validateModel(model, stepSchema)`, полный
123
+ submit — по общей схеме (композиция шагов через `apply(...)`).
124
+
125
+ ```typescript
126
+ import {
127
+ validate,
128
+ apply,
129
+ defineValidation,
130
+ validateModel,
131
+ type ValidationDefinition,
132
+ } from '@reformer/core/validation';
133
+ import { required, min } from '@reformer/core/validators';
134
+
135
+ const loanRules = defineValidation<Form>(({ model }) => {
136
+ validate(model.$.loanType, [required()]);
137
+ validate(model.$.loanAmount, [required(), min(50000)]);
138
+ });
139
+
140
+ const applicantRules = defineValidation<Form>(({ model }) => {
141
+ validate(model.$.personalData.firstName, [required()]);
142
+ validate(model.$.personalData.lastName, [required()]);
143
+ });
144
+
145
+ const STEP_RULES: readonly ValidationDefinition<Form>[] = [loanRules, applicantRules];
146
+ const fullRules = defineValidation<Form>(() => apply(...STEP_RULES));
147
+
148
+ // true = нет блокирующих замечаний (severity:'warning' не блокирует); замечания уже в нодах
149
+ const stepOk = await validateModel(model, STEP_RULES[index], { touch: true });
150
+ const allOk = await validateModel(model, fullRules, { touch: true });
151
+ ```
152
+
153
+ Поток без сборки собирает `createFormFlow({ steps, validateStep })` — см. `38-form-flow.md`.
154
+
155
+ ### Удалено
156
+
157
+ | Прежний контракт | Сейчас |
158
+ | -------------------------------------------------------- | --------------------------------------------------------- |
159
+ | `validation: { steps: { loan: rules } }` | `flow: { steps: [{ id: 'loan', rules }] }` |
160
+ | `bundle.validation.validateStep(n)` / `.validateAll()` / `.runStep()` / `.runAll()` | `bundle.validateStep(id)` / `bundle.validate()` |
161
+ | `<FormWizard config={…} ref={…}>`, `FormWizardConfig`, `FormWizardHandle` | `<FormWizard flow={bundle.flow}>`, навигация — `bundle.flow` |
162
+ | `defineSteps`, `useWizardStepValidation` | `flow` сборки; живая проверка — `validation.strategy` |
@@ -0,0 +1,101 @@
1
+ ## 13. EXTENDED COMMON MISTAKES
2
+
3
+ ### Reuse via apply / applyEach — не дублируй код
4
+
5
+ ```typescript
6
+ import { defineFormBehavior, apply, applyEach, compute } from '@reformer/core/behaviors';
7
+
8
+ // ✅ apply — одна под-схема на несколько групп
9
+ const addressBehavior = defineFormBehavior<Address>(({ model }) => {
10
+ compute(model.$.full, () => `${model.city}, ${model.street}`);
11
+ });
12
+
13
+ const behavior = defineFormBehavior<Form>(({ model }) => {
14
+ apply([model.$.homeAddress, model.$.workAddress], addressBehavior);
15
+
16
+ // ✅ applyEach — под-схема на КАЖДЫЙ элемент массива (реагирует на add/remove)
17
+ applyEach(
18
+ model.$.items,
19
+ defineFormBehavior<Item>(({ model: row }) => {
20
+ compute(row.$.lineTotal, () => row.qty * row.price);
21
+ })
22
+ );
23
+ });
24
+ ```
25
+
26
+ ### Идемпотентность transformValue
27
+
28
+ ```typescript
29
+ // ❌ неидемпотентный transformer — бесконечный цикл setValue → callback → setValue
30
+ transformValue(model.$.field, (v) => `prefix-${v}`); // f(f(x)) ≠ f(x)
31
+
32
+ // ✅ guard «уже преобразовано»
33
+ transformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));
34
+ ```
35
+
36
+ ### compute для производных полей вместо ручной синхронизации
37
+
38
+ ```typescript
39
+ // ❌ ручной onChange + запись — легко зациклить/забыть кейс
40
+ onChange(model.$.firstName, () => { model.fullName = `${model.firstName} ${model.lastName}`; });
41
+
42
+ // ✅ compute: цель не входит в источники → цикла нет, запись идемпотентна (peek-guard)
43
+ compute(model.$.fullName, () => [model.firstName, model.lastName].filter(Boolean).join(' '));
44
+ ```
45
+
46
+ ### Расходящийся цикл compute (Cycle detected)
47
+
48
+ Взаимные `compute`/`computeFrom` без стабилизации preact обрывает как «Cycle detected».
49
+ DSL перехватывает это и бросает понятную ошибку с именем поля. Решение — разорвать цикл
50
+ условием `when` или читать одну сторону через `peek()`:
51
+
52
+ ```typescript
53
+ // ❌ взаимный пересчёт без стабилизации
54
+ compute(model.$.a, () => model.b + 1);
55
+ compute(model.$.b, () => model.a + 1); // расходится → Cycle detected
56
+
57
+ // ✅ добавь стабилизирующее условие или однонаправленную зависимость
58
+ compute(model.$.total, () => model.price * model.qty); // одно направление
59
+ ```
60
+
61
+ ### Cross-field валидация — через `cross` области схемы
62
+
63
+ Cross-field живёт в схеме валидации (`@reformer/core/validation`), а не в схеме формы: правило
64
+ вешается на ПОЛЕ-НОСИТЕЛЬ ошибки оператором `cross(sig, check)` из аргумента схемы, а `check`
65
+ получает снимок модели области.
66
+
67
+ ```typescript
68
+ import { defineValidation, validate, validateModel } from '@reformer/core/validation';
69
+ import { required } from '@reformer/core/validators';
70
+ import { type ValidationIssue } from '@reformer/core';
71
+ import { revalidateWhen } from '@reformer/core/model';
72
+
73
+ // ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
74
+ const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
75
+ root.field1 > root.field2 ? { code: 'error' } : null;
76
+ const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
77
+ validateFormModel(model, legacySchema);
78
+
79
+ // ✅ новое: cross-правило — обычная функция над снимком модели
80
+ const field1LessThanField2 = (form: Form): ValidationIssue | null =>
81
+ form.field1 > form.field2 ? { code: 'field1.notLess' } : null;
82
+
83
+ const schema = defineValidation<Form>(({ model, cross }) => {
84
+ validate(model.$.field1, [required()]);
85
+ cross(model.$.field1, field1LessThanField2); // правило на поле-носителе замечания
86
+ });
87
+
88
+ // Прогон — уровнем формы (createForm({ …, validation: schema })): bundle.validate() /
89
+ // bundle.submission.submit(handler); без сборки — раннер. Замечания сами доезжают до нод формы.
90
+ await validateModel(model, schema);
91
+
92
+ // Перезапуск правила при изменении соседнего поля — мост «поведение → валидация»:
93
+ revalidateWhen([model.$.field2], () => void validateModel(model, schema));
94
+ ```
95
+
96
+ > **Актуальный vs удалённый API.** Операторы валидации `validate`/`validateAsync`/`validateWhen`/`apply`/`applyEach`
97
+ > существуют — в `@reformer/core/validation` (активны только внутри прогона схемы); `cross` — в аргументе
98
+ > схемы. УДАЛЕНЫ: `applyWhen`, `validateGroup`/`validateTree`/`validateForm`/`validateFormModel`,
99
+ > типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn` и `ctx.form.*`/`ctx.setFieldValue` (наследие
100
+ > path-based архитектуры), а также текст в правиле (`message`), методы проверки на нодах
101
+ > (`form.validate()` / `form.submit()`) и `validation.steps` сборки. См. `17-nonexistent-api.md`.
@@ -0,0 +1,279 @@
1
+ ## 14. PROJECT STRUCTURE (COLOCATION)
2
+
3
+ One form is one module folder. **Default layout — flat minimalist:** every concern is a single
4
+ file at the module root, and the whole form (entry + all wizard steps inline) lives in one
5
+ `index.tsx`. Naming rule: **`form.<role>` is a form artefact, the suffix names its role** —
6
+ `form.schema.*` (layout), `form.behavior.ts` (form behavior: model operators AND schema-node rules
7
+ such as `hideWhen` / `onComponentEvent`), `form.validation.ts` (validation rules), and the optional
8
+ `form.flow.ts` (the steps of a wizard) and `form.messages.ts` (the issue-message dictionary); every
9
+ other file has no prefix. There is one behavior per form:
10
+ the former render-layer file `form.render.ts` (and `renderer.behavior.ts`) is retired — its node
11
+ rules move into `form.behavior.ts`. The old `renderer.schema.*` / `validation.ts` names, and the
12
+ retired `form.render.ts` / `wizard.tsx` / `renderer.wizard.tsx`, are accepted with a warning. No
13
+ folders until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts` and
14
+ rendered with `FieldArray` / `FieldArraySection` — no per-step component files.
15
+
16
+ ```
17
+ src/
18
+ ├── components/ui/ # App-wide reusable UI (FormField, FieldArraySection, ...)
19
+ │
20
+ ├── forms/
21
+ │ └── [form-name]/ # Form module — flat, one file per concern
22
+ │ ├── index.tsx # entry + whole form: ONE createForm call → <FormWizard flow={bundle.flow}> with all steps inline
23
+ │ ├── types.ts # form type (interface or type) + enums + { value, label } option type + constant dictionaries
24
+ │ ├── model.ts # createModel + initial values + arrayOf(blank) templates of array rows
25
+ │ ├── form.schema.ts # schema builder (model) => node tree: { model: model.$.x, component, componentProps }
26
+ │ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / copyFrom / onChange + hideWhen / onComponentEvent
27
+ │ ├── form.validation.ts # rules of each step (exported) + extras
28
+ │ ├── form.flow.ts # optional, wizard: flow { steps: [{ id, rules }] }
29
+ │ ├── form.messages.ts # optional: defineIssueMessages dictionary (issue texts by code)
30
+ │ ├── data-sources.ts # options + async loaders (dataSources)
31
+ │ └── api.ts # submit + prefill/load
32
+ ```
33
+
34
+ Optionally a wizard may keep its steps in folders: `steps/index.ts` (step order) and one
35
+ `steps/<slug>/` folder per step (`<slug>` — kebab-case of the step title, no number) with the same
36
+ file names — `form.schema.ts`, `form.validation.ts`, `form.behavior.ts` — for the step only;
37
+ everything that crosses steps stays in the root files.
38
+
39
+ Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
40
+ `index.tsx`; validation rules are a single `form.validation.ts`, the steps — `form.flow.ts`.** This is the working default for
41
+ almost every form — reach for folders only when a file stops fitting on a screen.
42
+
43
+ ### Key Files
44
+
45
+ ```typescript
46
+ // forms/credit-application/types.ts — `interface` and `type` both work
47
+ export interface CreditApplication {
48
+ loanType: LoanType;
49
+ loanAmount: number | null; // an empty number input is `null`
50
+ // ...
51
+ }
52
+ // computed fields (targets of `compute`) — a separate type; the form is the intersection
53
+ export interface CreditApplicationDerived {
54
+ monthlyPayment: number;
55
+ // ...
56
+ }
57
+ export type CreditApplicationForm = CreditApplication & CreditApplicationDerived;
58
+
59
+ // forms/credit-application/model.ts
60
+ import { createModel, type FormModel } from '@reformer/core';
61
+ export const createCreditApplicationModel = (): FormModel<CreditApplicationForm> =>
62
+ createModel<CreditApplicationForm>(createInitialCreditApplication());
63
+
64
+ // forms/credit-application/form.schema.ts
65
+ import type { FormModel, FormSchemaNode } from '@reformer/core';
66
+ export const creditApplicationSchema = (
67
+ model: FormModel<CreditApplicationForm>
68
+ ): FormSchemaNode => ({
69
+ // Nested nodes live in `children` only — not in componentProps, not under arbitrary keys.
70
+ children: [
71
+ { model: model.$.loanType, component: SelectAsync, componentProps: { /* ... */ } }, // field
72
+ { model: model.$.registrationAddress, part: address }, // sub-form: group + part
73
+ { model: model.$.properties, component: FieldArray, item: propertyRow }, // array of sub-forms
74
+ ],
75
+ });
76
+
77
+ // forms/credit-application/form.behavior.ts — ONE behavior: model operators and node rules
78
+ import { defineFormBehavior, compute, enableWhen, hideWhen } from '@reformer/core/behaviors';
79
+ export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(
80
+ ({ model, schema }) => {
81
+ compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
82
+ enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
83
+ hideWhen(schema.node('mortgage'), () => model.loanType !== 'mortgage'); // executed by the renderer
84
+ }
85
+ );
86
+
87
+ // forms/credit-application/form.validation.ts — step rules (exported) and the extras
88
+ import type { FormValidation } from '@reformer/core';
89
+ import { defineValidation } from '@reformer/core/validation';
90
+ export const loanRules = defineValidation<CreditApplicationForm>(({ model }) => { /* ... */ });
91
+ export const contactsRules = defineValidation<CreditApplicationForm>(({ model }) => { /* ... */ });
92
+ export const creditApplicationValidation: FormValidation<CreditApplicationForm> = {
93
+ extras: crossStepRules, // checked only as a whole (submit)
94
+ };
95
+
96
+ // forms/credit-application/form.flow.ts — the steps of the wizard
97
+ import type { FormFlowConfig } from '@reformer/core';
98
+ import { loanRules, contactsRules } from './form.validation';
99
+ export const creditApplicationFlow: FormFlowConfig<CreditApplicationForm> = {
100
+ steps: [
101
+ { id: 'loan', rules: loanRules }, // id = selector of the step node in form.schema.ts
102
+ { id: 'contacts', rules: contactsRules },
103
+ ],
104
+ };
105
+
106
+ // forms/credit-application/form.messages.ts — the issue texts by code
107
+ import { defineIssueMessages } from '@reformer/core/i18n';
108
+ export const creditApplicationMessages = defineIssueMessages({
109
+ 'loanAmount.required': 'Укажите сумму кредита',
110
+ });
111
+
112
+ // forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline
113
+ import { createForm } from '@reformer/core';
114
+ // ONE call: model + form + behavior + validation + flow. In the page: useFormBundle(createCreditApplicationForm).
115
+ export const createCreditApplicationForm = () =>
116
+ createForm<CreditApplicationForm>({
117
+ model: createCreditApplicationModel(),
118
+ schema: creditApplicationSchema, // builder (model) => tree
119
+ behavior: creditApplicationBehavior,
120
+ validation: creditApplicationValidation, // { extras } — rules outside the steps
121
+ flow: creditApplicationFlow, // steps → bundle.flow, bundle.validateStep(id)
122
+ });
123
+ // The page: <ValidationMessagesProvider messages={creditApplicationMessages}> around the form,
124
+ // <FormWizard form={bundle.form} flow={bundle.flow} … onSubmit={() => bundle.submission.submit(api.send)} />.
125
+ ```
126
+
127
+ ### Scaling up: folders (large forms)
128
+
129
+ When the flat module gets unwieldy — a multi-step wizard, sub-forms reused across steps, rules
130
+ that outgrow one file — split it by **concern**, one folder per layer. Each layer answers one
131
+ question about the form:
132
+
133
+ | Folder | Question it answers |
134
+ | -------------- | ------------------------------------------------------- |
135
+ | `model/` | What data does the form hold, and what does it start as |
136
+ | `schema/` | How is the form laid out |
137
+ | `validation/` | What is a valid value |
138
+ | `behavior/` | How do values and field state react to each other |
139
+ | `flow/` | Which steps does the user walk through, in what order |
140
+ | `application/` | How is the form assembled, loaded and submitted |
141
+
142
+ ```
143
+ forms/
144
+ └── [form-name]/
145
+ ├── [FormName]Form.tsx # Entry: builds the form, renders FormWizard + step components
146
+ │
147
+ ├── model/
148
+ │ ├── model.ts # create[FormName]Model — the reactive model
149
+ │ ├── initial-value.ts # initial values; arrays via arrayOf(blank)
150
+ │ ├── predicates.ts # conditions shared by behavior, validation and JSX
151
+ │ └── factories/ # blank values of sub-forms and array rows
152
+ │ ├── address.ts
153
+ │ └── property.ts
154
+ │
155
+ ├── schema/
156
+ │ ├── form.ts # the tree: loading → wizard → steps taken from the flow
157
+ │ ├── steps/ # content of each step (an array of nodes)
158
+ │ │ ├── loan.ts
159
+ │ │ └── contacts.ts
160
+ │ └── sections/ # sections, sub-forms (part) and array rows (item)
161
+ │ ├── address.ts
162
+ │ └── property.ts
163
+ │
164
+ ├── validation/
165
+ │ ├── form.ts # FormValidation: { extras } — rules outside the steps
166
+ │ ├── messages.ts # defineIssueMessages: issue code → text
167
+ │ ├── common/ # shared rule sets, rules of sub-forms and array rows
168
+ │ │ ├── rules.ts
169
+ │ │ └── address.ts
170
+ │ ├── loan.ts # rules of one step — one file per step
171
+ │ ├── contacts.ts
172
+ │ └── cross-step.ts # rules of the whole form (checked on submit)
173
+ │
174
+ ├── behavior/
175
+ │ ├── index.ts # the form behavior, composed of the parts below
176
+ │ ├── derived.ts # computed fields
177
+ │ ├── conditions.ts # enabling fields and hiding sections
178
+ │ ├── synchronization.ts # copies, clearing lists
179
+ │ ├── dynamic-options.ts # options and limits that depend on values
180
+ │ └── operators.ts # custom operators built on the built-in ones
181
+ │
182
+ ├── flow/
183
+ │ └── [form-name]-flow.ts # the list of steps: selector, title, icon, content, rules
184
+ │ # + FormFlowConfig { steps: [{ id, rules }] } for createForm
185
+ │
186
+ ├── application/
187
+ │ ├── create.ts # ONE createForm call shared by every way of rendering
188
+ │ ├── load.ts # network: fetch the entity and dictionaries
189
+ │ ├── mapping.ts # server response → model (model.patch + captureInitial)
190
+ │ ├── payload.ts # form values → request body: drops the computed fields
191
+ │ ├── submit.ts # sending and reporting the outcome (handler of bundle.submission.submit)
192
+ │ └── renderer.ts # node rules for the renderer: loading, submit, navigation (setup)
193
+ │
194
+ ├── components/ # React layout: steps/, nested-forms/, ui/
195
+ ├── api/ # HTTP calls
196
+ └── types/ constants/ utils/ # form type, option dictionaries, pure calculations
197
+ ```
198
+
199
+ Dependencies point one way: `application → flow → schema/steps, validation/<step>`;
200
+ `application → behavior → model`. Nothing below `application/` knows the entity id, the network or
201
+ the wizard — the behavior is static and reusable.
202
+
203
+ **The flow is the single place a step is declared.** One entry carries the step's `selector`,
204
+ title, icon, content and rules, so the step nodes of the schema, the `flow` of the assembly and
205
+ the wizard steps of a hand-written React page are all derived from the same list — there is
206
+ nowhere to pair the layout of one step with the rules of another:
207
+
208
+ ```typescript
209
+ // flow/credit-application-flow.ts
210
+ export const creditApplicationFlow = [
211
+ { selector: 'loan', title: 'Loan', icon: '💰', content: loanStep, rules: loanRules },
212
+ { selector: 'contacts', title: 'Contacts', icon: '📞', content: contactsStep, rules: contactsRules },
213
+ ] as const satisfies readonly FlowStep[];
214
+
215
+ // the same list → the flow of createForm; the step id is the step selector
216
+ export const creditApplicationFlowSteps: FormFlowConfig<CreditApplicationForm> = {
217
+ steps: creditApplicationFlow.map((step) => ({ id: step.selector, rules: step.rules })),
218
+ };
219
+
220
+ // schema/form.ts — step nodes come from the flow
221
+ children: creditApplicationFlow.map((step) => ({
222
+ selector: step.selector,
223
+ component: Step,
224
+ componentProps: { title: step.title, icon: step.icon },
225
+ children: step.content(model),
226
+ })),
227
+
228
+ // validation/form.ts — only the rules outside the steps
229
+ export const creditApplicationValidation: FormValidation<CreditApplicationForm> = {
230
+ extras: crossStepRules,
231
+ };
232
+
233
+ // application/create.ts
234
+ createForm<CreditApplicationForm>({
235
+ model: createCreditApplicationModel(),
236
+ schema: creditApplicationSchema,
237
+ behavior: creditApplicationBehavior,
238
+ validation: creditApplicationValidation,
239
+ flow: creditApplicationFlowSteps,
240
+ });
241
+ ```
242
+
243
+ **Texts of the issues live in `validation/messages.ts`.** Rules carry codes
244
+ (`required({ code: 'loanAmount.required' })`), the dictionary maps a code to its text, and every
245
+ way of rendering wraps the form in `<ValidationMessagesProvider messages={…}>` from
246
+ `@reformer/cdk` — the texts are the same in hand-written React, renderer-react and renderer-json.
247
+
248
+ **Conditions live in `model/predicates.ts`** as pure functions over values
249
+ (`isMortgage(loanType)`). Behavior (`enableWhen`, `hideWhen`), validation (`validateWhen`) and the
250
+ JSX of a step call the same function — a condition is written once.
251
+
252
+ **Loading and submitting are not behavior.** They belong to `application/`: the page (or, for the
253
+ renderer, `application/renderer.ts` passed as `setup`) calls `load`, hands the response to
254
+ `mapping`, and sends through `bundle.submission.submit(handler)` — one run of all the rules, the
255
+ handler is called only when the form is valid. Written into the model with `model.patch` followed
256
+ by `model.captureInitial()`, the loaded values become the form's starting point: the form is not
257
+ dirty, and `model.reset()` returns to them. The handler receives `model.get()` — the whole model,
258
+ disabled fields included; `application/payload.ts` drops the fields the server does not expect
259
+ (computed ones) before the request.
260
+
261
+ Rule of thumb for this layout: **one folder per question; a step is declared once, in the flow;
262
+ the root holds only the entry component.**
263
+
264
+ ### Scaling
265
+
266
+ | Complexity | Structure |
267
+ | ---------- | --------------------------------------------------------------------------------------- |
268
+ | Simple | Single file: `index.tsx` (model + schema + behavior + component) |
269
+ | **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `form.validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |
270
+ | Folders (`model/` + `schema/` + `validation/` + `behavior/` + `flow/` + `application/`) | Large forms: one folder per layer, a step declared once in the flow, reusable sections and rules |
271
+
272
+ > The leading layout of the MCP generators is configurable: set `REFORMER_FORM_LAYOUT`
273
+ > (`minimalist` | `folders`) when registering the MCP server. Default is `minimalist`.
274
+
275
+ > The MCP layout check (`validate_form` with `kind: "layout"`) verifies the flat layout file names
276
+ > (and the optional `steps/<slug>/` folders); the folders layout is recognized by its top-level
277
+ > folders only. Cross-target variants (renderer-react: the same files; renderer-json: plus
278
+ > `registry.ts`, and `form.schema.ts` holds the JSON-DSL document) are described in the
279
+ > **form-directory-layout** guide (`find_recipe directory-layout`).