@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
@@ -4,29 +4,52 @@
4
4
 
5
5
  Одна декларативная точка выбора «когда прогонять схему валидации» вместо ручной разводки
6
6
  `markAsTouched` + `validateModel` + `revalidateWhen` + `useState(pending)`. Аддитивный слой над
7
- функциональной схемой (`@reformer/core/validation`): переиспользует `validateModel` как
8
- ЕДИНСТВЕННЫЙ движок (роутинг ошибок в ноды, отмена устаревших прогонов, дедуп по `(model, schema)`),
9
- второго движка не заводит. Node-level `updateOn` / `debounce` на самой ноде поля (legacy-триггеры)
10
- этот API НЕ трогает.
7
+ функциональной схемой (`@reformer/core/validation`): переиспользует раннер схемы как
8
+ ЕДИНСТВЕННЫЙ движок (разнос ошибок по нодам, отмена устаревших прогонов, дедуп по `(model, schema)`),
9
+ второго движка не заводит. Других триггеров валидации нет: у ноды поля собственных валидаторов,
10
+ `updateOn` и `debounce` не бывает — когда запускать проверку, решает только стратегия.
11
11
 
12
12
  Стратегия определяет только момент запуска и раскрытие ошибок — сами правила остаются в отдельной
13
- `defineValidationSchema<T>(({ model }) => …)`.
13
+ `defineValidation<T>(({ model }) => …)`.
14
14
 
15
15
  ## API
16
16
 
17
- Основной способ — React-хук `useFormValidation` из `@reformer/core` (рядом с `useFormControl`).
18
- Под ним — headless-фабрика `createFormValidation` из `@reformer/core/validation` (для SSR / тестов /
19
- не-React потребителей).
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 потребителей.
20
27
 
21
28
  ```typescript
22
- // React (@reformer/core)
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)
23
42
  function useFormValidation<T>(args: {
24
43
  model: FormModel<T>;
25
- schema: ValidationSchema<T>; // СТАБИЛЬНАЯ ссылка (module-level const / useMemo)
44
+ schema: ValidationDefinition<T>; // СТАБИЛЬНАЯ ссылка (module-level const / useMemo)
26
45
  strategy?: ValidationStrategyKind; // default 'submit'
27
46
  debounce?: number; // мс, для live-фаз (change / live-часть afterFirstSubmit)
28
47
  liveAfterSubmit?: 'change' | 'blur'; // default 'change'
29
- }): { submit: () => Promise<boolean>; validate: () => Promise<boolean>; isValidating: boolean };
48
+ }): {
49
+ submit: () => Promise<boolean>; // попытка отправки: при отказе раскрыта вся форма, afterFirstSubmit → живая фаза
50
+ validate: () => Promise<boolean>; // проверка без отправки: раскрыты проверенные поля, фаза не меняется
51
+ isValidating: boolean;
52
+ };
30
53
 
31
54
  // headless (@reformer/core/validation)
32
55
  type ValidationStrategyKind = 'submit' | 'blur' | 'change' | 'afterFirstSubmit';
@@ -35,51 +58,101 @@ interface ValidationStrategyOptions {
35
58
  debounce?: number;
36
59
  liveAfterSubmit?: 'change' | 'blur';
37
60
  }
61
+ interface FormValidationRunOptions {
62
+ touch?: true | 'all'; // true (default) — проверенные поля; 'all' — при отказе ещё и вся форма
63
+ submit?: boolean; // прогон — попытка отправки: afterFirstSubmit → живая фаза
64
+ }
38
65
  interface FormValidationController {
39
- validate(): Promise<boolean>; // полный прогон, touch:true (раскрыть ВСЕ ошибки); переводит afterFirstSubmit в live-фазу
66
+ validate(): Promise<boolean>; // нейтральная проверка: touch:true, фазу afterFirstSubmit не трогает; true — только valid
67
+ run(options?: FormValidationRunOptions): Promise<ValidationResult>; // valid | invalid | error | cancelled
40
68
  start(): () => void; // армит реактивные подписки (ТОЛЬКО на клиенте) → dispose
41
- dispose(): void;
69
+ dispose(): void; // снять подписки и таймеры, отменить идущий прогон (он получит cancelled)
42
70
  readonly isValidating: boolean;
43
71
  readonly validating: ReadonlySignal<boolean>;
44
72
  }
45
73
  function createFormValidation<T>(
46
74
  model: FormModel<T>,
47
- schema: ValidationSchema<T>,
75
+ schema: ValidationDefinition<T>,
48
76
  options?: ValidationStrategyOptions,
49
77
  ): FormValidationController;
50
78
  ```
