@reformer/core 11.0.0-beta.7 → 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 (226) hide show
  1. package/README.md +90 -68
  2. package/dist/behaviors-value-HA_Q03hc.js +108 -0
  3. package/dist/behaviors.js +382 -217
  4. package/dist/create-model-BvcbNPgL.js +30 -0
  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 +42 -8
  9. package/dist/form/behaviors/context.d.ts +37 -4
  10. package/dist/form/behaviors/index.d.ts +6 -4
  11. package/dist/form/behaviors/internals.d.ts +9 -3
  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 +78 -0
  15. package/dist/form/behaviors/types.d.ts +25 -5
  16. package/dist/form/create-form.d.ts +57 -70
  17. package/dist/form/flow/flow.d.ts +87 -0
  18. package/dist/form/form-bundle.d.ts +153 -0
  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 +20 -20
  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 +153 -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 +19 -92
  42. package/dist/form/types/field-config.d.ts +27 -0
  43. package/dist/form/types/index.d.ts +5 -57
  44. package/dist/form/types/nodes.d.ts +159 -0
  45. package/dist/form/types/schema-node.d.ts +147 -46
  46. package/dist/form/types/validation-schema.d.ts +23 -27
  47. package/dist/form/validation/context.d.ts +46 -6
  48. package/dist/form/validation/index.d.ts +9 -8
  49. package/dist/form/validation/operators.d.ts +71 -17
  50. package/dist/form/validation/run.d.ts +93 -18
  51. package/dist/form/validation/strategy.d.ts +41 -13
  52. package/dist/form/validation/types.d.ts +74 -19
  53. package/dist/form/validators/date-utils.d.ts +13 -1
  54. package/dist/form/validators/email.d.ts +8 -7
  55. package/dist/form/validators/file-type.d.ts +8 -7
  56. package/dist/form/validators/file-utils.d.ts +2 -1
  57. package/dist/form/validators/future-date.d.ts +8 -7
  58. package/dist/form/validators/index.d.ts +1 -1
  59. package/dist/form/validators/integer.d.ts +8 -7
  60. package/dist/form/validators/is-date.d.ts +11 -9
  61. package/dist/form/validators/is-number.d.ts +11 -9
  62. package/dist/form/validators/max-age.d.ts +13 -9
  63. package/dist/form/validators/max-date.d.ts +8 -7
  64. package/dist/form/validators/max-file-size.d.ts +8 -7
  65. package/dist/form/validators/max-files.d.ts +8 -7
  66. package/dist/form/validators/max-length.d.ts +14 -7
  67. package/dist/form/validators/max-total-file-size.d.ts +8 -7
  68. package/dist/form/validators/max.d.ts +10 -8
  69. package/dist/form/validators/min-age.d.ts +12 -9
  70. package/dist/form/validators/min-date.d.ts +8 -7
  71. package/dist/form/validators/min-file-size.d.ts +8 -7
  72. package/dist/form/validators/min-files.d.ts +11 -9
  73. package/dist/form/validators/min-length.d.ts +14 -7
  74. package/dist/form/validators/min.d.ts +10 -8
  75. package/dist/form/validators/multiple-of.d.ts +10 -8
  76. package/dist/form/validators/non-negative.d.ts +8 -7
  77. package/dist/form/validators/non-zero.d.ts +8 -7
  78. package/dist/form/validators/past-date.d.ts +8 -7
  79. package/dist/form/validators/pattern.d.ts +9 -8
  80. package/dist/form/validators/phone.d.ts +8 -7
  81. package/dist/form/validators/required.d.ts +11 -9
  82. package/dist/form/validators/url.d.ts +20 -13
  83. package/dist/form/validators/validation-issue.d.ts +8 -0
  84. package/dist/i18n/descriptor.d.ts +1 -1
  85. package/dist/i18n/en.json.d.ts +5 -1
  86. package/dist/i18n/locale-store.d.ts +7 -0
  87. package/dist/i18n/message-format.d.ts +24 -4
  88. package/dist/i18n/ru.json.d.ts +5 -1
  89. package/dist/i18n/translator.d.ts +11 -0
  90. package/dist/i18n/validation-message.d.ts +42 -7
  91. package/dist/i18n.d.ts +7 -12
  92. package/dist/i18n.js +66 -283
  93. package/dist/index.d.ts +16 -7
  94. package/dist/index.js +958 -2798
  95. package/dist/loader-BWkigpVe.js +52 -0
  96. package/dist/locale/en.js +1 -1
  97. package/dist/locale/en.json +4 -1
  98. package/dist/locale/ru.js +4 -1
  99. package/dist/locale/ru.json +4 -1
  100. package/dist/locale-CG54AU2Q.js +219 -0
  101. package/dist/locale.js +1 -1
  102. package/dist/model/behaviors-value.d.ts +24 -6
  103. package/dist/model/create-model.d.ts +2 -2
  104. package/dist/model/derived-registry.d.ts +1 -1
  105. package/dist/model/index.d.ts +16 -5
  106. package/dist/model/model-nodes.d.ts +138 -8
  107. package/dist/model/model-signals-proxy.d.ts +11 -5
  108. package/dist/model/model-value-proxy.d.ts +84 -3
  109. package/dist/model/safe-effect.d.ts +33 -1
  110. package/dist/model/types.d.ts +70 -17
  111. package/dist/model.js +21 -17
  112. package/dist/node-kind-C-r6ZMdo.js +15 -0
  113. package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
  114. package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
  115. package/dist/platforms/react/form-bundle-context.d.ts +27 -0
  116. package/dist/platforms/react/hooks/types.d.ts +57 -38
  117. package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
  118. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  119. package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
  120. package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
  121. package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
  122. package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
  123. package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
  124. package/dist/platforms/react/i18n/context.d.ts +6 -1
  125. package/dist/platforms/react/i18n/hooks.d.ts +16 -10
  126. package/dist/platforms/react/i18n/provider.d.ts +9 -2
  127. package/dist/platforms/react/index.d.ts +7 -4
  128. package/dist/react.d.ts +22 -0
  129. package/dist/react.js +505 -0
  130. package/dist/runtime-token.d.ts +1 -2
  131. package/dist/safe-effect-BbtX3KuN.js +643 -0
  132. package/dist/schema-controller-CpiO2JoK.js +143 -0
  133. package/dist/signals.d.ts +15 -9
  134. package/dist/signals.js +11 -6
  135. package/dist/strategy-CoFSemuP.js +375 -0
  136. package/dist/type-guards-D-N_5Ddh.js +19 -0
  137. package/dist/validation-issue-B9mjI8jz.js +7 -0
  138. package/dist/validation-message-Ced9MGxs.js +194 -0
  139. package/dist/validation.js +9 -8
  140. package/dist/validators/email.js +5 -5
  141. package/dist/validators/future-date.js +8 -8
  142. package/dist/validators/integer.js +3 -3
  143. package/dist/validators/is-date.js +4 -4
  144. package/dist/validators/is-number.js +4 -4
  145. package/dist/validators/max-age.js +8 -9
  146. package/dist/validators/max-date.js +8 -8
  147. package/dist/validators/max-length.js +7 -7
  148. package/dist/validators/max.js +4 -4
  149. package/dist/validators/min-age.js +10 -8
  150. package/dist/validators/min-date.js +3 -3
  151. package/dist/validators/min-length.js +5 -5
  152. package/dist/validators/min.js +4 -4
  153. package/dist/validators/multiple-of.js +11 -7
  154. package/dist/validators/non-negative.js +3 -3
  155. package/dist/validators/non-zero.js +3 -3
  156. package/dist/validators/past-date.js +8 -8
  157. package/dist/validators/pattern.js +5 -5
  158. package/dist/validators/phone.js +11 -9
  159. package/dist/validators/required.js +2 -2
  160. package/dist/validators/url.js +48 -12
  161. package/dist/validators.js +17 -17
  162. package/docs/llms/01-api-reference.md +60 -34
  163. package/docs/llms/02-quick-start.md +111 -80
  164. package/docs/llms/03-api-signatures.md +368 -153
  165. package/docs/llms/04-common-patterns.md +58 -56
  166. package/docs/llms/05-common-mistakes.md +69 -44
  167. package/docs/llms/06-troubleshooting.md +14 -9
  168. package/docs/llms/07-complete-import.md +74 -43
  169. package/docs/llms/08-form-types.md +11 -4
  170. package/docs/llms/09-formschema.md +131 -83
  171. package/docs/llms/10-arrays.md +119 -69
  172. package/docs/llms/11-async-watchfield.md +2 -2
  173. package/docs/llms/13-multi-step.md +134 -63
  174. package/docs/llms/14-extended-mistakes.md +20 -17
  175. package/docs/llms/15-project-structure.md +221 -74
  176. package/docs/llms/16-ui-components.md +55 -42
  177. package/docs/llms/17-nonexistent-api.md +199 -32
  178. package/docs/llms/18-conditional-fields.md +66 -32
  179. package/docs/llms/19-reading-values.md +15 -3
  180. package/docs/llms/20-compute-vs-watch.md +16 -10
  181. package/docs/llms/21-array-operations.md +76 -26
  182. package/docs/llms/22-cycle-detection.md +1 -1
  183. package/docs/llms/23-copy-from.md +2 -2
  184. package/docs/llms/24-sync-fields.md +6 -5
  185. package/docs/llms/25-reset-when.md +14 -7
  186. package/docs/llms/26-transform-value.md +4 -4
  187. package/docs/llms/27-revalidate-when.md +16 -15
  188. package/docs/llms/28-submit-and-reset.md +276 -128
  189. package/docs/llms/29-async-preload.md +40 -30
  190. package/docs/llms/30-type-safety-recipes.md +105 -51
  191. package/docs/llms/31-async-validator-debounce.md +96 -53
  192. package/docs/llms/33-validation-strategy.md +178 -66
  193. package/docs/llms/34-i18n.md +69 -43
  194. package/docs/llms/35-unified-contract.md +307 -0
  195. package/docs/llms/36-issue-messages.md +284 -0
  196. package/docs/llms/37-compiled-schema.md +332 -0
  197. package/docs/llms/38-form-flow.md +397 -0
  198. package/llms-index.json +1 -1
  199. package/llms.txt +6968 -3278
  200. package/package.json +23 -4
  201. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  202. package/dist/create-model-B8U-COv-.js +0 -103
  203. package/dist/date-utils-dTT_x_Be.js +0 -31
  204. package/dist/form/create-core-form.d.ts +0 -70
  205. package/dist/form/factories/index.d.ts +0 -6
  206. package/dist/form/factories/node-factory.d.ts +0 -189
  207. package/dist/form/form-submitter.d.ts +0 -121
  208. package/dist/form/nodes/array-node.d.ts +0 -276
  209. package/dist/form/nodes/field-node.d.ts +0 -297
  210. package/dist/form/nodes/form-node.d.ts +0 -326
  211. package/dist/form/nodes/group-node.d.ts +0 -284
  212. package/dist/form/nodes/model-array-node.d.ts +0 -121
  213. package/dist/form/nodes/subscription-manager.d.ts +0 -180
  214. package/dist/form/status-machine.d.ts +0 -115
  215. package/dist/form/types/deep-schema.d.ts +0 -107
  216. package/dist/form/types/form-proxy.d.ts +0 -141
  217. package/dist/form/unique-id.d.ts +0 -45
  218. package/dist/form/validation/config.d.ts +0 -73
  219. package/dist/form/validation/error-handler.d.ts +0 -166
  220. package/dist/form/validators/validation-error.d.ts +0 -8
  221. package/dist/index-BrNkQj1y.js +0 -105
  222. package/dist/loader-Dv_Hl7JC.js +0 -216
  223. package/dist/model-signals-proxy-2vqS243J.js +0 -250
  224. package/dist/node-DGEFMTQu.js +0 -18
  225. package/dist/strategy-BXS_gBfz.js +0 -170
  226. package/dist/validation-error-B-U3RsV0.js +0 -11
