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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +91 -73
  2. package/dist/behaviors-value-HA_Q03hc.js +108 -0
  3. package/dist/behaviors.js +370 -242
  4. package/dist/{create-model-BBRHhHwK.js → create-model-BvcbNPgL.js} +6 -6
  5. package/dist/date-utils-90-qZbse.js +43 -0
  6. package/dist/{en-CjAqn7h0.js → en-HP8XEych.js} +6 -3
  7. package/dist/form/aggregate-signals.d.ts +21 -63
  8. package/dist/form/behaviors/collections.d.ts +28 -7
  9. package/dist/form/behaviors/context.d.ts +19 -2
  10. package/dist/form/behaviors/index.d.ts +3 -3
  11. package/dist/form/behaviors/internals.d.ts +9 -2
  12. package/dist/form/behaviors/node.d.ts +25 -2
  13. package/dist/form/behaviors/operators.d.ts +17 -9
  14. package/dist/form/behaviors/schema-operators.d.ts +6 -4
  15. package/dist/form/behaviors/types.d.ts +12 -4
  16. package/dist/form/create-form.d.ts +47 -35
  17. package/dist/form/flow/flow.d.ts +87 -0
  18. package/dist/form/form-bundle.d.ts +45 -28
  19. package/dist/form/form-config-base.d.ts +46 -0
  20. package/dist/form/form-level.d.ts +121 -0
  21. package/dist/form/form-proxy-builder.d.ts +6 -22
  22. package/dist/form/index.d.ts +16 -21
  23. package/dist/form/nodes/form-array-node.d.ts +100 -0
  24. package/dist/form/nodes/form-control-node.d.ts +73 -0
  25. package/dist/form/nodes/form-group-node.d.ts +66 -0
  26. package/dist/form/nodes/node-kind.d.ts +29 -0
  27. package/dist/form/nodes/state/availability.d.ts +6 -0
  28. package/dist/form/nodes/state/component.d.ts +6 -0
  29. package/dist/form/nodes/state/index.d.ts +12 -0
  30. package/dist/form/nodes/state/interaction.d.ts +13 -0
  31. package/dist/form/nodes/state/issues.d.ts +9 -0
  32. package/dist/form/nodes/state/pending.d.ts +6 -0
  33. package/dist/form/schema/compile.d.ts +59 -0
  34. package/dist/form/schema/compiled.d.ts +143 -0
  35. package/dist/form/schema/row-key.d.ts +11 -0
  36. package/dist/form/schema-controller.d.ts +30 -14
  37. package/dist/form/signal-node-registry.d.ts +6 -7
  38. package/dist/form/submission.d.ts +93 -0
  39. package/dist/form/type-guards.d.ts +14 -73
  40. package/dist/form/types/contracts.d.ts +19 -92
  41. package/dist/form/types/field-config.d.ts +27 -0
  42. package/dist/form/types/index.d.ts +5 -57
  43. package/dist/form/types/nodes.d.ts +159 -0
  44. package/dist/form/types/schema-node.d.ts +139 -63
  45. package/dist/form/types/validation-schema.d.ts +23 -27
  46. package/dist/form/validation/context.d.ts +42 -7
  47. package/dist/form/validation/index.d.ts +9 -8
  48. package/dist/form/validation/operators.d.ts +22 -30
  49. package/dist/form/validation/run.d.ts +93 -18
  50. package/dist/form/validation/strategy.d.ts +41 -13
  51. package/dist/form/validation/types.d.ts +74 -20
  52. package/dist/form/validators/date-utils.d.ts +13 -1
  53. package/dist/form/validators/email.d.ts +8 -7
  54. package/dist/form/validators/file-type.d.ts +8 -7
  55. package/dist/form/validators/file-utils.d.ts +2 -1
  56. package/dist/form/validators/future-date.d.ts +8 -7
  57. package/dist/form/validators/index.d.ts +1 -1
  58. package/dist/form/validators/integer.d.ts +8 -7
  59. package/dist/form/validators/is-date.d.ts +11 -9
  60. package/dist/form/validators/is-number.d.ts +11 -9
  61. package/dist/form/validators/max-age.d.ts +13 -9
  62. package/dist/form/validators/max-date.d.ts +8 -7
  63. package/dist/form/validators/max-file-size.d.ts +8 -7
  64. package/dist/form/validators/max-files.d.ts +8 -7
  65. package/dist/form/validators/max-length.d.ts +14 -7
  66. package/dist/form/validators/max-total-file-size.d.ts +8 -7
  67. package/dist/form/validators/max.d.ts +10 -8
  68. package/dist/form/validators/min-age.d.ts +12 -9
  69. package/dist/form/validators/min-date.d.ts +8 -7
  70. package/dist/form/validators/min-file-size.d.ts +8 -7
  71. package/dist/form/validators/min-files.d.ts +11 -9
  72. package/dist/form/validators/min-length.d.ts +14 -7
  73. package/dist/form/validators/min.d.ts +10 -8
  74. package/dist/form/validators/multiple-of.d.ts +10 -8
  75. package/dist/form/validators/non-negative.d.ts +8 -7
  76. package/dist/form/validators/non-zero.d.ts +8 -7
  77. package/dist/form/validators/past-date.d.ts +8 -7
  78. package/dist/form/validators/pattern.d.ts +9 -8
  79. package/dist/form/validators/phone.d.ts +8 -7
  80. package/dist/form/validators/required.d.ts +11 -9
  81. package/dist/form/validators/url.d.ts +20 -13
  82. package/dist/form/validators/validation-issue.d.ts +8 -0
  83. package/dist/i18n/descriptor.d.ts +1 -1
  84. package/dist/i18n/en.json.d.ts +5 -1
  85. package/dist/i18n/locale-store.d.ts +7 -0
  86. package/dist/i18n/message-format.d.ts +24 -4
  87. package/dist/i18n/ru.json.d.ts +5 -1
  88. package/dist/i18n/translator.d.ts +11 -0
  89. package/dist/i18n/validation-message.d.ts +42 -7
  90. package/dist/i18n.d.ts +7 -12
  91. package/dist/i18n.js +66 -283
  92. package/dist/index.d.ts +16 -7
  93. package/dist/index.js +924 -2855
  94. package/dist/loader-BWkigpVe.js +52 -0
  95. package/dist/locale/en.js +1 -1
  96. package/dist/locale/en.json +4 -1
  97. package/dist/locale/ru.js +4 -1
  98. package/dist/locale/ru.json +4 -1
  99. package/dist/locale-CG54AU2Q.js +219 -0
  100. package/dist/locale.js +1 -1
  101. package/dist/model/behaviors-value.d.ts +24 -6
  102. package/dist/model/create-model.d.ts +2 -2
  103. package/dist/model/derived-registry.d.ts +1 -1
  104. package/dist/model/index.d.ts +11 -5
  105. package/dist/model/model-nodes.d.ts +79 -12
  106. package/dist/model/model-signals-proxy.d.ts +5 -6
  107. package/dist/model/model-value-proxy.d.ts +20 -15
  108. package/dist/model/safe-effect.d.ts +33 -1
  109. package/dist/model/types.d.ts +63 -13
  110. package/dist/model.js +20 -18
  111. package/dist/node-kind-C-r6ZMdo.js +15 -0
  112. package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
  113. package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
  114. package/dist/platforms/react/form-bundle-context.d.ts +2 -2
  115. package/dist/platforms/react/hooks/types.d.ts +57 -38
  116. package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
  117. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  118. package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
  119. package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
  120. package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
  121. package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
  122. package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
  123. package/dist/platforms/react/i18n/context.d.ts +6 -1
  124. package/dist/platforms/react/i18n/hooks.d.ts +16 -10
  125. package/dist/platforms/react/i18n/provider.d.ts +9 -2
  126. package/dist/platforms/react/index.d.ts +6 -4
  127. package/dist/react.d.ts +22 -0
  128. package/dist/react.js +505 -0
  129. package/dist/runtime-token.d.ts +1 -2
  130. package/dist/safe-effect-BbtX3KuN.js +643 -0
  131. package/dist/schema-controller-CpiO2JoK.js +143 -0
  132. package/dist/signals.d.ts +15 -9
  133. package/dist/signals.js +11 -6
  134. package/dist/strategy-CoFSemuP.js +375 -0
  135. package/dist/type-guards-D-N_5Ddh.js +19 -0
  136. package/dist/validation-issue-B9mjI8jz.js +7 -0
  137. package/dist/validation-message-Ced9MGxs.js +194 -0
  138. package/dist/validation.js +9 -9
  139. package/dist/validators/email.js +5 -5
  140. package/dist/validators/future-date.js +8 -8
  141. package/dist/validators/integer.js +3 -3
  142. package/dist/validators/is-date.js +4 -4
  143. package/dist/validators/is-number.js +4 -4
  144. package/dist/validators/max-age.js +8 -9
  145. package/dist/validators/max-date.js +8 -8
  146. package/dist/validators/max-length.js +7 -7
  147. package/dist/validators/max.js +4 -4
  148. package/dist/validators/min-age.js +10 -8
  149. package/dist/validators/min-date.js +3 -3
  150. package/dist/validators/min-length.js +5 -5
  151. package/dist/validators/min.js +4 -4
  152. package/dist/validators/multiple-of.js +11 -7
  153. package/dist/validators/non-negative.js +3 -3
  154. package/dist/validators/non-zero.js +3 -3
  155. package/dist/validators/past-date.js +8 -8
  156. package/dist/validators/pattern.js +5 -5
  157. package/dist/validators/phone.js +11 -9
  158. package/dist/validators/required.js +2 -2
  159. package/dist/validators/url.js +48 -12
  160. package/dist/validators.js +17 -17
  161. package/docs/llms/01-api-reference.md +60 -34
  162. package/docs/llms/02-quick-start.md +111 -80
  163. package/docs/llms/03-api-signatures.md +368 -153
  164. package/docs/llms/04-common-patterns.md +58 -56
  165. package/docs/llms/05-common-mistakes.md +69 -44
  166. package/docs/llms/06-troubleshooting.md +14 -9
  167. package/docs/llms/07-complete-import.md +74 -43
  168. package/docs/llms/08-form-types.md +11 -4
  169. package/docs/llms/09-formschema.md +129 -81
  170. package/docs/llms/10-arrays.md +106 -75
  171. package/docs/llms/11-async-watchfield.md +2 -2
  172. package/docs/llms/13-multi-step.md +134 -63
  173. package/docs/llms/14-extended-mistakes.md +20 -17
  174. package/docs/llms/15-project-structure.md +221 -74
  175. package/docs/llms/16-ui-components.md +55 -42
  176. package/docs/llms/17-nonexistent-api.md +199 -32
  177. package/docs/llms/18-conditional-fields.md +66 -32
  178. package/docs/llms/19-reading-values.md +15 -3
  179. package/docs/llms/20-compute-vs-watch.md +16 -10
  180. package/docs/llms/21-array-operations.md +51 -27
  181. package/docs/llms/22-cycle-detection.md +1 -1
  182. package/docs/llms/23-copy-from.md +2 -2
  183. package/docs/llms/24-sync-fields.md +6 -5
  184. package/docs/llms/25-reset-when.md +14 -7
  185. package/docs/llms/26-transform-value.md +4 -4
  186. package/docs/llms/27-revalidate-when.md +16 -15
  187. package/docs/llms/28-submit-and-reset.md +276 -128
  188. package/docs/llms/29-async-preload.md +39 -29
  189. package/docs/llms/30-type-safety-recipes.md +105 -51
  190. package/docs/llms/31-async-validator-debounce.md +96 -53
  191. package/docs/llms/33-validation-strategy.md +177 -65
  192. package/docs/llms/34-i18n.md +69 -43
  193. package/docs/llms/35-unified-contract.md +100 -39
  194. package/docs/llms/36-issue-messages.md +284 -0
  195. package/docs/llms/37-compiled-schema.md +332 -0
  196. package/docs/llms/38-form-flow.md +397 -0
  197. package/llms-index.json +1 -1
  198. package/llms.txt +6084 -3420
  199. package/package.json +23 -4
  200. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  201. package/dist/date-utils-dTT_x_Be.js +0 -31
  202. package/dist/form/create-core-form.d.ts +0 -72
  203. package/dist/form/factories/index.d.ts +0 -6
  204. package/dist/form/factories/node-factory.d.ts +0 -189
  205. package/dist/form/form-submitter.d.ts +0 -121
  206. package/dist/form/nodes/array-node.d.ts +0 -276
  207. package/dist/form/nodes/field-node.d.ts +0 -297
  208. package/dist/form/nodes/form-node.d.ts +0 -326
  209. package/dist/form/nodes/group-node.d.ts +0 -284
  210. package/dist/form/nodes/model-array-node.d.ts +0 -126
  211. package/dist/form/nodes/subscription-manager.d.ts +0 -180
  212. package/dist/form/status-machine.d.ts +0 -115
  213. package/dist/form/types/deep-schema.d.ts +0 -107
  214. package/dist/form/types/form-proxy.d.ts +0 -141
  215. package/dist/form/unique-id.d.ts +0 -45
  216. package/dist/form/validation/config.d.ts +0 -73
  217. package/dist/form/validation/error-handler.d.ts +0 -166
  218. package/dist/form/validators/validation-error.d.ts +0 -8
  219. package/dist/index-BrNkQj1y.js +0 -105
  220. package/dist/loader-Dv_Hl7JC.js +0 -216
  221. package/dist/model-value-proxy-caTmAiUd.js +0 -419
  222. package/dist/node-BeV_lV5i.js +0 -100
  223. package/dist/strategy-DgzELWno.js +0 -204
  224. package/dist/validation-error-B-U3RsV0.js +0 -11
@@ -7,8 +7,9 @@
7
7
  (`createForm`), поведение одно (`defineFormBehavior`). Способы отличаются только видом схемы
8
8
  (билдер или документ) и тем, кто рисует результат.
9
9
 
10
- Прежние записи — ключи `value` / `array`, оператор `each`, `createCoreForm` — пока принимаются.
11
- Новый код пишется так, как описано здесь.
10
+ Прежние записи узла — ключи `value` / `array` и запись «имя поля → узел» — удалены: тип узла
11
+ закрыт, сборка их не читает. Удалены и оператор `each`, свободный `cross` и имя `createCoreForm`:
12
+ их заменяют `applyEach` со схемой строки, `cross` из аргумента схемы и `createForm`.
12
13
 
13
14
  | Слой | поле | массив под-форм | подформа |
14
15
  | --------- | --------------------------------- | ----------------------------------------- | ------------------------------------- |
@@ -51,10 +52,10 @@ export const createCreditModel = () =>
51
52
  ## Схема — одно дерево
52
53
 
53
54
  ```typescript
54
- import type { FormModel } from '@reformer/core';
55
+ import type { FormModel, FormSchemaNode } from '@reformer/core';
55
56
 
56
57
  // подформа: объявлена один раз, получает под-модель
57
- const address = (model: FormModel<Address>) => ({
58
+ const address = (model: FormModel<Address>): FormSchemaNode => ({
58
59
  component: Box,
59
60
  children: [
60
61
  { model: model.$.city, component: Input, componentProps: { label: 'Город' } },
@@ -63,13 +64,13 @@ const address = (model: FormModel<Address>) => ({
63
64
  });
64
65
 
65
66
  // строка массива — такая же функция от под-модели
66
- const coBorrower = (model: FormModel<CoBorrower>) => ({
67
+ const coBorrower = (model: FormModel<CoBorrower>): FormSchemaNode => ({
67
68
  selector: 'co-borrower',
68
69
  component: Box,
69
70
  children: [{ model: model.$.name, component: Input, componentProps: { label: 'ФИО' } }],
70
71
  });
71
72
 
72
- export const creditSchema = (model: FormModel<CreditForm>) => ({
73
+ export const creditSchema = (model: FormModel<CreditForm>): FormSchemaNode => ({
73
74
  component: Box,
74
75
  children: [
75
76
  { model: model.$.loanType, component: SelectAsync, componentProps: { label: 'Тип' } }, // поле
@@ -80,7 +81,7 @@ export const creditSchema = (model: FormModel<CreditForm>) => ({
80
81
  componentProps: { title: 'Адрес регистрации' },
81
82
  children: [{ model: model.$.registrationAddress, part: address }], // подформа
82
83
  },
83
- { model: model.$.coBorrowers, component: FormArray, item: coBorrower }, // массив под-форм
84
+ { model: model.$.coBorrowers, component: FieldArray, item: coBorrower }, // массив под-форм
84
85
  ],
85
86
  });
86
87
  ```