51
79
 
52
- - `useFormValidation` мемоизирует контроллер по `[model, schema, strategy, debounce, liveAfterSubmit]`,
53
- армит стратегию в `useEffect` (SSR-safe), а `submit()` = `controller.validate()`.
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`.
54
84
  - `createFormValidation` ЧИСТА до `start()` (никаких подписок) → SSR-safe; `start()` возвращает
55
85
  `dispose` и арминует триггеры только на клиенте.
56
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
+
57
102
  ### Стратегии
58
103
 
59
- | strategy | когда прогон | touch | раскрытие ошибок |
60
- | --- | --- | --- | --- |
61
- | `submit` (default) | только `validate()` / `submit()` | `true` | всё на submit |
62
- | `blur` | смена `touched` поля (потеря фокуса) | `false` | только сблюренные (по `touched`) |
63
- | `change` | на каждый ввод (+ `debounce`) | `false` | только редактированные (по `dirty`) |
64
- | `afterFirstSubmit` | тихо до 1-го submit, затем live (`liveAfterSubmit: 'change'` \| `'blur'`, default `'change'`) | `true` один раз на 1-м submit, дальше `false` | тихо → раскрыть всё на submit → далее live |
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 |
65
110
 
66
111
  ## Examples
67
112
 
68
- ### Простая форма — afterFirstSubmit + debounce
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
69
139
 
70
- Тихо до первой отправки, `submit()` раскрывает все ошибки и переводит форму в живую фазу
71
- (дальше проверка на ввод с задержкой 400 мс). `submit()` сам метит `touched` — ручной
72
- `markAsTouched` не нужен.
140
+ Тихо до первой отправки, `submit()` раскрывает все замечания и переводит форму в живую фазу
141
+ (дальше проверка на ввод с задержкой 400 мс). `submit()` сам метит `touched` (при отказе — всю
142
+ форму) — ручной `markAsTouched` не нужен. `validate()` — кнопка «Проверить»: раскрывает замечания
143
+ проверенных полей, а форма остаётся в тихой фазе.
73
144
 
74
145
  ```tsx
75
- import { createModel, createForm, useFormValidation } from '@reformer/core';
76
- import { defineValidationSchema, validate } from '@reformer/core/validation';
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';
77
150
  import { required, email, minLength } from '@reformer/core/validators';
78
151
 
79
152
  type RegistrationData = { username: string; email: string; password: string };
80
153
 
81
154
  // Схема — стабильный module-level const (иначе ломается дедуп раннера).