@@ -2,91 +2,120 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateModel`) →
6
- проверить `boolean`-результат → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
7
- Валидация — **отдельный слой** (`@reformer/core/validation`, схема через `defineValidationSchema`), она
8
- НЕ входит в layout-дерево, которое получает `createForm`. Раннер `validateModel(model, schema)` сам роутит
9
- ошибки в ноды формы, поэтому UI подсветит проблемные поля, а наружу вернёт лишь `boolean`
10
- (false = есть блокирующая ошибка; `severity:'warning'` показывается, но submit не блокирует).
11
- `model.reset()` возвращает значения к initial-снимку.
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)`.
12
16
 
13
17
  ## API
14
18
 
15
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
+
16
34
  // Модель (источник истины значений):
17
35
  interface ModelApi<T> {
18
- get(): T; // снимок значений — для submit
19
- set(value: T): void; // полная установка (все ключи T)
20
- patch(value: Partial<T>): void; // частичное слияние
21
- isDirty(): boolean; // отличаются ли значения от initial-снимка
22
- reset(): void; // вернуть к initial-снимку
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 не пишет — их пересчитает поведение)
23
42
  captureInitial(): void; // зафиксировать текущие как новый initial
24
43
  }
25
44
 
26
- // Валидация данных — headless-раннер: находит ноды по сигналам модели, роутит в них ошибки,
27
- // отменяет устаревшие прогоны той же (model, schema). Возвращает ТОЛЬКО boolean:
28
- validateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean>
29
- // true = нет блокирующих ошибок (severity:'warning' не блокирует, но показывается).
30
- // false = есть блокирующая ошибка; она уже проставлена в ноду → UI подсветит поле.
31
-
32
- // Схема валидации — отдельный слой над моделью (не смешивается с layout):
33
- defineValidationSchema<T>(({ model }) => {
34
- validate(model.$.field, [rules]); // синхронные правила поля
35
- // validateAsync / validateWhen / cross / each / apply — см. contract-spec
36
- }): ValidationSchema<T>
37
-
38
- // Нода поля/формы (для точечной работы с UI-состоянием):
39
- form.markAsTouched(); // тронуть все поля (показать ошибки)
40
- form.clearErrors(); // снять ошибки со всех нод
41
- form.<field>.setErrors([{ code, message }]); // серверная/бизнес-ошибка в конкретное поле
42
- form.<field>.clearErrors();
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)
43
59
  ```
44
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
+
45
72
  ## Examples
46
73
 
47
74
  ### Базовый submit-handler
48
75
 
49
76
  ```tsx