@@ -94,11 +95,16 @@ export const creditSchema = (model: FormModel<CreditForm>) => ({
94
95
  | подформа | `model` — группа, `part` — функция |
95
96
  | контейнер | есть `children`, нет `model` |
96
97
 
97
- - Узел узнаётся по ЗНАЧЕНИЮ: в записи «имя поля → узел» поле данных может называться `model`,
98
- `item`, `part` или `value` — под таким ключом лежит обычный вложенный узел.
98
+ - Тип узла `FormSchemaNode` закрыт: опечатка в ключе, группа без `part`, `item` не на массиве и
99
+ привязка value-фасадом (`model: model.email`) не компилируются.
100
+ - Вложенные узлы читаются только из `children` и из поддерева `part`. В `componentProps` и под
101
+ произвольными ключами сборка не заглядывает; в dev она предупреждает о ключах узла, которые не
102
+ читает.
103
+ - Вид ноды формы определяет узел модели, а не имя: поле данных может называться `value`, `schema`,
104
+ `form`, `model` или `children`.
99
105
  - Поля подформы принадлежат той же форме: `form.registrationAddress.city`.
100
- - Билдер `item` / `part` вызывается один раз на под-модель; сборка и рендерер получают одно и то
101
- же поддерево (`schemaSubtree(builder, subModel)`).
106
+ - Билдер `item` / `part` вызывается один раз на под-модель; сборка и рендерер получают одну и ту
107
+ же компиляцию поддерева: `content` подформы и `row(rowModel)` массива.
102
108
  - `initialValue` узла-массива — запасной шаблон нового элемента для форм, чья модель создаётся из
103
109
  данных без кода. Шаблон модели (`arrayOf`) главнее.
104
110
 
@@ -108,24 +114,24 @@ export const creditSchema = (model: FormModel<CreditForm>) => ({
108
114
  import {
109
115
  apply,
110
116
  applyEach,
111
- cross,
112
- defineValidationSchema,
117
+ defineValidation,
113
118
  validate,
114
119
  validateWhen,
115
120
  } from '@reformer/core/validation';
116
121
 
117
- const addressRules = defineValidationSchema<Address>(({ model }) => {
122
+ const addressRules = defineValidation<Address>(({ model, cross }) => {
118
123
  validate(model.$.city, [required()]);
119
- cross<Address>(model.$.street, (address) =>
124
+ // cross — из аргумента схемы: снимок области (Address) выведен, приведения не нужны
125
+ cross(model.$.street, (address) =>
120
126
  address.city !== '' && address.street === '' ? streetRequired : null
121
127
  );
122
128
  });
123
129
 
124
- const coBorrowerRules = defineValidationSchema<CoBorrower>(({ model }) => {
130
+ const coBorrowerRules = defineValidation<CoBorrower>(({ model }) => {
125
131
  validate(model.$.name, [required()]);
126
132
  });
127
133
 
128
- export const contactsRules = defineValidationSchema<CreditForm>(({ model }) => {
134
+ export const contactsRules = defineValidation<CreditForm>(({ model }) => {
129
135
  apply(model.$.registrationAddress, addressRules);
130
136
  validateWhen(
131
137
  () => !model.sameAsRegistration,
@@ -136,13 +142,36 @@ export const contactsRules = defineValidationSchema<CreditForm>(({ model }) => {
136
142
  ```
137
143
 
138
144
  - У схемы, подключённой через `apply(ручка, схема)` / `applyEach`, своя область: `model` — под-модель,
139
- `cross` получает её снапшот. Захватывать снапшот в замыкание больше не нужно.
145
+ `cross` из аргумента схемы получает её снимок. Захватывать снимок в замыкание не нужно.
140
146
  - Одна и та же схема подключается и к группе, и к элементам массива.
141
147
  - `apply(model.$.a, rules)` принимает и массив ручек: `apply([model.$.a, model.$.b], rules)`.
142
148
  - `apply(schemaA, schemaB)` — композиция схем над той же моделью — остаётся.
143
- - Правило получает `(value, scope, root)`: модель области и корень прогона.
144
- - `each(model.items, (item) => …)` — прежняя запись `applyEach`: принимает фасад и ручку, области
145
- не создаёт.
149
+ - Правило значения — `Rule<T> = (value: T) => ValidationIssue | null`: один аргумент. Тип значения
150
+ сверяется с типом поля; набор правил объявляют типом поля (`Rule<LoanType>[]`,
151
+ `Rule<number | null>[]`).
152
+ - Текста у замечания нет: `ValidationIssue = { code, params?, severity? }`. Правило несёт код
153
+ (встроенный либо свой — `required({ code: 'street.required' })`), строку по коду даёт словарь
154
+ формы (`defineIssueMessages`) поверх словаря локали — см. `36-issue-messages.md`.
155
+ - `cross` берётся только из аргумента схемы: свободного оператора с таким именем в
156
+ `@reformer/core/validation` нет. Нет и `each` — правила строки подключает `applyEach`.
157
+
158
+ ### Результат прогона
159
+
160
+ ```typescript
161
+ const result = await credit.validate(); // либо credit.validateStep('loan'), runValidation(model, rules)
162
+ // result.status: 'valid' | 'invalid' | 'error' | 'cancelled'
163
+ // result.issues: Map<ручка поля, замечания>; result.failures: правила, не вернувшие результат
164
+
165
+ const outcome = await credit.submission.submit(sendApplication); // SubmitResult
166
+ // outcome.status: 'submitted' | 'invalid' | 'error' | 'cancelled' | 'failed'
167
+ ```
168
+
169
+ - `validateModel()` (раннер без сборки) отвечает `boolean`: `true` только для `valid`.
170
+ - **Сбой async-правила блокирует**: статус `error`, на поле — замечание `{ code: 'ruleFailed' }`.
171
+ - Отменённый прогон (`cancelled`) по нодам не разносится.
172
+ - Пока идут async-правила поля, его нода в `pending`; `credit.validating` — идёт ли проверка.
173
+ - У нод методов `validate()` / `submit()` нет: проверка и отправка — уровень формы, см.
174
+ `38-form-flow.md`.
146
175
 
147
176
  ## Поведение — модель, форма и схема
148
177
 
@@ -155,19 +184,27 @@ import {
155
184
  hideWhen,
156
185
  onComponentEvent,
157
186
  onMount,
187
+ renderEffect,
158
188
  } from '@reformer/core/behaviors';
159
189
 
160
190
  const addressBehavior = defineFormBehavior<Address>(({ model, schema }) => {
161
191
  hideWhen(schema.node('street'), () => model.city === ''); // узел ищется внутри части
162
192
  });
163
193
 
164
- export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, schema }) => {
194
+ export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, schema, flow }) => {
165
195
  const isMortgage = () => model.loanType === 'mortgage';
166
196
 
167
197
  enableWhen(model.$.propertyValue, isMortgage, { resetOnDisable: true });
168
198
  hideWhen(schema.node('mortgage'), () => !isMortgage());
169
- onComponentEvent(schema.node('wizard'), 'onSubmit', () => submitApplication(model.get()));
199
+ // визард в рендерере зовёт onSubmit через bundle.submission.submit — только после успешной
200
+ // проверки; values — модель целиком, с выключенными полями
201
+ onComponentEvent(schema.node('wizard'), 'onSubmit', (values: CreditForm) =>
202
+ sendApplication(values)
203
+ );
170
204
  onMount(schema.node('data-boundary'), () => void loadApplication(model));
205
+ renderEffect(schema, () => {
206
+ if (isMortgage()) flow?.goTo('loan'); // поток шагов сборки; без flow — undefined
207
+ });
171
208
 
172
209
  apply([model.$.registrationAddress, model.$.residenceAddress], addressBehavior);
173
210
  applyEach(model.$.coBorrowers, coBorrowerBehavior);
@@ -186,6 +223,8 @@ export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, sch
186
223
  | `schema.node(sel).setHidden / patchProps` | принудительная видимость и пропсы узла |
187
224
  | `schema.node(sel).getRef()` | ref на компонент узла — объект `{ current }` |
188
225
 
226
+ - Область поведения — `{ model, form, schema, flow }`: `flow` — поток шагов сборки, один на всю
227
+ сборку (его же получают поведения подформ и строк).
189
228
  - **Области изолированы.** `schema.node(selector)` ищет узел только в своей области: корневое
190
229
  поведение — в корневом дереве, поведение подформы и строки массива — в своём поддереве. До узла
191
230
  строки или части корень дотягивается через `applyEach` / `apply`. Два монтирования одной части
@@ -194,48 +233,62 @@ export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, sch
194
233
  - **Операторы узлов только записывают правило — исполняет его рендерер.** При разметке в JSX они
195
234
  ничего не делают: видимость, отправка и загрузка данных остаются в JSX.
196
235
  - Правило на селектор, которого нет в дереве, ничего не делает; в dev — предупреждение.
236
+ - **Несколько поведений на один узел.** Условия `hideWhen` на один селектор объединяются по «или»:
237
+ узел скрыт, пока истинно любое. Обработчики событий и хуки монтирования — стеком: действует
238
+ последний, его снятие возвращает прежний. Снятие поведения убирает только его правила.
239
+ - **`setHidden` / `patchProps` поведения откатываются при его снятии** — вместе с записанным в
240
+ его `onDispose`. Значение, которое с тех пор переписал кто-то другой, остаётся.
197
241
 
198
242
  ## Сборка — `createForm`
199
243
 
200
244
  ```tsx
201
- import { createForm, useFormBundle } from '@reformer/core';
245
+ import { createForm } from '@reformer/core';
246
+ import { useFormBundle } from '@reformer/core/react';
202
247
 
203
248
  const credit = useFormBundle(() =>
204
249
  createForm<CreditForm>({
205
250
  model: createCreditModel(), // либо initial: { … }
206
251
  schema: creditSchema, // (model) => узел; для JSON — документ + registry
207
252
  behavior: creditBehavior,
208
- validation: { steps: { loan: loanRules, contacts: contactsRules }, extras: crossRules },
253
+ validation: { extras: crossRules }, // правила вне шагов; голая схема = { rules }
254
+ flow: { steps: [{ id: 'loan', rules: loanRules }, { id: 'contacts', rules: contactsRules }] },
209
255
  })
210
256
  );
211
- // credit = { model, form, validation, render }
257
+ // credit = { model, form, render, flow, validating, submission, validate, validateStep, dispose }
212
258
 
213
259
  <FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />; // рендерер
214
260
  <FormField control={credit.form.loanType} />; // разметка в JSX — та же сборка
215
261
  ```
216
262
 
217
- - Порядок: модель → `seed` → дерево схемы → ноды формы → поведение → валидация → `setup`. Дерево
218
- строится один раз.
219
- - `validation` в бандле типизирована по конфигу: правила переданы — поле есть всегда.
220
- - `render` — `{ tree, controller, node(selector) }`: готовое дерево и схема-контроллер сборки.
263
+ - Порядок: модель → `seed` → дерево схемы → компиляция → уровень формы (правила, поток шагов) →
264
+ ноды формы → поведение → `setup`. Дерево строится и компилируется один раз. Ноды строятся по
265
+ виду узла модели: лист и массив-значение — `FormControl`, группа — `FormGroup`, массив
266
+ под-форм — `FormArray`.
267
+ - Повтор `selector` в одной области — исключение при сборке. В dev правило поведения на узел,
268
+ которого нет, и шаг `flow.steps` без узла с таким `selector` дают предупреждение.
269
+ - Уровень формы: `validate()` и `validateStep(id)` → `ValidationResult`, отправка — отдельный
270
+ объект `submission`: `submission.submit(handler)` → `SubmitResult`, `submission.submitting`;
271
+ `flow` — поток шагов (`next` / `previous` / `goTo`). См. `38-form-flow.md`.
272
+ - `dispose()` снимает сборку: поведение формы, подписки нод, живую проверку; идущие проверки
273
+ получают `cancelled`, значения модели остаются. Вне React владелец сборки зовёт его сам;
274
+ `useFormBundle` сборку при размонтировании не снимает.
275
+ - `render` — `{ tree, controller, node(selector) }`: скомпилированная схема (`CompiledSchema`, см.
276
+ `37-compiled-schema.md`) и схема-контроллер сборки.
221
277
  - JSON: `createForm({ model, schema: document, registry })` — дерево собирает
222
278
  `registry.resolveSchema`. Ядро документ не разбирает.
223
279
  - Внутри рендерера бандл доступен компонентам через `useFormBundleContext()` — так визард берёт
224
- форму и валидацию сам.
280
+ форму и поток шагов сам и отправляет через `bundle.submission.submit`.
225
281
 
226
- ### Низкоуровневые фабрики
282
+ ### Низкоуровневая фабрика
227
283
 
228
- | Задача | Фабрика |
229
- | ---------------------------------------------- | ---------------------------------------- |
230
- | форма из модели и готового дерева, без бандла | `createFormFromModel({ model, schema })` |
231
- | форма без модели — плоская схема полей | `createLegacyForm(schema)` |
284
+ `createFormFromModel({ model, schema })` — форма из модели и готового дерева, без бандла.
232
285
 
233
- `createForm` с готовым деревом вместо билдера или без модели бросает ошибку с отсылкой к нужной
234
- фабрике.
286
+ Формы без модели нет: значения принадлежат модели. `createForm` без `model` и `initial` либо с
287
+ готовым деревом вместо билдера бросает ошибку с подсказкой.
235
288
 
236
289
  ## Частые ошибки
237
290
 
238
- | Ошибка | Как правильно |
291
+ | ❌ Ошибка | Как правильно |
239
292
  | -------------------------------------------------- | ------------------------------------------------------- |
240
293
  | `model: model.address` (под-модель у поля) | `model: model.$.address.city` — привязка всегда с `$` |
241
294
  | `{ model: model.$.address, component }` без `part` | группа полем не бывает — нужен `part` |
@@ -244,3 +297,11 @@ const credit = useFormBundle(() =>
244
297
  | `push({})` для новой строки | `arrayOf(blank)` в модели и `push()` без значения |
245
298
  | `createModel(structuredClone(INITIAL))` с `arrayOf` | фабрика начальных значений — клон теряет шаблон массива |
246
299
  | `cross` в подформе читает корень | `apply(model.$.group, rules)` — область подформы |
300
+ | `{ value: model.$.x }`, `{ array: model.items }` | `{ model: model.$.x }`, `{ model: model.$.items, item }` |
301
+ | узлы записью `{ email: { model, component } }` | `{ children: [{ model, component }] }` |
302
+ | `Rule<unknown>[]` и `(form: Root)` с приведением | `Rule<ТипПоля>[]`, `cross` из аргумента схемы |
303
+ | `try { … } catch { return null }` в async-правиле «на всякий случай» | сбой правила блокирует сам; `catch` — только если проверка необязательна |
304
+ | `validation: { steps: { loan: rules } }` | `flow: { steps: [{ id: 'loan', rules }] }` |
305
+ | `required({ message: '…' })` | `required({ code: '<поле>.required' })` + `defineIssueMessages` |
306
+ | два узла с одним `selector` в одной области | разные селекторы — повтор бросает исключение |
307
+ | отправка `api.send(model.get())` в обход проверки | `bundle.submission.submit(api.send)` |
@@ -0,0 +1,284 @@
1
+ # Тексты замечаний валидации — коды, словарь формы и резолвер
2
+
3
+ ## Purpose
4
+
5
+ Замечание валидации текста не несёт. Правило сообщает **что** нарушено (`code`) и **с какими
6
+ числами** (`params`), а отображаемую строку по коду собирает резолвер в момент показа:
7
+
8
+ ```typescript
9
+ interface ValidationIssue {
10
+ readonly code: string; // встроенный ('required', 'minLength') либо свой ('loanAmount.required')
11
+ readonly params?: Readonly<Record<string, unknown>>; // порог правила, фактическое значение
12
+ readonly severity?: 'error' | 'warning'; // 'warning' показывается, но отправку не блокирует
13
+ }
14
+ ```
15
+
16
+ Отсюда два следствия:
17
+
18
+ - смена языка переводит уже показанные замечания — повторная валидация не нужна;
19
+ - одно и то же правило получает разный текст у разных полей: правилу дают свой код, а текст кода
20
+ кладут в словарь формы.
21
+
22
+ Опций `message` / `messageKey` у правил нет, у замечания нет поля `message` — см.
23
+ [17-nonexistent-api.md](./17-nonexistent-api.md).
24
+
25
+ ## API
26
+
27
+ ```typescript
28
+ // @reformer/core — опции фабрик @reformer/core/validators (второй или последний аргумент)
29
+ interface RuleOptions {
30
+ code?: string; // свой код вместо встроенного; принято `<поле>.<правило>`
31
+ params?: Readonly<Record<string, unknown>>; // доп. параметры — поверх параметров правила
32
+ }
33
+
34
+ // @reformer/core/i18n — без React
35
+ type IssueMessage =
36
+ | string // шаблон: 'Не меньше {minLength} символов', plural, select
37
+ | ((params: Readonly<Record<string, unknown>>, i18n: I18nHandle) => string);
38
+ type IssueMessages = Readonly<Record<string, IssueMessage>>; // код → текст
39
+
40
+ function defineIssueMessages<const M extends IssueMessages>(messages: M): M; // возвращает аргумент
41
+ function resolveIssueMessage(
42
+ issue: ValidationIssue,
43
+ i18n: I18nHandle, // createI18n(locale)
44
+ messages?: IssueMessages // словарь формы
45
+ ): string;
46
+
47
+ // @reformer/core/react
48
+ function useIssueMessage(messages?: IssueMessages): (issue: ValidationIssue) => string;
49
+
50
+ // @reformer/cdk — словарь формы для поддерева; FormField читает его сам
51
+ function ValidationMessagesProvider(props: {
52
+ messages?: IssueMessages;
53
+ resolver?: (issue: ValidationIssue) => string; // полное переопределение
54
+ children: ReactNode;
55
+ }): ReactNode;
56
+ function useIssueMessageResolver(): (issue: ValidationIssue) => string;
57
+ ```
58
+
59
+ ### Порядок поиска текста
60
+
61
+ | Шаг | Источник | Когда срабатывает |
62
+ | --- | ---------------------------------------------------------- | --------------------------------------------------- |
63
+ | 1 | словарь формы (`defineIssueMessages`) — запись по `code` | у поля свой текст |
64
+ | 2 | `validation.<code>` из словаря активной локали | встроенный код либо свой код в словаре приложения |
65
+ | 3 | `validation.<code>` из встроенной английской таблицы ядра | встроенный код без провайдера локали |
66
+ | 4 | сам `code` | свой код, которого нет ни в одном словаре |
67
+
68
+ Свой код **не** падает обратно на текст встроенного правила: `required({ code: 'phone.required' })`
69
+ без записи `'phone.required'` в словаре покажет строку `phone.required`. Каждый свой код получает
70
+ запись в словаре формы (или ключ `validation.<код>` в словаре приложения).
71
+
72
+ ### Встроенные коды и параметры
73
+
74
+ | Фабрика | `code` | `params` |
75
+ | ------------------------------- | ------------------ | ------------------------------------------ |
76
+ | `required()` | `required` | — |
77
+ | `min(n)` / `max(n)` | `min` / `max` | `min` / `max`, `actual` |
78
+ | `minLength(n)` / `maxLength(n)` | `minLength` / `maxLength`; у массива — `minItems` / `maxItems` | `minLength` / `maxLength`, `actualLength` |
79
+ | `email()` | `email` | — |
80
+ | `pattern(re)` | `pattern` | `pattern` (исходник регулярки) |
81
+ | `url()` | `url`, `url_protocol` | `allowedProtocols` (у `url_protocol`) |
82
+ | `phone()` | `phone` | `format` |
83
+ | `isNumber()` / `integer()` | `isNumber` / `integer` | — |
84
+ | `multipleOf(n)` | `multipleOf` | `multipleOf` |
85
+ | `nonNegative()` / `nonZero()` | `nonNegative` / `nonZero` | — |
86
+ | `isDate()` | `date_invalid` | — |
87
+ | `minDate(d)` / `maxDate(d)` | `date_min` / `date_max` | `minDate` / `maxDate` |
88
+ | `pastDate()` / `futureDate()` | `date_future` / `date_past` | — (код называет нарушение) |
89
+ | `minAge(n)` / `maxAge(n)` | `date_min_age` / `date_max_age` | `minAge` / `maxAge`, `currentAge` |
90
+ | `minAge(n)`, рождение в будущем | `date_future` | `minAge` |
91
+ | `fileType(accept)` | `fileType` | `accept`, `fileName` |
92
+ | `maxFileSize(b)` / `minFileSize(b)` | `maxFileSize` / `minFileSize` | `maxFileSize` / `minFileSize`, `fileName`, `actualSize` |
93
+ | `maxFiles(n)` / `minFiles(n)` | `maxFiles` / `minFiles` | `maxFiles` / `minFiles`, `actualCount` |
94
+ | `maxTotalFileSize(b)` | `maxTotalFileSize` | `maxTotalFileSize`, `actualTotal` |
95
+ | отклонённое async-правило | `ruleFailed` | — |
96
+
97
+ Свой код (`{ code }`) заменяет только код: параметры правила остаются, `params` из опций
98
+ накладываются поверх них.
99
+
100
+ ### Шаблон и функция
101
+
102
+ - Шаблон — тот же формат, что в словарях локали (подмножество ICU): подстановка `{minLength}`,
103
+ `{count, plural, one{…} other{…}}`, `{kind, select, …}`.
104
+ - Параметры готовятся до подстановки: `Date` — по языку, массив — перечнем, размеры в байтах
105
+ (`maxFileSize`, `minFileSize`, `actualSize`, `maxTotalFileSize`, `actualTotal`) — строкой с
106
+ единицей. Числа остаются числами (иначе не сработал бы `plural`) и печатаются **без разрядов**.
107
+ - Число с разрядами, валюта, вычисление — функция `(params, i18n) => string`; `i18n` — ручка
108
+ активной локали (`i18n.number`, `i18n.date`, `i18n.t`).
109
+ - Битый шаблон в словаре формы текст поля не прячет: резолвер идёт дальше по порядку поиска.
110
+
111
+ ## Examples
112
+
113
+ ### Свой код у правила и словарь формы
114
+
115
+ ```typescript
116
+ // validation/loan.ts — правила несут коды
117
+ import { defineValidation, validate } from '@reformer/core/validation';
118
+ import { max, min, minLength, required } from '@reformer/core/validators';
119
+
120
+ export const loanRules = defineValidation<CreditForm>(({ model }) => {
121
+ validate(model.$.loanAmount, [
122
+ required({ code: 'loanAmount.required' }),
123
+ min(50_000, { code: 'loanAmount.min' }),
124
+ ]);
125
+ validate(model.$.loanPurpose, [minLength(10, { code: 'loanPurpose.minLength' })]);
126
+ validate(model.$.carYear, [max(new Date().getFullYear(), { code: 'carYear.max' })]);
127
+ validate(model.$.email, [required()]); // встроенный код: текст даёт локаль
128
+ });
129
+ ```
130
+
131
+ ```typescript
132
+ // validation/messages.ts — тексты по кодам; константа модуля (стабильная ссылка)
133
+ import { defineIssueMessages } from '@reformer/core/i18n';
134
+
135
+ export const creditMessages = defineIssueMessages({
136
+ 'loanAmount.required': 'Укажите сумму кредита',
137
+ 'loanAmount.min': (params, i18n) => `Минимум ${i18n.number(Number(params.min))} ₽`,
138
+ 'loanPurpose.minLength': 'Минимум {minLength} символов',
139
+ 'carYear.max': 'Не позднее {max}',
140
+ // встроенный код тоже можно переопределить — для всей формы
141
+ required: 'Заполните поле',
142
+ });
143
+ ```
144
+
145
+ ### Подключение словаря — провайдер cdk
146
+
147
+ `FormField` из `@reformer/cdk` и `@reformer/ui-kit` резолвит замечания сам; словарь формы
148
+ подключается один раз на корне формы:
149
+
150
+ ```tsx
151
+ import { ValidationMessagesProvider } from '@reformer/cdk';
152
+ import { creditMessages } from './validation/messages';
153
+
154
+ <ValidationMessagesProvider messages={creditMessages}>
155
+ <FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />
156
+ </ValidationMessagesProvider>;
157
+ ```
158
+
159
+ - Вложенные провайдеры с `messages` накладываются друг на друга: ближний важнее, код, которого в
160
+ ближнем словаре нет, ищется во внешнем, затем в локали.
161
+ - `resolver` заменяет резолвер поддерева целиком: локаль и внешние провайдеры для его замечаний не
162
+ участвуют (кроме `messages` этого же провайдера).
163
+ - Язык задаёт `I18nProvider` из `@reformer/core/react`; без него тексты английские — см.
164
+ [34-i18n.md](./34-i18n.md).
165
+
166
+ ### Свой компонент замечаний — `useIssueMessage`
167
+
168
+ ```tsx
169
+ import type { FormControl } from '@reformer/core';
170
+ import { useFormControl, useIssueMessage } from '@reformer/core/react';
171
+ import { creditMessages } from './validation/messages';
172
+
173
+ function FieldIssues({ control }: { control: FormControl<string> }) {
174
+ const message = useIssueMessage(creditMessages); // словарь формы → локаль → английский → код
175
+ // shouldShowIssues — и у поля с одним предупреждением; shouldShowError — только у невалидного
176
+ const { issues, shouldShowIssues } = useFormControl(control);
177
+ if (!shouldShowIssues) return null;
178
+ return (
179
+ <ul role="alert">
180
+ {issues.map((issue) => (
181
+ <li key={issue.code}>{message(issue)}</li>
182
+ ))}
183
+ </ul>
184
+ );
185
+ }
186
+ ```
187
+
188
+ Внутри дерева с `ValidationMessagesProvider` — `useIssueMessageResolver()` из `@reformer/cdk`: он
189
+ учитывает словари всех провайдеров выше.
190
+
191
+ ### Вне React
192
+
193
+ ```typescript
194
+ import { createI18n, resolveIssueMessage } from '@reformer/core/i18n';
195
+ import { ru } from '@reformer/core/locale/ru';
196
+
197
+ const i18n = createI18n(ru);
198
+ resolveIssueMessage({ code: 'minLength', params: { minLength: 8 } }, i18n); // 'Не меньше 8 символов'
199
+ resolveIssueMessage({ code: 'loanAmount.required' }, i18n, creditMessages); // 'Укажите сумму кредита'
200
+ ```
201
+
202
+ ### Замечание из своего правила и с сервера
203
+
204
+ Своё правило, `cross` и ответ сервера возвращают тот же `ValidationIssue` — код и параметры; текст
205
+ кода — в словаре формы:
206
+
207
+ ```typescript
208
+ const initialPaymentTooHigh = (loan: CreditForm): ValidationIssue | null =>
209
+ loan.initialPayment !== null &&
210
+ loan.propertyValue !== null &&
211
+ loan.initialPayment > loan.propertyValue
212
+ ? { code: 'initialPaymentTooHigh', params: { max: loan.propertyValue } }
213
+ : null;
214
+
215
+ // ответ сервера — в ноду поля
216
+ form.email.setIssues([{ code: 'email.taken' }]);
217
+ ```
218
+
219
+ ### Несколько языков в словаре формы
220
+
221
+ Запись словаря — функция, получающая ручку локали, поэтому текст поля можно взять из словаря
222
+ приложения и переключать вместе с языком:
223
+
224
+ ```typescript
225
+ export const creditMessages = defineIssueMessages({
226
+ 'loanAmount.required': (_params, i18n) => i18n.t('credit.loanAmount.required'),
227
+ });
228
+ ```
229
+
230
+ Другой путь — ключ `validation.loanAmount.required` в словаре приложения (шаг 2 порядка поиска).
231
+
232
+ ## Anti-patterns
233
+
234
+ ```typescript
235
+ // ❌ Текст в правиле: у RuleOptions нет message — не скомпилируется
236
+ required({ message: 'Укажите телефон' });
237
+
238
+ // ✅ Свой код в правиле, текст — в словаре формы
239
+ required({ code: 'phone.required' });
240
+ const messages = defineIssueMessages({ 'phone.required': 'Укажите телефон' });
241
+ ```
242
+
243
+ ```tsx
244
+ // ❌ Словарь, собранный в рендере: новая ссылка — резолвер пересоздаётся на каждый рендер
245
+ const message = useIssueMessage(defineIssueMessages({ 'phone.required': 'Укажите телефон' }));
246
+
247
+ // ✅ Словарь — константа модуля
248
+ const messages = defineIssueMessages({ 'phone.required': 'Укажите телефон' });
249
+ function PhoneIssues() {
250
+ const message = useIssueMessage(messages);
251
+ /* … */
252
+ }
253
+ ```
254
+
255
+ ```tsx
256
+ // ❌ Показ замечания как строки: текста у замечания нет
257
+ <span>{issues[0].message}</span>
258
+
259
+ // ✅ Текст — через резолвер
260
+ <span>{message(issues[0])}</span>
261
+ ```
262
+
263
+ ```typescript
264
+ // ❌ Свой код без записи в словаре — на экране сам код «loanAmount.required»
265
+ validate(model.$.loanAmount, [required({ code: 'loanAmount.required' })]);
266
+
267
+ // ✅ Каждому своему коду — запись в словаре формы (или validation.<код> в локали приложения)
268
+ defineIssueMessages({ 'loanAmount.required': 'Укажите сумму кредита' });
269
+ ```
270
+
271
+ ```typescript
272
+ // ❌ Число с разрядами шаблоном — шаблон печатает 50000 без пробелов
273
+ 'loanAmount.min': 'Минимум {min} ₽',
274
+
275
+ // ✅ Функция форматирует число сама
276
+ 'loanAmount.min': (params, i18n) => `Минимум ${i18n.number(Number(params.min))} ₽`,
277
+ ```
278
+
279
+ ## See also
280
+
281
+ - [34-i18n.md](./34-i18n.md) — локаль, загрузка словарей, `I18nProvider`
282
+ - [03-api-signatures.md](./03-api-signatures.md) — фабрики правил и `RuleOptions`
283
+ - [38-form-flow.md](./38-form-flow.md) — `bundle.submission.submit` и проверка шагов
284
+ - [17-nonexistent-api.md](./17-nonexistent-api.md) — удалённые `message`, `messageKey`, `ValidationError`