@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,133 @@
1
+ # revalidateWhen — Перевалидация по триггерам
2
+
3
+ ## Purpose
4
+
5
+ `revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
6
+ зависимостей. Применяется, когда правило зависит от значений других полей
7
+ (`amount <= maxAmount`, `confirmPassword === password`, `initialPayment >= propertyValue * 0.2`).
8
+ Валидация — отдельная функция-схема (`@reformer/core/validation`), прогоняемая по требованию через
9
+ `validateModel(model, schema)`. Поэтому ревалидация выражается явным колбэком, а не автоматической
10
+ привязкой к полю: поведение (`revalidateWhen`) лишь _инициирует_ прогон схемы валидации при изменении зависимости.
11
+
12
+ ## API
13
+
14
+ `revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core/model`).
15
+ Его сигнатура НЕ зависит от контракта валидации:
16
+
17
+ ```typescript
18
+ // примитив: возвращает cleanup
19
+ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void): () => void;
20
+
21
+ // DSL: cleanup управляется формой
22
+ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void): void;
23
+ ```
24
+
25
+ `deps` — массив сигналов-триггеров (`model.$.field`). `revalidate` — колбэк (обычно
26
+ `() => void validateModel(model, schema)`, где `schema` — `defineValidation<T>(({ model }) => …)`).
27
+ Вызывается при изменении любой зависимости (НЕ на инициализации), вне effect-контекста.
28
+
29
+ ## Examples
30
+
31
+ ### Базовый сценарий — перевалидация amount при смене maxAmount
32
+
33
+ ```typescript
34
+ import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
35
+ import { validateModel } from '@reformer/core/validation';
36
+
37
+ export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
38
+ revalidateWhen([model.$.maxAmount], () => {
39
+ void validateModel(model, paymentSchema);
40
+ });
41
+ });
42
+ ```
43
+
44
+ Пример: `revalidateWhen([model.$.maxAmount], () => void validateModel(model, paymentSchema))`.
45
+ Схема `paymentSchema` — стабильный module-level `const` (важно для отмены устаревших прогонов
46
+ в `validateModel`), где правило amount навешано оператором `cross` области схемы:
47
+ `cross(model.$.amount, amountVsMax)`.
48
+
49
+ ### Несколько триггеров
50
+
51
+ ```typescript
52
+ import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
53
+ import { validateModel } from '@reformer/core/validation';
54
+
55
+ export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) => {
56
+ // initialPayment зависит и от propertyValue, и от loanAmount
57
+ revalidateWhen([model.$.propertyValue, model.$.loanAmount], () => {
58
+ void validateModel(model, mortgageSchema);
59
+ });
60
+ });
61
+ ```
62
+
63
+ ### Парная перевалидация — confirmPassword
64
+
65
+ ```typescript
66
+ import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
67
+ import { validateModel } from '@reformer/core/validation';
68
+
69
+ // правило совпадения — cross-field в схеме: cross(model.$.confirmPassword, passwordsMatch),
70
+ // где passwordsMatch получает снимок формы (см. 03-api-signatures.md)
71
+ export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ model }) => {
72
+ // при смене password перевалидируем схему (confirm перепроверится)
73
+ revalidateWhen([model.$.password], () => {
74
+ void validateModel(model, registrationSchema);
75
+ });
76
+ });
77
+ ```
78
+
79
+ ### Как примитив (вне defineFormBehavior)
80
+
81
+ ```typescript
82
+ import { revalidateWhen } from '@reformer/core/model';
83
+ import { validateModel } from '@reformer/core/validation';
84
+
85
+ const stop = revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
86
+ ```
87
+
88
+ ## Anti-patterns
89
+
90
+ ```typescript
91
+ // ❌ Триггер == поле, которое и так меняется само
92
+ revalidateWhen([model.$.amount], () => validateModel(model, schema));
93
+ // amount и так валидируется при собственном изменении в общем прогоне
94
+
95
+ // ✅ Триггеры — ДРУГИЕ поля, от которых зависит правило amount
96
+ revalidateWhen([model.$.maxAmount, model.$.discount], () => validateModel(model, schema));
97
+ ```
98
+
99
+ ```typescript
100
+ // ❌ Правило только через revalidateWhen, но в схеме нет зависимости от триггера
101
+ revalidateWhen([model.$.maxAmount], () => validateModel(model, schema));
102
+ // а валидатор amount — статичный max(1000), не читает maxAmount
103
+
104
+ // ✅ Сначала cross-field правило (получает снимок формы) в схеме валидации, потом revalidateWhen
105
+ const amountVsMax = (form: Form): ValidationIssue | null =>
106
+ form.amount != null && form.maxAmount != null && form.amount > form.maxAmount
107
+ ? { code: 'tooBig' }
108
+ : null;
109
+
110
+ const schema = defineValidation<Form>(({ model, cross }) => {
111
+ cross(model.$.amount, amountVsMax); // навешиваем правило на поле amount
112
+ });
113
+ revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
114
+ ```
115
+
116
+ ## Troubleshooting
117
+
118
+ **Q: Ошибка target не пропадает после изменения триггера.**
119
+ A: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, check)`,
120
+ где `check` получает снимок формы; (2) в `deps` передан именно сигнал (`model.$.trigger`);
121
+ (3) колбэк вызывает `validateModel` (он разносит ошибки по нодам формы).
122
+
123
+ **Q: Как перевалидировать только одно поле, а не всю схему?**
124
+ A: Схема — обычная функция, поэтому опиши стабильную под-схему только для этого поля и прогони её:
125
+ `validateModel(model, oneFieldSchema)`, где
126
+ `const oneFieldSchema = defineValidation<Form>(({ model, cross }) => cross(model.$.amount, amountVsMax))`.
127
+ Раннер выставит/погасит ошибки только затронутых полей (гашение накапливается на пару `(model, schema)`).
128
+
129
+ ## See also
130
+
131
+ - [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross` области схемы и раннер `validateModel`
132
+ - [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
133
+ - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения
@@ -0,0 +1,409 @@
1
+ # Submit и Reset — Жизненный цикл отправки формы
2
+
3
+ ## Purpose
4
+
5
+ Канонический submit-флоу — один вызов отправки сборки: `bundle.submission.submit(handler)`.
6
+ Отправка — отдельный объект сборки (`FormSubmission`): она прогоняет все правила один раз,
7
+ разносит замечания по нодам и зовёт обработчик со значениями модели (`model.get()`) только при
8
+ статусе `valid`. Результат — объект `SubmitResult`, по которому отличают отправку (`submitted`),
9
+ замечания в форме (`invalid`), сбой проверки (`error`), отмену (`cancelled`) и исключение
10
+ обработчика (`failed`). После успеха — `model.reset()` (к initial-снимку) либо
11
+ `model.captureInitial()` (сохранённые значения — новая точка отсчёта).
12
+
13
+ Валидация — **отдельный слой** (`@reformer/core/validation`, схема через `defineValidation`): она
14
+ НЕ входит в схему формы и уходит в сборку полем `validation` (шаги — `flow`). Без сборки тот же
15
+ результат даёт раннер `validateModel(model, schema)`.
16
+
17
+ ## API
18
+
19
+ ```typescript
20
+ // Уровень формы — сборка createForm (см. 38-form-flow.md); отправка — объект bundle.submission
21
+ bundle.submission.submit<R>(handler: (values: T) => R | Promise<R>): Promise<SubmitResult<R>>;
22
+ type SubmitResult<R> =
23
+ | { status: 'submitted'; result: R } // обработчик отработал
24
+ | { status: 'invalid' | 'error' | 'cancelled'; validation: ValidationResult } // не вызывался
25
+ | { status: 'failed'; error: unknown }; // обработчик бросил исключение
26
+ bundle.submission.submitting: ReadonlySignal<boolean>; // работает обработчик
27
+ bundle.validate(): Promise<ValidationResult>; // проверить всю форму без отправки
28
+ bundle.validating: ReadonlySignal<boolean>; // идёт проверка
29
+ bundle.dispose(): void; // снять сборку: поведение, подписки нод, живую проверку; модель остаётся
30
+
31
+ // Отправка без сборки (визард в разметке без createForm) — без проверки
32
+ createSubmission<T>({ check: undefined, values: () => T }): FormSubmission<T>;
33
+
34
+ // Модель (источник истины значений):
35
+ interface ModelApi<T> {
36
+ get(): T; // снимок значений — его получает обработчик submit
37
+ set(value: T): void; // полная установка (все ключи T); при нехватке ключей — dev-предупреждение
38
+ patch(value: DeepPartial<T>): void; // частичное слияние: вложенные группы тоже частично, массив — целиком;
39
+ // ключ со значением undefined не трогается
40
+ isDirty(): boolean; // отличаются ли значения от initial-снимка (цели compute не в счёт)
41
+ reset(): void; // вернуть к initial-снимку (цели compute не пишет — их пересчитает поведение)
42
+ captureInitial(): void; // зафиксировать текущие как новый initial
43
+ }
44
+
45
+ // Раннер без сборки — headless: находит ноды по сигналам модели, разносит по ним замечания,
46
+ // отменяет устаревшие прогоны той же (model, schema):
47
+ validateModel<T>(model: FormModel<T>, schema: ValidationDefinition<T>, options?: { touch?: boolean }): Promise<boolean>
48
+ // true = статус 'valid': блокирующих замечаний нет (severity:'warning' не блокирует, но показывается).
49
+ // false = 'invalid', 'error' (правило не удалось проверить — на поле замечание ruleFailed) либо 'cancelled'.
50
+ runValidation<T>(model, schema): Promise<ValidationResult> // только сбор
51
+ applyValidationResult(result, options?: { touch?: boolean }): void // разнос по нодам
52
+
53
+ // Нода поля/формы (точечная работа с UI-состоянием):
54
+ form.markAsTouched(); // тронуть все поля (показать замечания)
55
+ form.clearIssues(); // снять замечания со всех нод
56
+ form.<field>.setIssues([{ code, params }]); // серверная/бизнес-ошибка в конкретное поле
57
+ form.<field>.clearIssues();
58
+ form.reset(); // к точке отсчёта модели + чистые ноды (замечания, touched, dirty)
59
+ ```
60
+
61
+ ### Что отправка делает с полями
62
+
63
+ | Итог отправки | Касание (`touched`) |
64
+ | --------------------- | --------------------------------------------------------------------- |
65
+ | `invalid`, `error` | вся форма — видны замечания и полей, которых правила не касались |
66
+ | `cancelled` | форма тронутой не помечается: прогон устарел, замечаний итог не несёт |
67
+ | `submitted`, `failed` | только поля, которые проверили правила |
68
+
69
+ Отправка — попытка отправки: стратегия `afterFirstSubmit` после неё переходит в живую фазу
70
+ (`bundle.validate()` фазу не трогает, см. `33-validation-strategy.md`).
71
+
72
+ ## Examples
73
+
74
+ ### Базовый submit-handler
75
+
76
+ ```tsx
77
+ import { createForm, type FormModel, type FormSchemaNode } from '@reformer/core';
78
+ import { useFormBundle } from '@reformer/core/react';
79
+ import { defineValidation, validate } from '@reformer/core/validation';
80
+ import { required, email, minLength } from '@reformer/core/validators';
81
+
82
+ interface RegistrationFormData {
83
+ username: string;
84
+ email: string;
85
+ password: string;
86
+ }
87
+
88
+ // Валидация — отдельная схема над моделью; стабильная module-level `const`-ссылка.
89
+ const registrationRules = defineValidation<RegistrationFormData>(({ model }) => {
90
+ validate(model.$.username, [required({ code: 'username.required' }), minLength(3)]);
91
+ validate(model.$.email, [required({ code: 'email.required' }), email()]);
92
+ validate(model.$.password, [required({ code: 'password.required' }), minLength(8)]);
93
+ });
94
+
95
+ const createRegistration = () =>
96
+ createForm<RegistrationFormData>({
97
+ initial: { username: '', email: '', password: '' },
98
+ schema: registrationSchema, // (model) => дерево узлов, без правил
99
+ validation: registrationRules,
100
+ });
101
+
102
+ async function register(values: RegistrationFormData): Promise<Response> {
103
+ return fetch('/api/v1/auth/register', {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify(values),
107
+ });
108
+ }
109
+
110
+ function RegistrationForm() {
111
+ const registration = useFormBundle(createRegistration);
112
+ const { form, model } = registration;
113
+
114
+ const handleSubmit = async (event: React.FormEvent) => {
115
+ event.preventDefault();
116
+ // Один прогон правил: при замечаниях обработчик не вызывается, вся форма помечена тронутой.
117
+ const outcome = await registration.submission.submit(register);
118
+ if (outcome.status === 'submitted' && outcome.result.ok) model.reset(); // чистый старт
119
+ };
120
+
121
+ return (
122
+ <form onSubmit={handleSubmit}>
123
+ <FormField control={form.username} testId="username" />
124
+ <FormField control={form.email} testId="email" />
125
+ <FormField control={form.password} testId="password" />
126
+ <button type="submit">Отправить</button>
127
+ </form>
128
+ );
129
+ }
130
+ ```
131
+
132
+ ### Submit с разбором результата и серверными замечаниями
133
+
134
+ ```tsx
135
+ async function handleSubmit(event: React.FormEvent) {
136
+ event.preventDefault();
137
+
138
+ const outcome = await registration.submission.submit(async (values) => {
139
+ const response = await api.register(values);
140
+ if (!response.success) {
141
+ // Серверная бизнес-ошибка — НЕ сбрасываем форму, кладём замечание в поле
142
+ registration.form.username.setIssues([{ code: 'username.taken' }]);
143
+ }
144
+ return response;
145
+ });
146
+
147
+ switch (outcome.status) {
148
+ case 'submitted':
149
+ if (outcome.result.success) {
150
+ registration.model.reset();
151
+ navigate('/welcome');
152
+ }
153
+ break;
154
+ case 'invalid': // замечания уже на полях
155
+ break;
156
+ case 'error': // async-правило не ответило — outcome.validation.failures
157
+ showToast('Не удалось проверить форму, повторите попытку');
158
+ break;
159
+ case 'cancelled': // форму правили, пока шла проверка, либо отправку сменила следующая
160
+ break; // замечаний нет — пользователь отправит ещё раз
161
+ case 'failed': // обработчик бросил исключение (сеть) — значения остаются
162
+ showToast(`Ошибка сети: ${String(outcome.error)}`);
163
+ break;
164
+ }
165
+ }
166
+ ```
167
+
168
+ Текст кода `username.taken` — запись в словаре формы (`defineIssueMessages`), см.
169
+ `36-issue-messages.md`.
170
+
171
+ ### Значения для сервера
172
+
173
+ Обработчик получает `model.get()` — значения модели целиком: вычисляемые поля и поля, выключенные
174
+ `enableWhen` (их правила не считаются, но данные принадлежат модели). Поля, которых сервер не
175
+ ждёт, отрезает сам обработчик: удобно держать ввод и расчёт разными типами
176
+ (`CreditApplication` и `CreditApplicationDerived`, форма — их пересечение) и функцию
177
+ `toPayload(values): CreditApplication`, убирающую вычисляемые ключи.
178
+
179
+ ```typescript
180
+ const outcome = await credit.submission.submit((values) => api.send(toApplicationPayload(values)));
181
+ ```
182
+
183
+ ### Reset с подтверждением
184
+
185
+ ```tsx
186
+ function ActionButtons({ model }: { model: FormModel<RegistrationFormData> }) {
187
+ const handleReset = () => {
188
+ if (!model.isDirty()) return; // нечего сбрасывать
189
+ if (confirm('Очистить форму? Несохранённые изменения будут потеряны.')) {
190
+ model.reset();
191
+ }
192
+ };
193
+
194
+ const clearJustPassword = () => {
195
+ model.password = ''; // сброс одного поля — прямая запись в модель
196
+ };
197
+
198
+ return (
199
+ <>
200
+ <button type="button" onClick={handleReset}>Очистить</button>
201
+ <button type="button" onClick={clearJustPassword}>Очистить только пароль</button>
202
+ </>
203
+ );
204
+ }
205
+ ```
206
+
207
+ ### Без сборки — раннер (server action / Node)
208
+
209
+ Раннер headless — работает без UI-компонентов, только с моделью и схемой:
210
+
211
+ ```typescript
212
+ import { createModel } from '@reformer/core';
213
+ import { runValidation } from '@reformer/core/validation';
214
+
215
+ async function processFormPayload(rawData: Partial<MyForm>) {
216
+ const model = createModel<MyForm>(initialValues);
217
+ model.patch(rawData); // частичный load данных
218
+
219
+ // Полный результат: статус и замечания по ручкам полей — без нод формы
220
+ const result = await runValidation(model, myFormRules);
221
+ if (result.status !== 'valid') return { ok: false as const, status: result.status };
222
+ return { ok: true as const, data: model.get() };
223
+ }
224
+ ```
225
+
226
+ ### Снятие сборки — `bundle.dispose()`
227
+
228
+ Владелец сборки — тот, кто вызвал `createForm`. Вне React (тесты, сервер, своя оболочка) сборку
229
+ снимают, когда форма больше не нужна: `bundle.dispose()` снимает поведение формы, подписки нод и
230
+ живую проверку, а идущие проверки — полная, шага, отправки — получают `cancelled`, и их замечания
231
+ на ноды не ложатся. Значения модели остаются. Вызов идемпотентен.
232
+
233
+ ```typescript
234
+ const registration = createRegistration();
235
+ try {
236
+ const outcome = await registration.submission.submit(register);
237
+ expect(outcome.status).toBe('invalid');
238
+ } finally {
239
+ registration.dispose();
240
+ }
241
+ ```
242
+
243
+ - `level.dispose()` у `FormLevel` (уровень без сборки, `createFormLevel`) снимает только уровень —
244
+ живую проверку и идущие проверки; форму не трогает.
245
+ - Форму из `createFormFromModel` снимает `disposeForm(form)` — поведение и подписки нод.
246
+ - `useFormBundle` сборку при размонтировании не снимает.
247
+
248
+ ## Anti-patterns
249
+
250
+ ```typescript
251
+ // ❌ Отправка в обход проверки — правила не прогнаны, замечания не показаны
252
+ await api.send(bundle.model.get());
253
+
254
+ // ✅ Проверка и отправка одним вызовом
255
+ await bundle.submission.submit(api.send);
256
+ ```
257
+
258
+ ```typescript
259
+ // ❌ Результат как boolean — объект всегда truthy
260
+ if (await bundle.submission.submit(api.send)) navigate('/done');
261
+
262
+ // ✅ Статус результата
263
+ const outcome = await bundle.submission.submit(api.send);
264
+ if (outcome.status === 'submitted') navigate('/done');
265
+ ```
266
+
267
+ ```typescript
268
+ // ❌ Методы проверки и отправки на ноде — удалены
269
+ await form.validate();
270
+ await form.submit(api.send);
271
+
272
+ // ✅ Проверка — у сборки, отправка — её объект submission (см. 17-nonexistent-api.md)
273
+ await bundle.validate();
274
+ await bundle.submission.submit(api.send);
275
+ ```
276
+
277
+ ```typescript
278
+ // ❌ Чтение результата без await — сабмитим по неполному вердикту
279
+ const handleSubmit = (event) => {
280
+ event.preventDefault();
281
+ validateModel(model, schema); // Promise проигнорирован, async-правила не дождались
282
+ send(model.get());
283
+ };
284
+
285
+ // ✅ bundle.submission.submit дожидается async-правил сам
286
+ const handleSubmit = async (event) => {
287
+ event.preventDefault();
288
+ await bundle.submission.submit(send);
289
+ };
290
+ ```
291
+
292
+ ```typescript
293
+ // ❌ Правила в узле схемы формы — ключа validators у узла нет, схема формы правил не несёт
294
+ createForm({
295
+ initial,
296
+ schema: (model) => ({ children: [{ model: model.$.username, component: Input, validators: [required()] }] }),
297
+ });
298
+
299
+ // ✅ Валидация — отдельная схема, уходит в сборку полем validation
300
+ const rules = defineValidation<T>(({ model }) => {
301
+ validate(model.$.username, [required()]);
302
+ });
303
+ createForm({ initial, schema, validation: rules });
304
+ ```
305
+
306
+ ```typescript
307
+ // ❌ model.reset() до ответа сервера — потеря данных при ошибке
308
+ await api.send(model.get());
309
+ model.reset();
310
+
311
+ // ✅ Reset только после успеха — внутри обработчика либо по статусу 'submitted'
312
+ const outcome = await bundle.submission.submit(async (values) => {
313
+ await api.send(values);
314
+ bundle.model.reset();
315
+ });
316
+ ```
317
+
318
+ ```typescript
319
+ // ❌ Сборка в тесте или на сервере брошена — поведение и живая проверка продолжают работать
320
+ const bundle = createForm<RegistrationFormData>({ initial, schema, validation: rules });
321
+ await bundle.submission.submit(register);
322
+
323
+ // ✅ Владелец сборки снимает её сам; модель при этом остаётся
324
+ bundle.dispose();
325
+ ```
326
+
327
+ ```typescript
328
+ // ❌ Сброс полей по очереди — многословно
329
+ model.username = '';
330
+ model.email = '';
331
+ model.password = '';
332
+
333
+ // ✅ model.reset() — к initial-снимку одним вызовом
334
+ model.reset();
335
+ ```
336
+
337
+ ## Troubleshooting
338
+
339
+ **Q: После `reset()` в UI остались старые замечания.**
340
+ A: `model.reset()` меняет значения; замечания в нодах чистит валидация. Перезапусти
341
+ `bundle.validate()` после reset (валидные поля погаснут сами), очисти напрямую —
342
+ `form.clearIssues()` / `form.<field>.clearIssues()`, либо сбрось ноды — `form.reset()`.
343
+
344
+ **Q: `submit` вызвал обработчик, но в поле висит замечание.**
345
+ A: Это правило с `severity: 'warning'` — оно показывается, но отправку не блокирует. Блокируют только
346
+ замечания без `severity` (по умолчанию `'error'`). Чтобы не пускать submit при warning — проверь
347
+ `(await bundle.validate()).issues` сам до отправки.
348
+
349
+ **Q: `submit` вернул `status: 'error'`, а замечаний правил в форме нет — на поле «Не удалось проверить поле».**
350
+ A: Async-правило не вернуло результат (сеть, исключение). Сбой блокирует: на поле — замечание
351
+ `{ code: 'ruleFailed' }`. Причина — в `outcome.validation.failures`. Повторная отправка после
352
+ восстановления сети снимет замечание.
353
+
354
+ **Q: `submit` вернул `status: 'cancelled'`, обработчик не вызван, замечаний нет.**
355
+ A: Проверку отправки сменила следующая (вторая отправка, живой прогон стратегии `change`) либо
356
+ модель изменилась, пока проверка шла. Пока отвечает async-правило, форма не заперта
357
+ (`submission.submitting` ещё `false`): пользователь поправил поле, пришла загрузка, поведение записало
358
+ значение после таймера или запроса — правила этих значений не видели, и отправлять их нельзя.
359
+ Форма тронутой не помечается. Отправку повторяют: следующий прогон проверит уже новые значения.
360
+ Отменой кончается и отправка, идущая в момент `bundle.dispose()`. Чтобы правок не было — запри
361
+ форму на время проверки по `bundle.validating`. Вызов в одном тике со сборкой формы или правкой
362
+ поля отправку не отменяет: проверка сначала дожидается отложенных записей поведения (колбэк
363
+ `onChange`, сброс `resetOnDisable`), и правила, и обработчик видят значения после них.
364
+
365
+ **Q: В тесте запрос async-правила не уходит сразу после `validate()` / `submit()`.**
366
+ A: Перед вызовом в том же тике правили модель, и поведение ещё не дописало свои отложенные
367
+ записи — проверка ждёт их, правила стартуют микротаском позже (`validating` поднят сразу).
368
+ Дождись записей поведения до вызова (`await Promise.resolve()` / свой `settle()`) либо читай
369
+ запрос правила после `await`. Без отложенных записей правила исполняются синхронно в момент
370
+ вызова.
371
+
372
+ **Q: `reset()` не возвращает данные, загруженные с сервера.**
373
+ A: `reset()` возвращает к initial-снимку (значения на момент `createModel`). У модели, которую
374
+ `createForm` создал из `initial`, снимок — собранная форма: в него входят правки `seed` и первые
375
+ записи поведения (копия `syncFields`, нормализация `transformValue`) — сборка выполняет их до
376
+ возврата, поэтому пара `syncFields` сбрасывается согласованной. `set/patch` НЕ меняют initial. Чтобы сделать
377
+ загруженные данные новой «точкой отсчёта» — вызови `model.captureInitial()` после загрузки.
378
+
379
+ **Q: Что сброс и `isDirty()` делают с вычисляемыми полями?**
380
+ A: Ничего. Цели `compute` / `computeFrom` и поля с `markDerived` (например, номер заявки от
381
+ сервера) `model.reset()` и `form.reset()` не пишут — так же, как `set` и `patch`: значение
382
+ пересчитает поведение, когда сброс вернёт его источники. В `isDirty()` такие поля не входят,
383
+ поэтому форма без единой правки не считается изменённой, даже если поведение уже посчитало ставку.
384
+ Явная запись — `model.rate = 0` или `form.rate.reset(0)` — пишет как обычно. Исключение —
385
+ `enableWhen(…, { resetOnDisable: true })`: выключенное поле возвращается к значению создания, даже
386
+ если оно цель `compute` (её `compute` обязан нести то же условие `when`).
387
+
388
+ **Q: Чем `form.reset()` отличается от `model.reset()`?**
389
+ A: Точка отсчёта у них одна — initial-снимок модели либо последний `model.captureInitial()`
390
+ (загруженные данные). `form.reset()` — сброс для пользователя (кнопка «Сбросить»): значения к точке
391
+ отсчёта и чистые ноды — замечания, touched, dirty; после него и форма, и модель не изменены.
392
+ `model.reset()` — сброс значений модели: ноды не трогает, замечания погаснут при следующей
393
+ проверке. Исключение — `enableWhen({ resetOnDisable: true })`: выключенное поле очищается к
394
+ значению, с которым нода создана, а не к загруженному.
395
+
396
+ **Q: Disabled-поля участвуют в submit-данных?**
397
+ A: Да. Обработчик получает `model.get()` — модель целиком, с выключенными полями: их правила не
398
+ считаются, но данные принадлежат модели. Фильтруй сам либо используй
399
+ `enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial. Замечания
400
+ отключённых полей сняты, в проверку они не входят.
401
+
402
+ ## See also
403
+
404
+ - [38-form-flow.md](./38-form-flow.md) — уровень формы: `validate`, `validateStep`, `submission`, поток шагов
405
+ - [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
406
+ - [13-multi-step.md](./13-multi-step.md) — пошаговая форма: поток `flow`, визард
407
+ - [27-revalidate-when.md](./27-revalidate-when.md) — мост «поведение → валидация»: `revalidateWhen([deps], () => void validateModel(model, schema))`
408
+ - [36-issue-messages.md](./36-issue-messages.md) — тексты замечаний по коду
409
+ - [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки