@reformer/core 11.0.0-beta.8 → 11.0.0-beta.9

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 (224) hide show
  1. package/README.md +91 -73
  2. package/dist/behaviors-value-HA_Q03hc.js +108 -0
  3. package/dist/behaviors.js +370 -242
  4. package/dist/{create-model-BBRHhHwK.js → create-model-BvcbNPgL.js} +6 -6
  5. package/dist/date-utils-90-qZbse.js +43 -0
  6. package/dist/{en-CjAqn7h0.js → en-HP8XEych.js} +6 -3
  7. package/dist/form/aggregate-signals.d.ts +21 -63
  8. package/dist/form/behaviors/collections.d.ts +28 -7
  9. package/dist/form/behaviors/context.d.ts +19 -2
  10. package/dist/form/behaviors/index.d.ts +3 -3
  11. package/dist/form/behaviors/internals.d.ts +9 -2
  12. package/dist/form/behaviors/node.d.ts +25 -2
  13. package/dist/form/behaviors/operators.d.ts +17 -9
  14. package/dist/form/behaviors/schema-operators.d.ts +6 -4
  15. package/dist/form/behaviors/types.d.ts +12 -4
  16. package/dist/form/create-form.d.ts +47 -35
  17. package/dist/form/flow/flow.d.ts +87 -0
  18. package/dist/form/form-bundle.d.ts +45 -28
  19. package/dist/form/form-config-base.d.ts +46 -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 +16 -21
  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 +30 -14
  37. package/dist/form/signal-node-registry.d.ts +6 -7
  38. package/dist/form/submission.d.ts +93 -0
  39. package/dist/form/type-guards.d.ts +14 -73
  40. package/dist/form/types/contracts.d.ts +19 -92
  41. package/dist/form/types/field-config.d.ts +27 -0
  42. package/dist/form/types/index.d.ts +5 -57
  43. package/dist/form/types/nodes.d.ts +159 -0
  44. package/dist/form/types/schema-node.d.ts +139 -63
  45. package/dist/form/types/validation-schema.d.ts +23 -27
  46. package/dist/form/validation/context.d.ts +42 -7
  47. package/dist/form/validation/index.d.ts +9 -8
  48. package/dist/form/validation/operators.d.ts +22 -30
  49. package/dist/form/validation/run.d.ts +93 -18
  50. package/dist/form/validation/strategy.d.ts +41 -13
  51. package/dist/form/validation/types.d.ts +74 -20
  52. package/dist/form/validators/date-utils.d.ts +13 -1
  53. package/dist/form/validators/email.d.ts +8 -7
  54. package/dist/form/validators/file-type.d.ts +8 -7
  55. package/dist/form/validators/file-utils.d.ts +2 -1
  56. package/dist/form/validators/future-date.d.ts +8 -7
  57. package/dist/form/validators/index.d.ts +1 -1
  58. package/dist/form/validators/integer.d.ts +8 -7
  59. package/dist/form/validators/is-date.d.ts +11 -9
  60. package/dist/form/validators/is-number.d.ts +11 -9
  61. package/dist/form/validators/max-age.d.ts +13 -9
  62. package/dist/form/validators/max-date.d.ts +8 -7
  63. package/dist/form/validators/max-file-size.d.ts +8 -7
  64. package/dist/form/validators/max-files.d.ts +8 -7
  65. package/dist/form/validators/max-length.d.ts +14 -7
  66. package/dist/form/validators/max-total-file-size.d.ts +8 -7
  67. package/dist/form/validators/max.d.ts +10 -8
  68. package/dist/form/validators/min-age.d.ts +12 -9
  69. package/dist/form/validators/min-date.d.ts +8 -7
  70. package/dist/form/validators/min-file-size.d.ts +8 -7
  71. package/dist/form/validators/min-files.d.ts +11 -9
  72. package/dist/form/validators/min-length.d.ts +14 -7
  73. package/dist/form/validators/min.d.ts +10 -8
  74. package/dist/form/validators/multiple-of.d.ts +10 -8
  75. package/dist/form/validators/non-negative.d.ts +8 -7
  76. package/dist/form/validators/non-zero.d.ts +8 -7
  77. package/dist/form/validators/past-date.d.ts +8 -7
  78. package/dist/form/validators/pattern.d.ts +9 -8
  79. package/dist/form/validators/phone.d.ts +8 -7
  80. package/dist/form/validators/required.d.ts +11 -9
  81. package/dist/form/validators/url.d.ts +20 -13
  82. package/dist/form/validators/validation-issue.d.ts +8 -0
  83. package/dist/i18n/descriptor.d.ts +1 -1
  84. package/dist/i18n/en.json.d.ts +5 -1
  85. package/dist/i18n/locale-store.d.ts +7 -0
  86. package/dist/i18n/message-format.d.ts +24 -4
  87. package/dist/i18n/ru.json.d.ts +5 -1
  88. package/dist/i18n/translator.d.ts +11 -0
  89. package/dist/i18n/validation-message.d.ts +42 -7
  90. package/dist/i18n.d.ts +7 -12
  91. package/dist/i18n.js +66 -283
  92. package/dist/index.d.ts +16 -7
  93. package/dist/index.js +924 -2855
  94. package/dist/loader-BWkigpVe.js +52 -0
  95. package/dist/locale/en.js +1 -1
  96. package/dist/locale/en.json +4 -1
  97. package/dist/locale/ru.js +4 -1
  98. package/dist/locale/ru.json +4 -1
  99. package/dist/locale-CG54AU2Q.js +219 -0
  100. package/dist/locale.js +1 -1
  101. package/dist/model/behaviors-value.d.ts +24 -6
  102. package/dist/model/create-model.d.ts +2 -2
  103. package/dist/model/derived-registry.d.ts +1 -1
  104. package/dist/model/index.d.ts +11 -5
  105. package/dist/model/model-nodes.d.ts +79 -12
  106. package/dist/model/model-signals-proxy.d.ts +5 -6
  107. package/dist/model/model-value-proxy.d.ts +20 -15
  108. package/dist/model/safe-effect.d.ts +33 -1
  109. package/dist/model/types.d.ts +63 -13
  110. package/dist/model.js +20 -18
  111. package/dist/node-kind-C-r6ZMdo.js +15 -0
  112. package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
  113. package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
  114. package/dist/platforms/react/form-bundle-context.d.ts +2 -2
  115. package/dist/platforms/react/hooks/types.d.ts +57 -38
  116. package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
  117. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  118. package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
  119. package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
  120. package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
  121. package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
  122. package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
  123. package/dist/platforms/react/i18n/context.d.ts +6 -1
  124. package/dist/platforms/react/i18n/hooks.d.ts +16 -10
  125. package/dist/platforms/react/i18n/provider.d.ts +9 -2
  126. package/dist/platforms/react/index.d.ts +6 -4
  127. package/dist/react.d.ts +22 -0
  128. package/dist/react.js +505 -0
  129. package/dist/runtime-token.d.ts +1 -2
  130. package/dist/safe-effect-BbtX3KuN.js +643 -0
  131. package/dist/schema-controller-CpiO2JoK.js +143 -0
  132. package/dist/signals.d.ts +15 -9
  133. package/dist/signals.js +11 -6
  134. package/dist/strategy-CoFSemuP.js +375 -0
  135. package/dist/type-guards-D-N_5Ddh.js +19 -0
  136. package/dist/validation-issue-B9mjI8jz.js +7 -0
  137. package/dist/validation-message-Ced9MGxs.js +194 -0
  138. package/dist/validation.js +9 -9
  139. package/dist/validators/email.js +5 -5
  140. package/dist/validators/future-date.js +8 -8
  141. package/dist/validators/integer.js +3 -3
  142. package/dist/validators/is-date.js +4 -4
  143. package/dist/validators/is-number.js +4 -4
  144. package/dist/validators/max-age.js +8 -9
  145. package/dist/validators/max-date.js +8 -8
  146. package/dist/validators/max-length.js +7 -7
  147. package/dist/validators/max.js +4 -4
  148. package/dist/validators/min-age.js +10 -8
  149. package/dist/validators/min-date.js +3 -3
  150. package/dist/validators/min-length.js +5 -5
  151. package/dist/validators/min.js +4 -4
  152. package/dist/validators/multiple-of.js +11 -7
  153. package/dist/validators/non-negative.js +3 -3
  154. package/dist/validators/non-zero.js +3 -3
  155. package/dist/validators/past-date.js +8 -8
  156. package/dist/validators/pattern.js +5 -5
  157. package/dist/validators/phone.js +11 -9
  158. package/dist/validators/required.js +2 -2
  159. package/dist/validators/url.js +48 -12
  160. package/dist/validators.js +17 -17
  161. package/docs/llms/01-api-reference.md +60 -34
  162. package/docs/llms/02-quick-start.md +111 -80
  163. package/docs/llms/03-api-signatures.md +368 -153
  164. package/docs/llms/04-common-patterns.md +58 -56
  165. package/docs/llms/05-common-mistakes.md +69 -44
  166. package/docs/llms/06-troubleshooting.md +14 -9
  167. package/docs/llms/07-complete-import.md +74 -43
  168. package/docs/llms/08-form-types.md +11 -4
  169. package/docs/llms/09-formschema.md +129 -81
  170. package/docs/llms/10-arrays.md +106 -75
  171. package/docs/llms/11-async-watchfield.md +2 -2
  172. package/docs/llms/13-multi-step.md +134 -63
  173. package/docs/llms/14-extended-mistakes.md +20 -17
  174. package/docs/llms/15-project-structure.md +221 -74
  175. package/docs/llms/16-ui-components.md +55 -42
  176. package/docs/llms/17-nonexistent-api.md +199 -32
  177. package/docs/llms/18-conditional-fields.md +66 -32
  178. package/docs/llms/19-reading-values.md +15 -3
  179. package/docs/llms/20-compute-vs-watch.md +16 -10
  180. package/docs/llms/21-array-operations.md +51 -27
  181. package/docs/llms/22-cycle-detection.md +1 -1
  182. package/docs/llms/23-copy-from.md +2 -2
  183. package/docs/llms/24-sync-fields.md +6 -5
  184. package/docs/llms/25-reset-when.md +14 -7
  185. package/docs/llms/26-transform-value.md +4 -4
  186. package/docs/llms/27-revalidate-when.md +16 -15
  187. package/docs/llms/28-submit-and-reset.md +276 -128
  188. package/docs/llms/29-async-preload.md +39 -29
  189. package/docs/llms/30-type-safety-recipes.md +105 -51
  190. package/docs/llms/31-async-validator-debounce.md +96 -53
  191. package/docs/llms/33-validation-strategy.md +177 -65
  192. package/docs/llms/34-i18n.md +69 -43
  193. package/docs/llms/35-unified-contract.md +100 -39
  194. package/docs/llms/36-issue-messages.md +284 -0
  195. package/docs/llms/37-compiled-schema.md +332 -0
  196. package/docs/llms/38-form-flow.md +397 -0
  197. package/llms-index.json +1 -1
  198. package/llms.txt +6084 -3420
  199. package/package.json +23 -4
  200. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  201. package/dist/date-utils-dTT_x_Be.js +0 -31
  202. package/dist/form/create-core-form.d.ts +0 -72
  203. package/dist/form/factories/index.d.ts +0 -6
  204. package/dist/form/factories/node-factory.d.ts +0 -189
  205. package/dist/form/form-submitter.d.ts +0 -121
  206. package/dist/form/nodes/array-node.d.ts +0 -276
  207. package/dist/form/nodes/field-node.d.ts +0 -297
  208. package/dist/form/nodes/form-node.d.ts +0 -326
  209. package/dist/form/nodes/group-node.d.ts +0 -284
  210. package/dist/form/nodes/model-array-node.d.ts +0 -126
  211. package/dist/form/nodes/subscription-manager.d.ts +0 -180
  212. package/dist/form/status-machine.d.ts +0 -115
  213. package/dist/form/types/deep-schema.d.ts +0 -107
  214. package/dist/form/types/form-proxy.d.ts +0 -141
  215. package/dist/form/unique-id.d.ts +0 -45
  216. package/dist/form/validation/config.d.ts +0 -73
  217. package/dist/form/validation/error-handler.d.ts +0 -166
  218. package/dist/form/validators/validation-error.d.ts +0 -8
  219. package/dist/index-BrNkQj1y.js +0 -105
  220. package/dist/loader-Dv_Hl7JC.js +0 -216
  221. package/dist/model-value-proxy-caTmAiUd.js +0 -419
  222. package/dist/node-BeV_lV5i.js +0 -100
  223. package/dist/strategy-DgzELWno.js +0 -204
  224. package/dist/validation-error-B-U3RsV0.js +0 -11
@@ -0,0 +1,397 @@
1
+ # Уровень формы и поток шагов — validate, submit и FormFlow
2
+
3
+ ## Purpose
4
+
5
+ `createForm` возвращает сборку: модель, форму, часть для рендерера и **уровень формы** — проверку,
6
+ проверку шага, отправку и поток шагов. Уровень один для всех способов реализации: React-руками,
7
+ рендерер по TS-схеме (`createReactForm`), рендерер по JSON (`createJsonForm`) собирают его одной
8
+ функцией `createFormLevel`, поэтому `validate`, отправка (`submission.submit`) и поток ведут себя
9
+ одинаково.
10
+
11
+ - Правила формы передаются сборке данными: `validation` — полный набор (`rules`), правила вне
12
+ шагов (`extras`) и стратегия живой проверки.
13
+ - Шаги и их правила — поток: `flow: { steps: [{ id, rules }] }`. Идентификатор шага — `selector`
14
+ узла шага в схеме.
15
+ - Результаты — объекты со статусом, а не `boolean`: по ним отличают ошибки в форме (`invalid`) от
16
+ сбоя проверки (`error`), отмены (`cancelled`) и исключения обработчика (`failed`).
17
+
18
+ ## API
19
+
20
+ ```typescript
21
+ import {
22
+ createForm,
23
+ createFormFlow,
24
+ type FormBundle,
25
+ type FormFlow,
26
+ type FormFlowConfig,
27
+ type FormValidation,
28
+ type FlowTransition,
29
+ type FormSubmission,
30
+ type SubmitResult,
31
+ } from '@reformer/core';
32
+ import { useFormBundle, useFormFlow } from '@reformer/core/react';
33
+
34
+ // Конфиг сборки — поля уровня формы
35
+ interface CreateFormConfig<T> {
36
+ // … initial | model, schema, registry, behavior, seed, setup
37
+ validation?: FormValidation<T> | ValidationDefinition<T>; // голая схема = { rules }
38
+ flow?: FormFlowConfig<T>;
39
+ }
40
+ interface FormValidation<T> {
41
+ rules?: ValidationDefinition<T>; // полный набор; без него — правила всех шагов + extras
42
+ extras?: ValidationDefinition<T>; // правила вне шагов: проверяются только целиком
43
+ strategy?: 'submit' | 'blur' | 'change' | 'afterFirstSubmit'; // живая проверка, по умолчанию 'submit'
44
+ debounce?: number;
45
+ liveAfterSubmit?: 'change' | 'blur';
46
+ }
47
+ interface FormFlowConfig<T> {
48
+ steps: readonly { id: string; rules?: ValidationDefinition<T> | null }[];
49
+ }
50
+
51
+ // Сборка
52
+ interface FormBundle<T> {
53
+ model: FormModel<T>;
54
+ form: FormGroup<T>;
55
+ render: FormRender; // { tree: CompiledSchema | undefined, controller, node(selector) }
56
+ flow: FormFlow | undefined; // без flow в конфиге — undefined
57
+ validating: ReadonlySignal<boolean>; // идёт полная проверка или проверка шага
58
+ submission: FormSubmission<T>; // отправка — отдельный объект сборки
59
+ // служебное: полный набор правил и контроллер живой проверки (его армирует useFormBundle);
60
+ // без правил — undefined. Методов проверки на нём нет — проверяют validate/validateStep/submission
61
+ validation: { rules: ValidationDefinition<T>; controller: FormValidationController } | undefined;
62
+ validate(): Promise<ValidationResult>; // вся форма, замечания показываются
63
+ validateStep(stepId: string): Promise<ValidationResult>; // неизвестный шаг — исключение
64
+ dispose(): void; // снять сборку; идущие проверки — cancelled, модель остаётся
65
+ }
66
+
67
+ // Отправка
68
+ interface FormSubmission<T> {
69
+ submitting: ReadonlySignal<boolean>; // работает обработчик (пока идёт проверка — false)
70
+ submit<R>(handler: (values: T) => R | Promise<R>): Promise<SubmitResult<R>>;
71
+ }
72
+
73
+ type SubmitResult<R> =
74
+ | { status: 'submitted'; result: R } // обработчик отработал
75
+ | { status: 'invalid' | 'error' | 'cancelled'; validation: ValidationResult } // обработчик не вызывался
76
+ | { status: 'failed'; error: unknown }; // обработчик бросил исключение
77
+
78
+ // Поток шагов
79
+ interface FormFlow {
80
+ readonly steps: readonly string[];
81
+ readonly current: ReadonlySignal<string>;
82
+ readonly index: ReadonlySignal<number>; // с нуля
83
+ readonly completed: ReadonlySignal<ReadonlySet<string>>;
84
+ readonly isFirst: ReadonlySignal<boolean>;
85
+ readonly isLast: ReadonlySignal<boolean>;
86
+ readonly validating: ReadonlySignal<boolean>; // идёт проверка шага в next()
87
+ canGoTo(stepId: string): boolean;
88
+ next(): Promise<FlowTransition>; // проверить правила текущего шага и перейти
89
+ previous(): FlowTransition;
90
+ goTo(stepId: string): FlowTransition; // неизвестный шаг — исключение
91
+ reset(): void; // на первый шаг, пройденные забыты
92
+ }
93
+ type FlowTransition =
94
+ | { status: 'moved'; from: string; to: string }
95
+ | { status: 'stayed'; step: string; reason: FlowStayReason; validation?: ValidationResult };
96
+ type FlowStayReason = 'invalid' | 'error' | 'cancelled' | 'edge' | 'locked' | 'current';
97
+
98
+ // React: снимок потока для рендера
99
+ function useFormFlow(flow: FormFlow): {
100
+ current: string; index: number; completed: ReadonlySet<string>;
101
+ isFirst: boolean; isLast: boolean; validating: boolean;
102
+ };
103
+
104
+ // Поток без сборки
105
+ function createFormFlow(options: {
106
+ steps: readonly string[];
107
+ validateStep?: (stepId: string) => Promise<ValidationResult>; // без неё шаги без проверки
108
+ }): FormFlow;
109
+ ```
110
+
111
+ ### Проверка и отправка
112
+
113
+ | Вызов | Какие правила | Что делает с нодами |
114
+ | ----------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------- |
115
+ | `bundle.validate()` | полный набор (`rules` либо шаги + `extras`) | разносит замечания, проверенные поля — `touched` |
116
+ | `bundle.validateStep(id)` | правила шага `id`; шаг без правил — `valid` | разносит замечания, поля шага — `touched` |
117
+ | `bundle.submission.submit(handler)` | один прогон полного набора | `invalid` / `error` — вся форма `touched`, `cancelled` — ничего; обработчик не зовётся |
118
+ | `flow.next()` | правила текущего шага (`validateStep`) | как `validateStep`; при успехе — переход |
119
+
120
+ - `ValidationResult` — `{ status, issues, failures }`: `status` — `valid | invalid | error |
121
+ cancelled`, `issues` — замечания по ручке поля, `failures` — правила, не вернувшие результат.
122
+ - Без правил `validate()` отвечает `valid`; `submission.submit` при этом сразу зовёт обработчик.
123
+ - Обработчик получает `model.get()` — значения модели целиком, с выключенными полями (их правила
124
+ не считаются, но данные принадлежат модели); лишние для сервера поля отрезает он сам.
125
+ - `submission.submitting` истинен, пока работает обработчик; `validating` — пока идёт проверка.
126
+ - Обработчик получает то, что видели правила: если модель изменилась, пока шла проверка отправки
127
+ (правка поля, загрузка, запись поведения после таймера или запроса), `submission.submit`
128
+ отвечает `cancelled` и обработчик не зовёт — отправку повторяют.
129
+ - Правила читают модель в момент вызова. Исключение — вызов `validate` / `validateStep` /
130
+ `submission.submit` / `flow.next()` в одном тике со сборкой формы или правкой поля: поведение ещё пишет
131
+ отложенно (колбэк `onChange`, сброс `resetOnDisable`), проверка дожидается этих записей и
132
+ читает модель после них; `validating` поднят сразу. Раннеры без сборки (`validateModel`,
133
+ `runValidation`) исполняют правила в момент вызова.
134
+ - Прогоны шага и всей формы в одно поле друг другу не мешают: замечания поля учтены по
135
+ определению-источнику, и `validateStep` заменяет только замечания правил шага — замечание `extras`
136
+ полной проверки остаётся, а устаревшее замечание правил шага стирается. `reset()` / `clearIssues()`
137
+ ноды учёт сбрасывают.
138
+ - Замечание правила на поле без ноды (массив примитивов без узла в схеме) ложится на корень формы
139
+ (`form.ownIssues`): статус прогона совпадает с `form.valid`. Ошибку сервера на корне прогон не стирает.
140
+ - Живую фазу стратегии `afterFirstSubmit` включает только попытка отправки —
141
+ `submission.submit(handler)`. `validate()`, `validateStep(id)` и `flow.next()` — проверки без
142
+ отправки, фазу не переключают. Живую проверку армирует `useFormBundle` в эффекте (на сервере не
143
+ армируется).
144
+ - `bundle.dispose()` снимает сборку: поведение формы, подписки нод, живую проверку; идущие
145
+ проверки (полная, шага, отправки) получают `cancelled`. Значения модели остаются; вызов
146
+ идемпотентен. `useFormBundle` сборку при размонтировании не снимает.
147
+ - `rules` и `extras` — стабильные ссылки (`defineValidation` в модуле): отмена устаревших прогонов
148
+ ключится по паре «модель + правила».
149
+
150
+ ### Правила переходов потока
151
+
152
+ - `next()` на шаге с замечаниями — `stayed` с причиной `invalid` / `error` / `cancelled` и
153
+ результатом проверки в `validation`. На последнем шаге — `stayed`, `reason: 'edge'` (шаг при этом
154
+ отмечается пройденным).
155
+ - Пока шла проверка, поток перевели или сбросили — `stayed`, `reason: 'cancelled'`.
156
+ - `next()` — действие по результату проверки, как отправка: если модель изменилась, пока шла
157
+ проверка шага (правка поля, загрузка, запись поведения после таймера или запроса), правила
158
+ этих значений не видели — `stayed`, `reason: 'cancelled'`, шаг пройденным не считается; переход
159
+ повторяют. `validateStep` и `validate` — отчёты: их статус совпадает с состоянием нод и о
160
+ модели не судит.
161
+ - `previous()` на первом шаге — `stayed`, `reason: 'edge'`. Назад: пройденными остаются только шаги
162
+ до цели.
163
+ - `goTo(id)`: первый шаг доступен всегда, остальные — после прохождения предыдущего; иначе
164
+ `stayed`, `reason: 'locked'`. На шаг, где поток уже стоит, — не переход: `stayed`,
165
+ `reason: 'current'`, ждущий `next()` не отменяется. Неизвестный `id` — исключение.
166
+ - Если поток перевели, пока шла проверка шага, `next()` отвечает `stayed`, `reason: 'cancelled'` с
167
+ шагом, где поток стоит сейчас, — и при успехе, и при отказе проверки.
168
+ - Повтор `id` в `flow.steps` — исключение при сборке. Шаг, для которого в корневой области схемы
169
+ нет узла с таким `selector`, — dev-предупреждение (если хотя бы один шаг узел нашёл).
170
+
171
+ ## Examples
172
+
173
+ ### Сборка с потоком шагов
174
+
175
+ ```typescript
176
+ // flow/credit-flow.ts — шаг объявлен один раз: id = selector узла шага
177
+ import type { FormFlowConfig, FormValidation } from '@reformer/core';
178
+
179
+ export const creditFlow: FormFlowConfig<CreditForm> = {
180
+ steps: [
181
+ { id: 'loan', rules: loanRules },
182
+ { id: 'applicant', rules: applicantRules },
183
+ { id: 'confirm' }, // шаг без правил
184
+ ],
185
+ };
186
+
187
+ // правила вне шагов — проверяются только целиком (submission.submit, validate)
188
+ export const creditValidation: FormValidation<CreditForm> = { extras: crossStepRules };
189
+ ```
190
+
191
+ ```tsx
192
+ import { createForm } from '@reformer/core';
193
+ import { useFormBundle } from '@reformer/core/react';
194
+
195
+ const createCredit = () =>
196
+ createForm<CreditForm>({
197
+ model: createCreditModel(),
198
+ schema: creditSchema, // узлы шагов несут selector: 'loan', 'applicant', 'confirm'
199
+ behavior: creditBehavior,
200
+ validation: creditValidation,
201
+ flow: creditFlow,
202
+ });
203
+
204
+ function CreditPage() {
205
+ const credit = useFormBundle(createCredit); // фабрика вызывается один раз
206
+ // …
207
+ }
208
+ ```
209
+
210
+ ### Визард ui-kit — поток сборки
211
+
212
+ ```tsx
213
+ import { FormWizard } from '@reformer/ui-kit';
214
+ import { ValidationMessagesProvider } from '@reformer/cdk';
215
+
216
+ function CreditWizard() {
217
+ const credit = useFormBundle(createCredit);
218
+
219
+ const send = async () => {
220
+ const outcome = await credit.submission.submit(sendApplication);
221
+ if (outcome.status === 'submitted') showToast(`Заявка ${outcome.result.id} отправлена`);
222
+ else if (outcome.status === 'invalid') showToast('Проверьте форму');
223
+ else if (outcome.status === 'error') showToast('Не удалось проверить форму, повторите');
224
+ else if (outcome.status === 'failed') showToast('Сервер недоступен');
225
+ };
226
+
227
+ return (
228
+ <ValidationMessagesProvider messages={creditMessages}>
229
+ <FormWizard form={credit.form} flow={credit.flow} steps={STEPS} onSubmit={send} />
230
+ </ValidationMessagesProvider>
231
+ );
232
+ }
233
+ ```
234
+
235
+ - `flow={credit.flow}` — визард рисует поток и зовёт его переходы; «Далее» — `flow.next()`, при
236
+ замечаниях шаг остаётся, поля показываются.
237
+ - Визард внутри рендерера (`FormRenderer` кладёт сборку в контекст) берёт `form` и `flow` из
238
+ сборки сам; его `onSubmit` зовётся через `bundle.submission.submit` — только после успешной
239
+ проверки всей формы.
240
+ - Без `flow` и без сборки в контексте визард строит позиционный поток **без правил**.
241
+
242
+ ### Свои кнопки — `useFormFlow`
243
+
244
+ ```tsx
245
+ import type { FormBundle } from '@reformer/core';
246
+ import { useFormFlow } from '@reformer/core/react';
247
+
248
+ function StepActions({ bundle }: { bundle: FormBundle<CreditForm> }) {
249
+ const flow = bundle.flow!;
250
+ const { current, isFirst, isLast, validating } = useFormFlow(flow);
251
+
252
+ const next = async () => {
253
+ const transition = await flow.next();
254
+ if (transition.status === 'stayed' && transition.reason === 'error') {
255
+ showToast('Не удалось проверить шаг, повторите');
256
+ }
257
+ };
258
+
259
+ return (
260
+ <div data-step={current}>
261
+ {!isFirst && <button onClick={() => flow.previous()}>Назад</button>}
262
+ {isLast ? (
263
+ <button onClick={() => void bundle.submission.submit(sendApplication)}>Отправить</button>
264
+ ) : (
265
+ <button disabled={validating} onClick={() => void next()}>
266
+ Далее
267
+ </button>
268
+ )}
269
+ </div>
270
+ );
271
+ }
272
+ ```
273
+
274
+ ### Поток в поведении
275
+
276
+ Область поведения получает поток сборки: `defineFormBehavior(({ model, form, schema, flow }) => …)`.
277
+ Без `flow` в конфиге он `undefined`; поведения подформ и строк (`apply`, `applyEach`) получают тот
278
+ же поток.
279
+
280
+ ```typescript
281
+ import { defineFormBehavior, renderEffect } from '@reformer/core/behaviors';
282
+
283
+ export const navigationBehavior = defineFormBehavior<CreditForm>(({ model, schema, flow }) => {
284
+ // смена типа на ипотеку возвращает к шагу кредита
285
+ renderEffect(schema, () => {
286
+ if (model.loanType === 'mortgage') flow?.goTo('loan');
287
+ });
288
+ });
289
+ ```
290
+
291
+ ### Проверка шага и всей формы без перехода
292
+
293
+ ```typescript
294
+ const step = await credit.validateStep('applicant'); // ValidationResult
295
+ if (step.status === 'invalid') scrollToFirstIssue();
296
+
297
+ const all = await credit.validate();
298
+ if (all.status === 'error') console.warn(all.failures); // какое async-правило не ответило
299
+ ```
300
+
301
+ ### Ответ сервера — замечания в поля
302
+
303
+ ```typescript
304
+ // статус 'submitted' значит «обработчик отработал»; отказ сервера — дело обработчика
305
+ const outcome = await credit.submission.submit(async (values) => {
306
+ const response = await api.send(values);
307
+ if (response.status === 422) {
308
+ for (const [field, code] of Object.entries(response.fieldCodes)) {
309
+ credit.form.$[field as keyof CreditForm]?.setIssues([{ code }]);
310
+ }
311
+ }
312
+ return response;
313
+ });
314
+ ```
315
+
316
+ ### Поток без сборки
317
+
318
+ ```typescript
319
+ import { createFormFlow } from '@reformer/core';
320
+ import { applyValidationResult, runValidation } from '@reformer/core/validation';
321
+
322
+ const STEP_RULES = { loan: loanRules, confirm: confirmRules } as const;
323
+
324
+ const flow = createFormFlow({
325
+ steps: ['loan', 'confirm'],
326
+ validateStep: async (id) => {
327
+ const result = await runValidation(model, STEP_RULES[id as keyof typeof STEP_RULES]);
328
+ if (result.status !== 'cancelled') applyValidationResult(result, { touch: true });
329
+ return result;
330
+ },
331
+ });
332
+ await flow.next(); // { status: 'moved', from: 'loan', to: 'confirm' } либо 'stayed'
333
+ ```
334
+
335
+ Обычно это не нужно: `createForm({ flow })` собирает такой поток сам, а визард без потока
336
+ строит позиционный.
337
+
338
+ ## Anti-patterns
339
+
340
+ ```typescript
341
+ // ❌ Шаги ключами validation.steps — удалено
342
+ createForm({ model, schema, validation: { steps: { loan: loanRules }, extras } });
343
+
344
+ // ✅ Шаги — поток сборки; правила вне шагов — extras
345
+ createForm({ model, schema, validation: { extras }, flow: { steps: [{ id: 'loan', rules: loanRules }] } });
346
+ ```
347
+
348
+ ```typescript
349
+ // ❌ Результат проверки как boolean
350
+ if (await credit.validate()) send(); // объект всегда truthy
351
+
352
+ // ✅ Статус результата
353
+ if ((await credit.validate()).status === 'valid') send();
354
+ // для отправки — submission.submit: проверка и обработчик одним вызовом
355
+ await credit.submission.submit(send);
356
+ ```
357
+
358
+ ```typescript
359
+ // ❌ Переход как boolean
360
+ if (await flow.next()) track('step');
361
+
362
+ // ✅ Статус перехода
363
+ const transition = await flow.next();
364
+ if (transition.status === 'moved') track(transition.to);
365
+ ```
366
+
367
+ ```typescript
368
+ // ❌ Прыжок вперёд через непройденный шаг — 'locked', поток не двигается
369
+ flow.goTo('confirm');
370
+
371
+ // ✅ Вперёд — next() (он проверяет шаг); goTo — назад и на уже открытые шаги
372
+ if (flow.canGoTo('confirm')) flow.goTo('confirm');
373
+ ```
374
+
375
+ ```typescript
376
+ // ❌ Номер шага с единицы — прежний контракт; параметр — строка, номер не скомпилируется
377
+ credit.validateStep(1);
378
+
379
+ // ✅ Идентификатор шага — selector узла шага
380
+ credit.validateStep('loan');
381
+ ```
382
+
383
+ ```typescript
384
+ // ❌ Отправка в обход проверки
385
+ await sendApplication(credit.model.get());
386
+
387
+ // ✅ bundle.submission.submit: один прогон всех правил, обработчик — только при valid
388
+ await credit.submission.submit(sendApplication);
389
+ ```
390
+
391
+ ## See also
392
+
393
+ - [13-multi-step.md](./13-multi-step.md) — многошаговая форма: правила шагов, визард, раскладка
394
+ - [28-submit-and-reset.md](./28-submit-and-reset.md) — отправка, сброс, серверные ошибки
395
+ - [33-validation-strategy.md](./33-validation-strategy.md) — живая проверка: `strategy`, `debounce`
396
+ - [36-issue-messages.md](./36-issue-messages.md) — тексты замечаний
397
+ - [37-compiled-schema.md](./37-compiled-schema.md) — `selector`, области и `bundle.render.tree`