@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,108 @@
1
+ ## 32. ASYNC OPTIONS LOADING
2
+
3
+ Для динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,
4
+ `carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +
5
+ `form.field.updateComponentProps({ options })`. `onChange` даёт debounce и AbortSignal из коробки.
6
+
7
+ ```ts
8
+ import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
9
+
10
+ type CityOption = { value: string; label: string };
11
+
12
+ async function fetchCitiesByRegion(region: string, opts?: { signal?: AbortSignal }): Promise<CityOption[]> {
13
+ const res = await fetch(`/api/cities?region=${encodeURIComponent(region)}`, { signal: opts?.signal });
14
+ return res.json();
15
+ }
16
+
17
+ export const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
18
+ onChange(
19
+ model.$.registrationAddress.region,
20
+ async (region, { signal }) => {
21
+ // сбросить зависимое поле при смене source
22
+ model.registrationAddress.city = '';
23
+
24
+ if (!region) {
25
+ form.registrationAddress.city.updateComponentProps({ options: [] });
26
+ return;
27
+ }
28
+
29
+ // (опционально) loading-состояние
30
+ form.registrationAddress.city.updateComponentProps({ loading: true, options: [] });
31
+
32
+ try {
33
+ const options = await fetchCitiesByRegion(region, { signal }); // отмена устаревших
34
+ form.registrationAddress.city.updateComponentProps({ loading: false, options });
35
+ } catch (e) {
36
+ if ((e as Error).name === 'AbortError') return; // устаревший запрос — молча выходим
37
+ form.registrationAddress.city.updateComponentProps({ loading: false, options: [] });
38
+ }
39
+ },
40
+ { debounce: 300 }
41
+ );
42
+ });
43
+ ```
44
+
45
+ ### Lifecycle
46
+
47
+ 1. `onChange` подписан на изменения `model.$.region`.
48
+ 2. Debounce 300 мс (не fetch на каждое нажатие клавиши).
49
+ 3. Async `fetchCitiesByRegion(region, { signal })` — `signal` отменяет устаревший запрос при
50
+ следующей смене значения (защита от race).
51
+ 4. Результат — `updateComponentProps({ options })` на target-поле.
52
+ 5. UI (Select / Combobox) обновляется через сигнал `componentProps`.
53
+
54
+ ### Переиспользуемый оператор
55
+
56
+ Как в монорепо — собираешь свой оператор поверх `onChange`:
57
+
58
+ ```ts
59
+ import { onChange } from '@reformer/core/behaviors';
60
+ import type { ReadonlySignal } from '@reformer/core/behaviors';
61
+
62
+ function loadOptionsOn<TValue, TOption>(
63
+ source: ReadonlySignal<TValue>,
64
+ target: { updateComponentProps(p: Record<string, unknown>): void; reset?: () => void },
65
+ fetcher: (value: TValue, opts?: { signal?: AbortSignal }) => Promise<TOption[]>,
66
+ options: { debounce?: number; resetTarget?: boolean } = {}
67
+ ): void {
68
+ const { debounce = 300, resetTarget = false } = options;
69
+ onChange(
70
+ source,
71
+ async (value, { signal }) => {
72
+ if (resetTarget) target.reset?.();
73
+ if (!value) { target.updateComponentProps({ options: [] }); return; }
74
+ try {
75
+ const data = await fetcher(value, { signal });
76
+ target.updateComponentProps({ options: data });
77
+ } catch {
78
+ target.updateComponentProps({ options: [] });
79
+ }
80
+ },
81
+ { debounce }
82
+ );
83
+ }
84
+
85
+ // Usage:
86
+ loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: true });
87
+ ```
88
+
89
+ ### Common patterns
90
+
91
+ - **Debounce 300–500 мс** — баланс UX и rate-limit.
92
+ - **Reset target value** — при смене source очисти зависимое поле (`model.city = ''`), чтобы не
93
+ остался устаревший выбор.
94
+ - **Loading state** — `updateComponentProps({ loading: true })` пока идёт fetch (компонент должен поддержать).
95
+ - **Cancellation** — `onChange` даёт `{ signal }` (AbortSignal); передавай его в `fetch` и
96
+ игнорируй `AbortError`.
97
+
98
+ ### Initial load (preload at form mount)
99
+
100
+ Для загрузки опций при инициализации формы — external hook + `model.set` + `updateComponentProps`
101
+ (см. [29-async-preload.md](29-async-preload.md)). `onChange` не срабатывает на init (по умолчанию
102
+ `immediate: false`); для запуска сразу передай `{ immediate: true }`.
103
+
104
+ ### See also
105
+
106
+ - [11-async-watchfield.md](11-async-watchfield.md) — общий паттерн `onChange`
107
+ - [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
108
+ - [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)
@@ -0,0 +1,305 @@
1
+ # useFormValidation — Единый выбор стратегии валидации
2
+
3
+ ## Purpose
4
+
5
+ Одна декларативная точка выбора «когда прогонять схему валидации» вместо ручной разводки
6
+ `markAsTouched` + `validateModel` + `revalidateWhen` + `useState(pending)`. Аддитивный слой над
7
+ функциональной схемой (`@reformer/core/validation`): переиспользует раннер схемы как
8
+ ЕДИНСТВЕННЫЙ движок (разнос ошибок по нодам, отмена устаревших прогонов, дедуп по `(model, schema)`),
9
+ второго движка не заводит. Других триггеров валидации нет: у ноды поля собственных валидаторов,
10
+ `updateOn` и `debounce` не бывает — когда запускать проверку, решает только стратегия.
11
+
12
+ Стратегия определяет только момент запуска и раскрытие ошибок — сами правила остаются в отдельной
13
+ `defineValidation<T>(({ model }) => …)`.
14
+
15
+ ## API
16
+
17
+ Три точки входа к одному контроллеру:
18
+
19
+ - **сборка `createForm`** — `validation: { rules?, extras?, strategy, debounce, liveAfterSubmit }`.
20
+ Контроллер собирает уровень формы, армирует `useFormBundle` (в эффекте, не при SSR);
21
+ `bundle.validate()` и `bundle.submission.submit(handler)` — полный прогон с раскрытием замечаний.
22
+ Это путь по умолчанию для формы со сборкой;
23
+ - **React-хук `useFormValidation`** из `@reformer/core/react` — для формы без сборки
24
+ (`createFormFromModel` + своя схема);
25
+ - **headless-фабрика `createFormValidation`** из `@reformer/core/validation` — для SSR / тестов /
26
+ не-React потребителей.
27
+
28
+ ```typescript
29
+ // Сборка (@reformer/core)
30
+ createForm<T>({
31
+ …,
32
+ validation: {
33
+ rules?: ValidationDefinition<T>; // полный набор; без него — правила шагов flow + extras
34
+ extras?: ValidationDefinition<T>;
35
+ strategy?: ValidationStrategyKind; // default 'submit'
36
+ debounce?: number;
37
+ liveAfterSubmit?: 'change' | 'blur';
38
+ },
39
+ });
40
+
41
+ // React (@reformer/core/react)
42
+ function useFormValidation<T>(args: {
43
+ model: FormModel<T>;
44
+ schema: ValidationDefinition<T>; // СТАБИЛЬНАЯ ссылка (module-level const / useMemo)
45
+ strategy?: ValidationStrategyKind; // default 'submit'
46
+ debounce?: number; // мс, для live-фаз (change / live-часть afterFirstSubmit)
47
+ liveAfterSubmit?: 'change' | 'blur'; // default 'change'
48
+ }): {
49
+ submit: () => Promise<boolean>; // попытка отправки: при отказе раскрыта вся форма, afterFirstSubmit → живая фаза
50
+ validate: () => Promise<boolean>; // проверка без отправки: раскрыты проверенные поля, фаза не меняется
51
+ isValidating: boolean;
52
+ };
53
+
54
+ // headless (@reformer/core/validation)
55
+ type ValidationStrategyKind = 'submit' | 'blur' | 'change' | 'afterFirstSubmit';
56
+ interface ValidationStrategyOptions {
57
+ strategy?: ValidationStrategyKind;
58
+ debounce?: number;
59
+ liveAfterSubmit?: 'change' | 'blur';
60
+ }
61
+ interface FormValidationRunOptions {
62
+ touch?: true | 'all'; // true (default) — проверенные поля; 'all' — при отказе ещё и вся форма
63
+ submit?: boolean; // прогон — попытка отправки: afterFirstSubmit → живая фаза
64
+ }
65
+ interface FormValidationController {
66
+ validate(): Promise<boolean>; // нейтральная проверка: touch:true, фазу afterFirstSubmit не трогает; true — только valid
67
+ run(options?: FormValidationRunOptions): Promise<ValidationResult>; // valid | invalid | error | cancelled
68
+ start(): () => void; // армит реактивные подписки (ТОЛЬКО на клиенте) → dispose
69
+ dispose(): void; // снять подписки и таймеры, отменить идущий прогон (он получит cancelled)
70
+ readonly isValidating: boolean;
71
+ readonly validating: ReadonlySignal<boolean>;
72
+ }
73
+ function createFormValidation<T>(
74
+ model: FormModel<T>,
75
+ schema: ValidationDefinition<T>,
76
+ options?: ValidationStrategyOptions,
77
+ ): FormValidationController;
78
+ ```
79
+
80
+ - `useFormValidation` мемоизирует контроллер по
81
+ `[model, schema, strategy, debounce, liveAfterSubmit]` и армит стратегию в `useEffect`
82
+ (SSR-safe). `submit()` = `controller.run({ touch: 'all', submit: true })`, `validate()` =
83
+ `controller.validate()`; обе — стабильные ссылки, отвечают `true` только при `valid`.
84
+ - `createFormValidation` ЧИСТА до `start()` (никаких подписок) → SSR-safe; `start()` возвращает
85
+ `dispose` и арминует триггеры только на клиенте.
86
+
87
+ ### Живая фаза `afterFirstSubmit` — одно правило
88
+
89
+ Живую фазу включает **только попытка отправки**:
90
+
91
+ | Вызов | Попытка отправки | Касание при отказе |
92
+ | --------------------------------------------------------------- | ---------------- | ------------------ |
93
+ | `bundle.submission.submit(handler)` | да | вся форма |
94
+ | `submit()` хука `useFormValidation` | да | вся форма |
95
+ | `controller.run({ touch: 'all', submit: true })` | да | вся форма |
96
+ | `bundle.validate()`, `validate()` хука, `controller.validate()` | нет | проверенные поля |
97
+ | `bundle.validateStep(id)`, `flow.next()` | нет | поля шага |
98
+
99
+ Кнопка «Проверить» — `validate()`: показывает замечания, но форма остаётся в тихой фазе. Отменённая
100
+ отправка (`cancelled`) форму тронутой не помечает.
101
+
102
+ ### Стратегии
103
+
104
+ | strategy | когда прогон | touch | раскрытие замечаний |
105
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------- |
106
+ | `submit` (default) | только `validate()` / `submit()` (в сборке — `bundle.validate()` / `bundle.submission.submit()`) | `true` | всё на submit |
107
+ | `blur` | каждый уход из поля (потеря фокуса), и повторный тоже | `false` | только сблюренные (по `touched`) |
108
+ | `change` | на каждый ввод (+ `debounce`) | `false` | только редактированные (по `dirty`) |
109
+ | `afterFirstSubmit` | тихо до первой попытки отправки, затем live (`liveAfterSubmit: 'change'` \| `'blur'`, default `'change'`); `validate()` фазу не переключает | при отправке — вся форма (при отказе), дальше live — `false` | тихо → раскрыть всё на отправке → далее live |
110
+
111
+ ## Examples
112
+
113
+ ### Сборка — стратегия полем `validation`
114
+
115
+ ```tsx
116
+ import { createForm } from '@reformer/core';
117
+ import { useFormBundle } from '@reformer/core/react';
118
+
119
+ const createRegistration = () =>
120
+ createForm<RegistrationData>({
121
+ initial: { username: '', email: '', password: '' },
122
+ schema: registrationSchema,
123
+ validation: { rules: registrationValidation, strategy: 'afterFirstSubmit', debounce: 400 },
124
+ });
125
+
126
+ function RegistrationForm() {
127
+ const registration = useFormBundle(createRegistration); // армирует живую стратегию в эффекте
128
+ const onSubmit = async (event: React.FormEvent) => {
129
+ event.preventDefault();
130
+ // первая отправка раскрывает все замечания и переводит форму в живую фазу;
131
+ // registration.validate() фазу не переключает
132
+ await registration.submission.submit(api.save);
133
+ };
134
+ /* … */
135
+ }
136
+ ```
137
+
138
+ ### Без сборки — useFormValidation, afterFirstSubmit + debounce
139
+
140
+ Тихо до первой отправки, `submit()` раскрывает все замечания и переводит форму в живую фазу
141
+ (дальше проверка на ввод с задержкой 400 мс). `submit()` сам метит `touched` (при отказе — всю
142
+ форму) — ручной `markAsTouched` не нужен. `validate()` — кнопка «Проверить»: раскрывает замечания
143
+ проверенных полей, а форма остаётся в тихой фазе.
144
+
145
+ ```tsx
146
+ import { useMemo } from 'react';
147
+ import { createModel, createFormFromModel } from '@reformer/core';
148
+ import { useFormValidation } from '@reformer/core/react';
149
+ import { defineValidation, validate } from '@reformer/core/validation';
150
+ import { required, email, minLength } from '@reformer/core/validators';
151
+
152
+ type RegistrationData = { username: string; email: string; password: string };
153
+
154
+ // Схема — стабильный module-level const (иначе ломается дедуп раннера).
155
+ const registrationValidation = defineValidation<RegistrationData>(({ model }) => {
156
+ validate(model.$.username, [required(), minLength(3)]);
157
+ validate(model.$.email, [required(), email()]);
158
+ validate(model.$.password, [required(), minLength(8)]);
159
+ });
160
+
161
+ function RegistrationForm() {
162
+ const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);
163
+ const form = useMemo(() => createFormFromModel({ model }), [model]);
164
+
165
+ const { submit, validate, isValidating } = useFormValidation({
166
+ model,
167
+ schema: registrationValidation,
168
+ strategy: 'afterFirstSubmit',
169
+ debounce: 400,
170
+ });
171
+
172
+ const onSubmit = async (event: React.FormEvent) => {
173
+ event.preventDefault();
174
+ if (await submit()) await api.save(model.get()); // true = нет блокирующих ошибок
175
+ };
176
+
177
+ return (
178
+ <form onSubmit={onSubmit}>
179
+ <FormField control={form.username} />
180
+ <FormField control={form.email} />
181
+ <FormField control={form.password} />
182
+ <button type="button" onClick={() => void validate()}>
183
+ Проверить
184
+ </button>
185
+ <button type="submit" disabled={isValidating}>
186
+ Отправить
187
+ </button>
188
+ </form>
189
+ );
190
+ }
191
+ ```
192
+
193
+ ### Визард — стратегия формы и проверка шага потоком
194
+
195
+ Живая стратегия — свойство формы, а не шага: она задаётся полем `validation` сборки и армируется
196
+ `useFormBundle`. Проверку шага делает поток: «Далее» визарда — `flow.next()`, он прогоняет правила
197
+ текущего шага и не пускает дальше при замечаниях. Отправка — `bundle.submission.submit` (все
198
+ шаги + `extras`). Живую фазу `afterFirstSubmit` включает только она: `flow.next()` и
199
+ `validateStep` фазу не переключают.
200
+
201
+ ```tsx
202
+ const createCredit = () =>
203
+ createForm<CreditForm>({
204
+ model: createCreditModel(),
205
+ schema: creditSchema,
206
+ validation: { extras: crossFieldRules, strategy: 'blur' }, // живая проверка при потере фокуса
207
+ flow: {
208
+ steps: [
209
+ { id: 'loan', rules: loanStep },
210
+ { id: 'applicant', rules: applicantStep },
211
+ { id: 'confirm' }, // без правил
212
+ ],
213
+ },
214
+ });
215
+
216
+ function CreditWizard() {
217
+ const credit = useFormBundle(createCredit);
218
+ return (
219
+ <FormWizard
220
+ form={credit.form}
221
+ flow={credit.flow}
222
+ steps={STEPS}
223
+ onSubmit={() => void credit.submission.submit(api.save)}
224
+ />
225
+ );
226
+ }
227
+ ```
228
+
229
+ Живая фаза прогоняет **полный** набор правил (все шаги + `extras`), а показ замечаний гейтится
230
+ `touched` / `dirty` полей: поля следующих шагов пользователь ещё не трогал — их замечания не видны.
231
+ Прежний per-step механизм (`defineSteps(…, { strategy })` + `useWizardStepValidation`) удалён.
232
+
233
+ ### Headless / SSR / тесты — createFormValidation
234
+
235
+ ```typescript
236
+ import { createFormValidation } from '@reformer/core/validation';
237
+
238
+ const controller = createFormValidation(model, registrationValidation, {
239
+ strategy: 'afterFirstSubmit',
240
+ debounce: 300,
241
+ });
242
+ const dispose = controller.start(); // ТОЛЬКО на клиенте (в React — из useEffect)
243
+ const checked = await controller.validate(); // проверка: замечания проверенных полей, фаза тихая
244
+ const sent = await controller.run({ touch: 'all', submit: true }); // попытка отправки → живая фаза
245
+ dispose();
246
+ ```
247
+
248
+ ## Anti-patterns
249
+
250
+ ```typescript
251
+ // ❌ schema пересоздаётся каждый рендер → контроллер и дедуп раннера ломаются
252
+ function MyForm() {
253
+ const schema = defineValidation<Form>(({ model }) => { … }); // новая ссылка на каждый рендер
254
+ useFormValidation({ model, schema, strategy: 'change' });
255
+ }
256
+
257
+ // ✅ Стабильная ссылка: module-level const (или useMemo с пустыми deps)
258
+ const formValidation = defineValidation<Form>(({ model }) => { … });
259
+ function MyForm() {
260
+ useFormValidation({ model, schema: formValidation, strategy: 'change' });
261
+ }
262
+ ```
263
+
264
+ ```typescript
265
+ // ❌ updateOn / debounce / validators в узле схемы — таких ключей у узла нет, сборка их не читает
266
+ // { model: model.$.email, component: Input, updateOn: 'blur', validators: [required()] }
267
+
268
+ // ✅ Момент запуска — стратегия формы; правила — схема валидации
269
+ // createForm({ …, validation: { rules: formValidation, strategy: 'blur', debounce: 300 } })
270
+ ```
271
+
272
+ ```typescript
273
+ // ❌ Своя стратегия поверх сборки: второй контроллер на ту же модель и правила
274
+ const bundle = useFormBundle(() => createForm({ …, validation: rules }));
275
+ useFormValidation({ model: bundle.model, schema: rules, strategy: 'change' });
276
+
277
+ // ✅ Стратегия — полем validation сборки; армирует useFormBundle
278
+ const bundle = useFormBundle(() => createForm({ …, validation: { rules, strategy: 'change' } }));
279
+ ```
280
+
281
+ ```typescript
282
+ // ❌ start() на сервере — арминует реактивные подписки при SSR
283
+ const controller = createFormValidation(model, schema, { strategy: 'change' });
284
+ controller.start(); // на сервере подписок быть не должно
285
+
286
+ // ✅ Фабрика чиста до start(); армить только на клиенте (useFormValidation делает это в useEffect)
287
+ const controller = createFormValidation(model, schema, { strategy: 'change' });
288
+ if (typeof window !== 'undefined') controller.start();
289
+ ```
290
+
291
+ ```typescript
292
+ // ❌ afterFirstSubmit: ждать живой фазы после проверки — validate() фазу не переключает
293
+ await bundle.validate(); // форма в тихой фазе, ввод не перепроверяется
294
+
295
+ // ✅ Живую фазу включает попытка отправки
296
+ await bundle.submission.submit(api.save); // без сборки — submit() хука useFormValidation
297
+ ```
298
+
299
+ ## See also
300
+
301
+ - [28-submit-and-reset.md](./28-submit-and-reset.md) — submit-флоу и `validateModel` (движок под стратегией)
302
+ - [27-revalidate-when.md](./27-revalidate-when.md) — ручные триггеры перевалидации (низкоуровневый примитив)
303
+ - [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов
304
+ - [13-multi-step.md](./13-multi-step.md) — многошаговая форма: поток `flow`, `bundle.validateStep(id)`
305
+ - [38-form-flow.md](./38-form-flow.md) — уровень формы: `validate`, `submission`, поток шагов