82
- const registrationValidation = defineValidationSchema<RegistrationData>(({ model }) => {
155
+ const registrationValidation = defineValidation<RegistrationData>(({ model }) => {
83
156
  validate(model.$.username, [required(), minLength(3)]);
84
157
  validate(model.$.email, [required(), email()]);
85
158
  validate(model.$.password, [required(), minLength(8)]);
@@ -87,17 +160,17 @@ const registrationValidation = defineValidationSchema<RegistrationData>(({ model
87
160
 
88
161
  function RegistrationForm() {
89
162
  const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);
90
- const form = useMemo(() => createForm({ model }), [model]);
163
+ const form = useMemo(() => createFormFromModel({ model }), [model]);
91
164
 
92
- const { submit, isValidating } = useFormValidation({
165
+ const { submit, validate, isValidating } = useFormValidation({
93
166
  model,
94
167
  schema: registrationValidation,
95
168
  strategy: 'afterFirstSubmit',
96
169
  debounce: 400,
97
170
  });
98
171
 
99
- const onSubmit = async (e: React.FormEvent) => {
100
- e.preventDefault();
172
+ const onSubmit = async (event: React.FormEvent) => {
173
+ event.preventDefault();
101
174
  if (await submit()) await api.save(model.get()); // true = нет блокирующих ошибок
102
175
  };
103
176
 
@@ -106,49 +179,69 @@ function RegistrationForm() {
106
179
  <FormField control={form.username} />
107
180
  <FormField control={form.email} />
108
181
  <FormField control={form.password} />
109
- <button type="submit" disabled={isValidating}>Отправить</button>
182
+ <button type="button" onClick={() => void validate()}>
183
+ Проверить
184
+ </button>
185
+ <button type="submit" disabled={isValidating}>
186
+ Отправить
187
+ </button>
110
188
  </form>
111
189
  );
112
190
  }
113
191
  ```
114
192
 
115
- ### Wizard — live-стратегия внутри активного шага
193
+ ### Визард — стратегия формы и проверка шага потоком
116
194
 
117
- `defineSteps(..., { strategy })` задаёт ЖИВУЮ стратегию ВНУТРИ шага; `useWizardStepValidation`
118
- арминует её под текущий шаг (`currentStep` из `FormWizardContext`) и снимает при смене шага / unmount.
119
- Per-step gate (`validateStep` на «Далее») и `validateAll` (submit) при этом НЕ меняются — стратегия
120
- добавляет живой слой ПОВЕРХ них. No-op, если `strategy` не задана / `'submit'` / у шага нет правил.
195
+ Живая стратегия — свойство формы, а не шага: она задаётся полем `validation` сборки и армируется
196
+ `useFormBundle`. Проверку шага делает поток: «Далее» визарда — `flow.next()`, он прогоняет правила
197
+ текущего шага и не пускает дальше при замечаниях. Отправка — `bundle.submission.submit` (все
198
+ шаги + `extras`). Живую фазу `afterFirstSubmit` включает только она: `flow.next()` и
199
+ `validateStep` фазу не переключают.
121
200
 
122
201
  ```tsx
123
- import { defineSteps, useWizardStepValidation, FormWizard } from '@reformer/cdk/form-wizard';
124
-
125
- const config = defineSteps<'loan' | 'applicant' | 'confirm', CreditForm>(model, {
126
- steps: { loan: loanStep, applicant: applicantStep, confirm: null }, // confirm — без правил, ЯВНО
127
- extras: crossFieldRules, // применяются только в validateAll (submit)
128
- strategy: 'blur', // живая валидация полей текущего шага при потере фокуса
129
- });
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
+ });
130
215
 
131
- function LoanStepBody() {
132
- useWizardStepValidation(config); // армит 'blur' под активный шаг, снимает при переходе
133
- return <>{/* поля шага */}</>;
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
+ );
134
226
  }
135
-
136
- <FormWizard form={form} config={config}>
137
- <LoanStepBody />
138
- </FormWizard>;
139
227
  ```
140
228
 
229
+ Живая фаза прогоняет **полный** набор правил (все шаги + `extras`), а показ замечаний гейтится
230
+ `touched` / `dirty` полей: поля следующих шагов пользователь ещё не трогал — их замечания не видны.
231
+ Прежний per-step механизм (`defineSteps(…, { strategy })` + `useWizardStepValidation`) удалён.
232
+
141
233
  ### Headless / SSR / тесты — createFormValidation
142
234
 
143
235
  ```typescript
144
236
  import { createFormValidation } from '@reformer/core/validation';
145
237
 
146
- const ctrl = createFormValidation(model, registrationValidation, {
238
+ const controller = createFormValidation(model, registrationValidation, {
147
239
  strategy: 'afterFirstSubmit',
148
240
  debounce: 300,
149
241
  });
150
- const dispose = ctrl.start(); // ТОЛЬКО на клиенте (в React — из useEffect)
151
- const ok = await ctrl.validate(); // submit → раскрыть все ошибки
242
+ const dispose = controller.start(); // ТОЛЬКО на клиенте (в React — из useEffect)
243
+ const checked = await controller.validate(); // проверка: замечания проверенных полей, фаза тихая
244
+ const sent = await controller.run({ touch: 'all', submit: true }); // попытка отправки → живая фаза
152
245
  dispose();
153
246
  ```
154
247
 
@@ -157,32 +250,50 @@ dispose();
157
250
  ```typescript
158
251
  // ❌ schema пересоздаётся каждый рендер → контроллер и дедуп раннера ломаются
159
252
  function MyForm() {
160
- const schema = defineValidationSchema<Form>(({ model }) => { … }); // новая ссылка на каждый рендер
253
+ const schema = defineValidation<Form>(({ model }) => { … }); // новая ссылка на каждый рендер
161
254
  useFormValidation({ model, schema, strategy: 'change' });
162
255
  }
163
256
 
164
257
  // ✅ Стабильная ссылка: module-level const (или useMemo с пустыми deps)
165
- const formValidation = defineValidationSchema<Form>(({ model }) => { … });
258
+ const formValidation = defineValidation<Form>(({ model }) => { … });
166
259
  function MyForm() {
167
260
  useFormValidation({ model, schema: formValidation, strategy: 'change' });
168
261
  }
169
262
  ```
170
263
 
171
264
  ```typescript
172
- // ❌ Node-level updateOn (Слой B) И активная schema-стратегия на ОДНОМ поле → оба пишут ошибки в ноду → мерцание
173
- // узел поля: { updateOn: 'blur', … } + useFormValidation({ strategy: 'blur', schema: включает это же поле })
265
+ // ❌ updateOn / debounce / validators в узле схемы — таких ключей у узла нет, сборка их не читает
266
+ // { model: model.$.email, component: Input, updateOn: 'blur', validators: [required()] }
174
267
 
175
- // ✅ Одно поле обслуживает ОДИН слой: либо node-level updateOn, либо schema-стратегия — не оба сразу
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' } }));
176
279
  ```
177
280
 
178
281
  ```typescript
179
282
  // ❌ start() на сервере — арминует реактивные подписки при SSR
180
- const ctrl = createFormValidation(model, schema, { strategy: 'change' });
181
- ctrl.start(); // на сервере подписок быть не должно
283
+ const controller = createFormValidation(model, schema, { strategy: 'change' });
284
+ controller.start(); // на сервере подписок быть не должно
182
285
 
183
286
  // ✅ Фабрика чиста до start(); армить только на клиенте (useFormValidation делает это в useEffect)
184
- const ctrl = createFormValidation(model, schema, { strategy: 'change' });
185
- if (typeof window !== 'undefined') ctrl.start();
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
186
297
  ```
187
298
 
188
299
  ## See also
@@ -190,4 +301,5 @@ if (typeof window !== 'undefined') ctrl.start();
190
301
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — submit-флоу и `validateModel` (движок под стратегией)
191
302
  - [27-revalidate-when.md](./27-revalidate-when.md) — ручные триггеры перевалидации (низкоуровневый примитив)
192
303
  - [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов
193
- - [13-multi-step.md](./13-multi-step.md) — per-step / полная валидация wizard (`validateStep` / `validateAll`)
304
+ - [13-multi-step.md](./13-multi-step.md) — многошаговая форма: поток `flow`, `bundle.validateStep(id)`
305
+ - [38-form-flow.md](./38-form-flow.md) — уровень формы: `validate`, `submission`, поток шагов
@@ -2,20 +2,23 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer/*`: объект
6
- локали, загрузка словарей, React-провайдер, перевод и форматирование. Язык задаётся провайдером,
7
- подгружается динамически и переключается на лету; узлы формы при этом не пересоздаются.
5
+ Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer/*` без React:
6
+ объект локали, загрузка словарей, перевод и форматирование, тексты замечаний валидации. React-часть
7
+ — провайдер `I18nProvider` и хуки `useI18n`, `useMessages`, `useIssueMessage` — отдаёт
8
+ `@reformer/core/react`. Язык задаётся провайдером, подгружается динамически и переключается на лету;
9
+ узлы формы при этом не пересоздаются.
8
10
 
9
11
  Без провайдера всё работает на английском: у каждого пакета есть встроенная английская таблица.
10
12
 
11
- > Импорт всегда из `@reformer/core/i18n`. Из главной бочки `@reformer/core` этот модуль не
12
- > реэкспортируется: React-контекст локализации обязан существовать в одном экземпляре.
13
+ > Импорт локали и перевода — из `@reformer/core/i18n`, провайдера и хуков — из
14
+ > `@reformer/core/react`. Из главной бочки `@reformer/core` ни то, ни другое не реэкспортируется:
15
+ > React-контекст локализации обязан существовать в одном экземпляре.
13
16
 
14
17
  ## Локаль — это данные
15
18
 
16
19
  ```typescript
17
20
  interface FormLocale {
18
- code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural
21
+ code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural; не BCP 47 — по en
19
22
  messages: Record<string, string>; // плоский словарь: ключ → сообщение ICU
20
23
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
21
24
  dateLocale?: unknown; // необязательно: объект локали react-day-picker / date-fns
@@ -93,7 +96,7 @@ export const loadLocale = createLocaleLoader([
93
96
  ## Провайдер
94
97
 
95
98
  ```tsx
96
- import { I18nProvider } from '@reformer/core/i18n';
99
+ import { I18nProvider } from '@reformer/core/react';
97
100
 
98
101
  function App() {
99
102
  const [lang, setLang] = useState('ru');
@@ -108,7 +111,7 @@ function App() {
108
111
 
109
112
  | Проп | Назначение |
110
113
  | --------------- | ------------------------------------------------------------------------ |
111
- | `lang` + `load` | провайдер грузит локаль сам |
114
+ | `lang` + `load` | провайдер грузит локаль сам; `load` — стабильная ссылка (константа модуля) |
112
115
  | `fallback` | что показать, пока не загружена самая первая локаль; по умолчанию ничего |
113
116
  | `onError` | отказ загрузки `(error, lang)` |
114
117
  | `locale` | готовый объект вместо `lang` + `load` — SSR, тесты |
@@ -118,7 +121,12 @@ function App() {
118
121
  - при смене `lang` на экране остаётся прежний язык, пока новый не загрузится;
119
122
  `useI18n().pending` — для индикатора;
120
123
  - при быстром переключении применяется только последний запрос;
121
- - ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`;
124
+ - ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`; повторить запрос
125
+ того же языка — `useI18n().retry()`;
126
+ - упала самая первая загрузка — дети показываются на встроенном английском (а не `fallback`),
127
+ ошибка — в `useI18n().error`;
128
+ - новая ссылка `load` — новое хранилище: язык запрашивается у нового загрузчика, на экране тем
129
+ временем прежняя локаль;
122
130
  - если локаль уже в кэше (`await loadLocale.preload(lang)` до монтирования), первый рендер
123
131
  синхронный и `fallback` не показывается.
124
132
 
@@ -130,7 +138,9 @@ SSR: эффекты на сервере не выполняются, поэто
130
138
  ## Хуки
131
139
 
132
140
  ```typescript
133
- const { code, locale, pending, error, t, has, number, date, fileSize } = useI18n();
141
+ import { useI18n, useMessages, useIssueMessage } from '@reformer/core/react';
142
+
143
+ const { code, locale, pending, error, retry, t, has, number, date, fileSize } = useI18n();
134
144
 
135
145
  t('profile.title'); // ключ приложения
136
146
  t('cart.items', { count: 3 }); // с подстановками
@@ -152,31 +162,40 @@ function ClearButton() {
152
162
  }
153
163
  ```
154
164
 
155
- ## Тексты ошибок валидации
165
+ ## Тексты замечаний валидации
156
166
 
157
- Правила из `@reformer/core/validators` несут только `code` и `params`; готового текста у них нет
158
- (`message` по умолчанию — пустая строка). Строку даёт резолвер при отображении ошибки:
167
+ Замечание валидации — `ValidationIssue = { code, params?, severity? }`: текста у него нет. Правила из
168
+ `@reformer/core/validators` несут только `code` и `params`, свой код задаёт опция `code`. Строку
169
+ даёт резолвер при отображении:
159
170
 
160
- 1. `messageKey` автора правила — если такой ключ есть в активной локали;
161
- 2. явное `message` автора правила;
162
- 3. `validation.<code>` из словаря локали;
163
- 4. `validation.<code>` из встроенной английской таблицы ядра;
164
- 5. сам код — для своих кодов, которых нет ни в одном словаре.
171
+ 1. словарь формы (`defineIssueMessages`) — запись по коду замечания;
172
+ 2. `validation.<code>` из словаря локали;
173
+ 3. `validation.<code>` из встроенной английской таблицы ядра;
174
+ 4. сам код — для своих кодов, которых нет ни в одном словаре.
165
175
 
166
176
  ```typescript
177
+ import { defineIssueMessages } from '@reformer/core/i18n';
178
+
167
179
  validate(model.$.email, [
168
180
  required(), // «Обязательное поле» в ru, английская фраза без провайдера
169
- minLength(5, { message: 'Слишком коротко' }), // явный текст: важнее словаря, но языком не переключается
170
- email({ messageKey: 'profile.email.invalid' }), // ключ словаря приложения: переключается вместе с языком
181
+ minLength(5, { code: 'email.minLength' }), // свой код: текст поля — в словаре формы
171
182
  ]);
183
+
184
+ // Словарь формы: шаблон с параметрами либо функция (числа с разрядами — функцией)
185
+ export const formMessages = defineIssueMessages({
186
+ 'email.minLength': 'Не меньше {minLength} символов',
187
+ 'loanAmount.min': (params, i18n) => `Минимум ${i18n.number(Number(params.min))} ₽`,
188
+ });
172
189
  ```
173
190
 
174
191
  - Текст получается при отображении, а не при проверке: смена языка переводит уже показанные
175
192
  ошибки без повторной валидации.
176
- - `FormField` (`@reformer/cdk`, `@reformer/ui-kit`) резолвит ошибку сам. В своём компоненте —
177
- `useValidationMessage()`; вне React — `resolveValidationError(error, createI18n(locale))`.
178
- - Свой код ошибки локализуется ключом `validation.<свой_код>` в словаре приложения либо
179
- `messageKey` в самом правиле.
193
+ - `FormField` (`@reformer/cdk`, `@reformer/ui-kit`) резолвит замечание сам; словарь формы подключает
194
+ `<ValidationMessagesProvider messages={formMessages}>` из `@reformer/cdk`. В своём компоненте —
195
+ `useIssueMessage(formMessages)` из `@reformer/core/react`; вне React —
196
+ `resolveIssueMessage(issue, createI18n(locale), formMessages)`.
197
+ - Свой код локализуется записью словаря формы либо ключом `validation.<свой_код>` в словаре
198
+ приложения — второй вариант переключается вместе с языком.
180
199
  - Встроенный текст меняется тем же ключом в словаре приложения:
181
200
  `extendLocale(ru, { messages: { 'validation.required': 'Заполните поле' } })`.
182
201
  - Параметры готовятся до подстановки: `Date` — по языку (`15 янв. 2030 г.`), массив — перечнем,
@@ -184,15 +203,17 @@ validate(model.$.email, [
184
203
  строкой с единицей (`5 МБ`). Числа остаются числами, поэтому в сообщении работает `plural`.
185
204
 
186
205
  ```tsx
187
- function FieldErrors({ control }: { control: FieldNode<string> }) {
188
- const message = useValidationMessage();
189
- const { errors, shouldShowError } = useFormControl(control);
190
- return shouldShowError ? <span role="alert">{message(errors[0])}</span> : null;
206
+ function FieldIssues({ control }: { control: FormControl<string> }) {
207
+ const message = useIssueMessage(formMessages);
208
+ const { issues, shouldShowIssues } = useFormControl(control);
209
+ return shouldShowIssues ? <span role="alert">{message(issues[0])}</span> : null;
191
210
  }
192
211
  ```
193
212
 
194
- Смонтированный `ValidationMessagesProvider` из `@reformer/cdk` заменяет этот резолвер целиком:
195
- локаль для ошибок в его поддереве не участвует.
213
+ `ValidationMessagesProvider` из `@reformer/cdk` с `messages` накладывает словарь формы поверх
214
+ локали (вложенные провайдеры накладываются друг на друга); с `resolver` — заменяет резолвер
215
+ целиком, и локаль для замечаний его поддерева не участвует. Подробно — коды, встроенные параметры,
216
+ шаблоны и функции словаря — [36-issue-messages.md](./36-issue-messages.md).
196
217
 
197
218
  ## Что показывается, когда ключа нет
198
219
 
@@ -201,8 +222,10 @@ function FieldErrors({ control }: { control: FieldNode<string> }) {
201
222
  | пакета (`useMessages`) | встроенный английский пакета | встроенный английский, `console.error` вне production |
202
223
  | приложения (`useI18n().t`) | `defaultMessage`, затем сам ключ; `console.warn` вне production | то же, `console.error` |
203
224
 
204
- Пропущенная подстановка выводится как `{name}`. Форма не падает ни в одном из случаев: неполный
205
- словарь, пришедший по сети, — законный сценарий.
225
+ Пропущенная подстановка выводится как `{name}`, значение, которое нельзя привести к строке
226
+ (`Object.create(null)`), — пустой строкой. Код языка не BCP 47 (пришёл из адресной строки) — числа,
227
+ даты и формы слов форматируются по `en`, в dev — предупреждение. Форма не падает ни в одном из
228
+ случаев: неполный словарь, пришедший по сети, — законный сценарий.
206
229
 
207
230
  ## Проверка словаря в тестах
208
231
 
@@ -210,7 +233,8 @@ function FieldErrors({ control }: { control: FieldNode<string> }) {
210
233
  import { validateLocale } from '@reformer/core/i18n';
211
234
 
212
235
  expect(validateLocale(ruJson, enJson)).toEqual([]);
213
- // находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка)
236
+ // находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка);
237
+ // битое сообщение образца — тоже syntax («в образце: …»), проверка остальных ключей продолжается
214
238
  ```
215
239
 
216
240
  ## Описатели сообщений (низкий уровень)
@@ -225,15 +249,14 @@ expect(validateLocale(ruJson, enJson)).toEqual([]);
225
249
  ## Anti-patterns
226
250
 
227
251
  ```typescript
228
- // ❌ Импорт из главной бочки — i18n там нет, а второй путь импорта дал бы вторую копию контекста
252
+ // ❌ Импорт из главной бочки — React-части локализации там нет
229
253
  import { I18nProvider } from '@reformer/core';
230
254
 
231
- // ✅ Единственный путь импорта — подпуть i18n
232
- import { I18nProvider } from '@reformer/core/i18n';
255
+ // ✅ Провайдер и хуки — из @reformer/core/react, локаль и перевод — из @reformer/core/i18n
233
256
  ```
234
257
 
235
258
  ```tsx
236
- // ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль грузится заново, первый кадр — fallback
259
+ // ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль запрашивается заново на каждый рендер
237
260
  <I18nProvider lang={lang} load={createLocaleLoader([loadCoreLocale])}>
238
261
 
239
262
  // ✅ Загрузчик — константа модуля: один кэш на приложение
@@ -250,12 +273,14 @@ t('cart.total', { total: number(total, { style: 'currency', currency: 'RUB' }) }
250
273
  ```
251
274
 
252
275
  ```tsx
253
- // ❌ Ошибка выводится полем message: у правил без своего текста оно пустое — на экране ничего
254
- <span>{errors[0].message}</span>;
255
-
256
- // ✅ Текст ошибки — через резолвер: он знает словарь локали, messageKey и явное сообщение
257
- const message = useValidationMessage();
258
- <span>{message(errors[0])}</span>;
276
+ // ❌ Текст в правиле: у RuleOptions и ValidationIssue нет message — не скомпилируется
277
+ required({ message: 'Укажите телефон' });
278
+
279
+ // ✅ Свой код в правиле, текст — в словаре формы (константа модуля), на экран — через резолвер
280
+ required({ code: 'phone.required' });
281
+ const phoneMessages = defineIssueMessages({ 'phone.required': 'Укажите телефон' }); // в модуле
282
+ const message = useIssueMessage(phoneMessages); // в компоненте
283
+ <span>{message(issues[0])}</span>;
259
284
  ```
260
285
 
261
286
  ```typescript
@@ -268,5 +293,6 @@ const source = () => ({ weekStartsOn: 1, messages: {} });
268
293
 
269
294
  ## See also
270
295
 
296
+ - [36-issue-messages.md](./36-issue-messages.md) — тексты замечаний: коды, словарь формы, резолвер
271
297
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — жизненный цикл формы; смена языка его не затрагивает
272
298
  - [33-validation-strategy.md](./33-validation-strategy.md) — когда запускается валидация