50
- import { useMemo } from 'react';
51
- import { createModel, createForm } from '@reformer/core';
52
- import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
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';
53
80
  import { required, email, minLength } from '@reformer/core/validators';
54
81
 
55
- type RegistrationFormData = { username: string; email: string; password: string };
82
+ interface RegistrationFormData {
83
+ username: string;
84
+ email: string;
85
+ password: string;
86
+ }
56
87
 
57
- // Валидация — отдельная схема над моделью. НЕ входит в layout, который получает createForm.
58
- // Стабильная module-level `const`-ссылка — чтобы validateModel мог отменять устаревшие прогоны.
59
- const registrationSchema = defineValidationSchema<RegistrationFormData>(({ model }) => {
60
- validate(model.$.username, [required({ message: 'Имя обязательно' }), minLength(3)]);
61
- validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);
62
- validate(model.$.password, [required({ message: 'Пароль обязателен' }), minLength(8)]);
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)]);
63
93
  });
64
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
+
65
110
  function RegistrationForm() {
66
- const { model, form } = useMemo(() => {
67
- const m = createModel<RegistrationFormData>({ username: '', email: '', password: '' });
68
- // schema здесь — layout-дерево нод (компоненты/раскладка), без валидаторов.
69
- return { model: m, form: createForm({ model: m, schema: buildLayout(m) }) };
70
- }, []);
71
-
72
- const handleSubmit = async (e: React.FormEvent) => {
73
- e.preventDefault();
74
- form.markAsTouched(); // показать ошибки на всех полях
75
-
76
- // Шаг 1: прогон валидации данных. Ошибки автоматически проставятся в ноды → UI подсветит.
77
- const valid = await validateModel(model, registrationSchema);
78
- if (!valid) return;
79
-
80
- // Шаг 2: достать чистые данные и отправить
81
- const payload = model.get();
82
- const response = await fetch('/api/v1/auth/register', {
83
- method: 'POST',
84
- headers: { 'Content-Type': 'application/json' },
85
- body: JSON.stringify(payload),
86
- });
87
-
88
- // Шаг 3: чистый старт после успеха
89
- if (response.ok) model.reset();
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(); // чистый старт
90
119
  };
91
120
 
92
121
  return (
@@ -100,32 +129,57 @@ function RegistrationForm() {
100
129
  }
101
130
  ```
102
131
 
103
- ### Submit с error-handling и серверными ошибками
132
+ ### Submit с разбором результата и серверными замечаниями
104
133
 
105
134
  ```tsx
106
- async function handleSubmit(e: React.FormEvent) {
107
- e.preventDefault();
108
- form.markAsTouched();
109
-
110
- const valid = await validateModel(model, registrationSchema);
111
- if (!valid) return;
112
-
113
- try {
114
- const response = await api.register(model.get());
115
- if (response.success) {
116
- model.reset();
117
- navigate('/welcome');
118
- } else {
119
- // Серверная бизнес-ошибка — НЕ сбрасываем форму, кладём ошибку в поле
120
- form.username.setErrors([{ code: 'taken', message: response.message }]);
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' }]);
121
143
  }
122
- } catch (error) {
123
- // Сеть/неожиданная ошибка — оставляем значения
124
- showToast(`Ошибка сети: ${(error as Error).message}`);
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;
125
164
  }
126
165
  }
127
166
  ```
128
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
+
129
183
  ### Reset с подтверждением
130
184
 
131
185
  ```tsx
@@ -150,61 +204,103 @@ function ActionButtons({ model }: { model: FormModel<RegistrationFormData> }) {
150
204
  }
151
205
  ```
152
206
 
153
- ### Использование вне React (server action / Node)
207
+ ### Без сборки — раннер (server action / Node)
154
208
 
155
209
  Раннер headless — работает без UI-компонентов, только с моделью и схемой:
156
210
 
157
211
  ```typescript
158
212
  import { createModel } from '@reformer/core';
159
- import { validateModel } from '@reformer/core/validation';
213
+ import { runValidation } from '@reformer/core/validation';
160
214
 
161
215
  async function processFormPayload(rawData: Partial<MyForm>) {
162
216
  const model = createModel<MyForm>(initialValues);
163
217
  model.patch(rawData); // частичный load данных
164
218
 
165
- // validateModel находит ноды по сигналам модели и роутит в них ошибки, возвращая boolean.
166
- const valid = await validateModel(model, myFormSchema);
167
- if (!valid) return { ok: false as const };
219
+ // Полный результат: статус и замечания по ручкам полей — без нод формы
220
+ const result = await runValidation(model, myFormRules);
221
+ if (result.status !== 'valid') return { ok: false as const, status: result.status };
168
222
  return { ok: true as const, data: model.get() };
169
223
  }
170
224
  ```
171
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
+
172
248
  ## Anti-patterns
173
249
 
174
250
  ```typescript
175
- // ❌ Обращение к .valid: раннер возвращает boolean, а не { valid, errors }
176
- const result = await validateModel(model, schema);
177
- if (!result.valid) return; // result — boolean, .valid === undefined → guard никогда не сработает
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);
178
271
 
179
- // ✅ Результат — сам boolean
180
- const valid = await validateModel(model, schema);
181
- if (!valid) return;
272
+ // ✅ Проверка — у сборки, отправка — её объект submission (см. 17-nonexistent-api.md)
273
+ await bundle.validate();
274
+ await bundle.submission.submit(api.send);
182
275
  ```
183
276
 
184
277
  ```typescript
185
- // ❌ Чтение результата валидации без await — сабмитим по неполному вердикту
186
- const handleSubmit = (e) => {
187
- e.preventDefault();
188
- validateModel(model, schema); // Promise проигнорирован, async-валидаторы не дождались
189
- submit(model.get());
278
+ // ❌ Чтение результата без await — сабмитим по неполному вердикту
279
+ const handleSubmit = (event) => {
280
+ event.preventDefault();
281
+ validateModel(model, schema); // Promise проигнорирован, async-правила не дождались
282
+ send(model.get());
190
283
  };
191
284
 
192
- // ✅ Дождаться и проверить boolean
193
- const handleSubmit = async (e) => {
194
- e.preventDefault();
195
- if (await validateModel(model, schema)) submit(model.get());
285
+ // ✅ bundle.submission.submit дожидается async-правил сам
286
+ const handleSubmit = async (event) => {
287
+ event.preventDefault();
288
+ await bundle.submission.submit(send);
196
289
  };
197
290
  ```
198
291
 
199
292
  ```typescript
200
- // ❌ Валидаторы в layout-дереве, отдаваемом в createForm — layout не несёт правил
201
- createForm({ model, schema: { username: { value: '', validators: [required()] } } });
293
+ // ❌ Правила в узле схемы формы — ключа validators у узла нет, схема формы правил не несёт
294
+ createForm({
295
+ initial,
296
+ schema: (model) => ({ children: [{ model: model.$.username, component: Input, validators: [required()] }] }),
297
+ });
202
298
 
203
- // ✅ Валидация — отдельная схема, прогоняется раннером
204
- const schema = defineValidationSchema<T>(({ model }) => {
299
+ // ✅ Валидация — отдельная схема, уходит в сборку полем validation
300
+ const rules = defineValidation<T>(({ model }) => {
205
301
  validate(model.$.username, [required()]);
206
302
  });
207
- await validateModel(model, schema);
303
+ createForm({ initial, schema, validation: rules });
208
304
  ```
209
305
 
210
306
  ```typescript
@@ -212,11 +308,20 @@ await validateModel(model, schema);
212
308
  await api.send(model.get());
213
309
  model.reset();
214
310
 
215
- // ✅ Reset только после успеха
216
- try {
217
- await api.send(model.get());
218
- model.reset();
219
- } catch (err) { showError(err); }
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();
220
325
  ```
221
326
 
222
327
  ```typescript
@@ -231,29 +336,72 @@ model.reset();
231
336
 
232
337
  ## Troubleshooting
233
338
 
234
- **Q: После `reset()` в UI остались старые ошибки.**
235
- A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
236
- `validateModel(model, schema)` после reset (валидные поля погаснут сами), либо очисти напрямую —
237
- `form.clearErrors()` / `form.<field>.clearErrors()`.
238
-
239
- **Q: `validateModel` вернул `true`, но в поле висит ошибка.**
240
- A: Это правило с `severity: 'warning'` — оно показывается, но submit не блокирует, поэтому раннер
241
- и вернул `true`. Блокируют только ошибки без `severity` (default). Чтобы не пускать submit при
242
- warning — проверяй его отдельно, вне `validateModel`.
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
+ вызова.
243
371
 
244
372
  **Q: `reset()` не возвращает данные, загруженные с сервера.**
245
- A: `reset()` возвращает к initial-снимку (значения на момент `createModel`). `set/patch` НЕ
246
- меняют initial. Чтобы сделать загруженные данные новой «точкой отсчёта» — вызови
247
- `model.captureInitial()` после загрузки.
373
+ A: `reset()` возвращает к initial-снимку (значения на момент `createModel`; правки `seed`
374
+ собственной модели фабрики в него входят). `set/patch` НЕ меняют initial. Чтобы сделать
375
+ загруженные данные новой «точкой отсчёта» — вызови `model.captureInitial()` после загрузки.
376
+
377
+ **Q: Что сброс и `isDirty()` делают с вычисляемыми полями?**
378
+ A: Ничего. Цели `compute` / `computeFrom` и поля с `markDerived` (например, номер заявки от
379
+ сервера) `model.reset()` и `form.reset()` не пишут — так же, как `set` и `patch`: значение
380
+ пересчитает поведение, когда сброс вернёт его источники. В `isDirty()` такие поля не входят,
381
+ поэтому форма без единой правки не считается изменённой, даже если поведение уже посчитало ставку.
382
+ Явная запись — `model.rate = 0` или `form.rate.reset(0)` — пишет как обычно. Исключение —
383
+ `enableWhen(…, { resetOnDisable: true })`: выключенное поле возвращается к значению создания, даже
384
+ если оно цель `compute` (её `compute` обязан нести то же условие `when`).
385
+
386
+ **Q: Чем `form.reset()` отличается от `model.reset()`?**
387
+ A: Точка отсчёта у них одна — initial-снимок модели либо последний `model.captureInitial()`
388
+ (загруженные данные). `form.reset()` — сброс для пользователя (кнопка «Сбросить»): значения к точке
389
+ отсчёта и чистые ноды — замечания, touched, dirty; после него и форма, и модель не изменены.
390
+ `model.reset()` — сброс значений модели: ноды не трогает, замечания погаснут при следующей
391
+ проверке. Исключение — `enableWhen({ resetOnDisable: true })`: выключенное поле очищается к
392
+ значению, с которым нода создана, а не к загруженному.
248
393
 
249
394
  **Q: Disabled-поля участвуют в submit-данных?**
250
- A: `model.get()` возвращает значения всех полей. Фильтруй вручную после `get()` или используй
251
- `enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial.
395
+ A: Да. Обработчик получает `model.get()` — модель целиком, с выключенными полями: их правила не
396
+ считаются, но данные принадлежат модели. Фильтруй сам либо используй
397
+ `enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial. Замечания
398
+ отключённых полей сняты, в проверку они не входят.
252
399
 
253
400
  ## See also
254
401
 
402
+ - [38-form-flow.md](./38-form-flow.md) — уровень формы: `validate`, `validateStep`, `submission`, поток шагов
255
403
  - [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
256
- - [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateModel` (`validateStep`/`validateAll`)
404
+ - [13-multi-step.md](./13-multi-step.md) — пошаговая форма: поток `flow`, визард
257
405
  - [27-revalidate-when.md](./27-revalidate-when.md) — мост «поведение → валидация»: `revalidateWhen([deps], () => void validateModel(model, schema))`
258
- - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateModel`
406
+ - [36-issue-messages.md](./36-issue-messages.md) — тексты замечаний по коду
259
407
  - [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки