@reformer/core 11.0.0-beta.1 → 11.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/README.md +90 -68
  2. package/dist/behaviors-value-D05z3my6.js +109 -0
  3. package/dist/behaviors.d.ts +1 -1
  4. package/dist/behaviors.js +376 -211
  5. package/dist/create-model-DPSG3I0O.js +30 -0
  6. package/dist/date-utils-90-qZbse.js +43 -0
  7. package/dist/en-HP8XEych.js +44 -0
  8. package/dist/form/aggregate-signals.d.ts +21 -63
  9. package/dist/form/behaviors/collections.d.ts +84 -0
  10. package/dist/form/behaviors/context.d.ts +56 -0
  11. package/dist/form/behaviors/index.d.ts +30 -0
  12. package/dist/form/behaviors/internals.d.ts +41 -0
  13. package/dist/form/{behaviors-node.d.ts → behaviors/node.d.ts} +28 -5
  14. package/dist/form/behaviors/operators.d.ts +59 -0
  15. package/dist/form/behaviors/schema-operators.d.ts +83 -0
  16. package/dist/form/behaviors/types.d.ts +37 -0
  17. package/dist/form/create-form.d.ts +59 -70
  18. package/dist/form/flow/flow.d.ts +87 -0
  19. package/dist/form/form-bundle.d.ts +153 -0
  20. package/dist/form/form-level.d.ts +121 -0
  21. package/dist/form/form-proxy-builder.d.ts +6 -22
  22. package/dist/form/index.d.ts +23 -24
  23. package/dist/form/nodes/form-array-node.d.ts +100 -0
  24. package/dist/form/nodes/form-control-node.d.ts +73 -0
  25. package/dist/form/nodes/form-group-node.d.ts +66 -0
  26. package/dist/form/nodes/node-kind.d.ts +29 -0
  27. package/dist/form/nodes/state/availability.d.ts +6 -0
  28. package/dist/form/nodes/state/component.d.ts +6 -0
  29. package/dist/form/nodes/state/index.d.ts +12 -0
  30. package/dist/form/nodes/state/interaction.d.ts +13 -0
  31. package/dist/form/nodes/state/issues.d.ts +9 -0
  32. package/dist/form/nodes/state/pending.d.ts +6 -0
  33. package/dist/form/schema/compile.d.ts +59 -0
  34. package/dist/form/schema/compiled.d.ts +143 -0
  35. package/dist/form/schema/row-key.d.ts +11 -0
  36. package/dist/form/schema-controller.d.ts +159 -0
  37. package/dist/form/schema-subtree.d.ts +28 -0
  38. package/dist/form/signal-node-registry.d.ts +27 -22
  39. package/dist/form/submission.d.ts +93 -0
  40. package/dist/form/type-guards.d.ts +14 -73
  41. package/dist/form/types/contracts.d.ts +21 -85
  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 +25 -17
  47. package/dist/form/validation/context.d.ts +67 -0
  48. package/dist/form/validation/index.d.ts +24 -28
  49. package/dist/form/validation/operators.d.ts +101 -0
  50. package/dist/form/validation/run.d.ts +120 -0
  51. package/dist/form/validation/strategy.d.ts +89 -0
  52. package/dist/form/validation/types.d.ts +83 -0
  53. package/dist/form/validators/date-utils.d.ts +42 -0
  54. package/dist/form/validators/email.d.ts +23 -0
  55. package/dist/form/{validation/validators → validators}/file-type.d.ts +10 -10
  56. package/dist/form/{validation/validators → validators}/file-utils.d.ts +3 -2
  57. package/dist/form/validators/future-date.d.ts +23 -0
  58. package/dist/form/{validation/validators → validators}/index.d.ts +7 -4
  59. package/dist/form/validators/integer.d.ts +22 -0
  60. package/dist/form/validators/is-date.d.ts +24 -0
  61. package/dist/form/validators/is-number.d.ts +25 -0
  62. package/dist/form/validators/max-age.d.ts +28 -0
  63. package/dist/form/validators/max-date.d.ts +25 -0
  64. package/dist/form/{validation/validators → validators}/max-file-size.d.ts +10 -10
  65. package/dist/form/validators/max-files.d.ts +26 -0
  66. package/dist/form/validators/max-length.d.ts +33 -0
  67. package/dist/form/validators/max-total-file-size.d.ts +26 -0
  68. package/dist/form/validators/max.d.ts +26 -0
  69. package/dist/form/validators/min-age.d.ts +27 -0
  70. package/dist/form/validators/min-date.d.ts +25 -0
  71. package/dist/form/{validation/validators → validators}/min-file-size.d.ts +10 -10
  72. package/dist/form/validators/min-files.d.ts +27 -0
  73. package/dist/form/validators/min-length.d.ts +33 -0
  74. package/dist/form/validators/min.d.ts +26 -0
  75. package/dist/form/validators/multiple-of.d.ts +25 -0
  76. package/dist/form/validators/non-negative.d.ts +22 -0
  77. package/dist/form/validators/non-zero.d.ts +22 -0
  78. package/dist/form/validators/past-date.d.ts +23 -0
  79. package/dist/form/validators/pattern.d.ts +26 -0
  80. package/dist/form/{validation/validators → validators}/phone.d.ts +11 -11
  81. package/dist/form/validators/required.d.ts +29 -0
  82. package/dist/form/validators/url.d.ts +45 -0
  83. package/dist/form/validators/validation-issue.d.ts +8 -0
  84. package/dist/i18n/descriptor.d.ts +54 -0
  85. package/dist/i18n/en.json.d.ts +49 -0
  86. package/dist/i18n/loader.d.ts +71 -0
  87. package/dist/i18n/locale-store.d.ts +46 -0
  88. package/dist/i18n/locale.d.ts +90 -0
  89. package/dist/i18n/message-format.d.ts +136 -0
  90. package/dist/i18n/resolve-localized.d.ts +17 -0
  91. package/dist/i18n/ru.json.d.ts +49 -0
  92. package/dist/i18n/translator.d.ts +61 -0
  93. package/dist/i18n/validation-message.d.ts +53 -0
  94. package/dist/i18n.d.ts +26 -0
  95. package/dist/i18n.js +86 -0
  96. package/dist/index.d.ts +17 -5
  97. package/dist/index.js +942 -2601
  98. package/dist/loader-BWkigpVe.js +52 -0
  99. package/dist/locale/en.d.ts +2 -0
  100. package/dist/locale/en.js +5 -0
  101. package/dist/locale/en.json +45 -0
  102. package/dist/locale/index.d.ts +18 -0
  103. package/dist/locale/ru.d.ts +2 -0
  104. package/dist/locale/ru.js +44 -0
  105. package/dist/locale/ru.json +45 -0
  106. package/dist/locale-CG54AU2Q.js +219 -0
  107. package/dist/locale.d.ts +2 -0
  108. package/dist/locale.js +9 -0
  109. package/dist/{state → model}/behaviors-value.d.ts +24 -6
  110. package/dist/{state/form-model.d.ts → model/create-model.d.ts} +18 -4
  111. package/dist/{state → model}/derived-registry.d.ts +1 -1
  112. package/dist/model/index.d.ts +42 -0
  113. package/dist/model/model-nodes.d.ts +190 -0
  114. package/dist/model/model-signals-proxy.d.ts +63 -0
  115. package/dist/model/model-value-proxy.d.ts +84 -0
  116. package/dist/{state → model}/safe-effect.d.ts +50 -2
  117. package/dist/model/types.d.ts +239 -0
  118. package/dist/model.d.ts +2 -0
  119. package/dist/model.js +27 -0
  120. package/dist/node-kind-C-r6ZMdo.js +15 -0
  121. package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
  122. package/dist/platforms/react/field-binding/field-handle.d.ts +53 -0
  123. package/dist/platforms/react/form-bundle-context.d.ts +27 -0
  124. package/dist/{form → platforms/react}/hooks/types.d.ts +58 -36
  125. package/dist/platforms/react/hooks/use-form-bundle.d.ts +41 -0
  126. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  127. package/dist/{form → platforms/react}/hooks/use-form-validation.d.ts +13 -7
  128. package/dist/{form → platforms/react}/hooks/useArrayLength.d.ts +6 -6
  129. package/dist/{form → platforms/react}/hooks/useFormControl.d.ts +7 -9
  130. package/dist/{form → platforms/react}/hooks/useFormControlValue.d.ts +18 -18
  131. package/dist/{form → platforms/react}/hooks/useSignalSubscription.d.ts +9 -1
  132. package/dist/platforms/react/i18n/context.d.ts +18 -0
  133. package/dist/platforms/react/i18n/hooks.d.ts +65 -0
  134. package/dist/platforms/react/i18n/provider.d.ts +59 -0
  135. package/dist/platforms/react/index.d.ts +30 -0
  136. package/dist/react.d.ts +22 -0
  137. package/dist/react.js +505 -0
  138. package/dist/runtime-token.d.ts +1 -2
  139. package/dist/safe-effect-BazLOv1g.js +669 -0
  140. package/dist/schema-controller-CpiO2JoK.js +143 -0
  141. package/dist/signals.d.ts +15 -9
  142. package/dist/signals.js +11 -6
  143. package/dist/strategy-DzvnsWLh.js +375 -0
  144. package/dist/type-guards-D-N_5Ddh.js +19 -0
  145. package/dist/validation-issue-B9mjI8jz.js +7 -0
  146. package/dist/validation-message-Ced9MGxs.js +194 -0
  147. package/dist/validation.d.ts +1 -1
  148. package/dist/validation.js +11 -165
  149. package/dist/validators/email.d.ts +1 -1
  150. package/dist/validators/email.js +5 -8
  151. package/dist/validators/future-date.d.ts +1 -1
  152. package/dist/validators/future-date.js +10 -13
  153. package/dist/validators/integer.d.ts +1 -1
  154. package/dist/validators/integer.js +4 -7
  155. package/dist/validators/is-date.d.ts +1 -1
  156. package/dist/validators/is-date.js +5 -8
  157. package/dist/validators/is-number.d.ts +1 -1
  158. package/dist/validators/is-number.js +4 -7
  159. package/dist/validators/max-age.d.ts +1 -1
  160. package/dist/validators/max-age.js +8 -12
  161. package/dist/validators/max-date.d.ts +1 -1
  162. package/dist/validators/max-date.js +10 -13
  163. package/dist/validators/max-length.d.ts +1 -1
  164. package/dist/validators/max-length.js +7 -10
  165. package/dist/validators/max.d.ts +1 -1
  166. package/dist/validators/max.js +4 -7
  167. package/dist/validators/min-age.d.ts +1 -1
  168. package/dist/validators/min-age.js +10 -11
  169. package/dist/validators/min-date.d.ts +1 -1
  170. package/dist/validators/min-date.js +8 -11
  171. package/dist/validators/min-length.d.ts +1 -1
  172. package/dist/validators/min-length.js +4 -7
  173. package/dist/validators/min.d.ts +1 -1
  174. package/dist/validators/min.js +4 -7
  175. package/dist/validators/multiple-of.d.ts +1 -1
  176. package/dist/validators/multiple-of.js +11 -10
  177. package/dist/validators/non-negative.d.ts +1 -1
  178. package/dist/validators/non-negative.js +2 -5
  179. package/dist/validators/non-zero.d.ts +1 -1
  180. package/dist/validators/non-zero.js +4 -7
  181. package/dist/validators/past-date.d.ts +1 -1
  182. package/dist/validators/past-date.js +10 -13
  183. package/dist/validators/pattern.d.ts +1 -1
  184. package/dist/validators/pattern.js +5 -8
  185. package/dist/validators/phone.d.ts +1 -1
  186. package/dist/validators/phone.js +10 -11
  187. package/dist/validators/required.d.ts +1 -1
  188. package/dist/validators/required.js +4 -7
  189. package/dist/validators/url.d.ts +1 -1
  190. package/dist/validators/url.js +48 -17
  191. package/dist/validators.d.ts +1 -1
  192. package/dist/validators.js +86 -108
  193. package/docs/llms/01-api-reference.md +60 -34
  194. package/docs/llms/02-quick-start.md +112 -74
  195. package/docs/llms/03-api-signatures.md +369 -148
  196. package/docs/llms/04-common-patterns.md +58 -56
  197. package/docs/llms/05-common-mistakes.md +69 -44
  198. package/docs/llms/06-troubleshooting.md +14 -9
  199. package/docs/llms/07-complete-import.md +74 -43
  200. package/docs/llms/08-form-types.md +11 -4
  201. package/docs/llms/09-formschema.md +132 -84
  202. package/docs/llms/10-arrays.md +141 -61
  203. package/docs/llms/11-async-watchfield.md +2 -2
  204. package/docs/llms/13-multi-step.md +134 -63
  205. package/docs/llms/14-extended-mistakes.md +20 -17
  206. package/docs/llms/15-project-structure.md +224 -70
  207. package/docs/llms/16-ui-components.md +86 -46
  208. package/docs/llms/17-nonexistent-api.md +217 -32
  209. package/docs/llms/18-conditional-fields.md +71 -32
  210. package/docs/llms/19-reading-values.md +45 -4
  211. package/docs/llms/20-compute-vs-watch.md +25 -10
  212. package/docs/llms/21-array-operations.md +76 -26
  213. package/docs/llms/22-cycle-detection.md +1 -1
  214. package/docs/llms/23-copy-from.md +2 -2
  215. package/docs/llms/24-sync-fields.md +6 -5
  216. package/docs/llms/25-reset-when.md +14 -7
  217. package/docs/llms/26-transform-value.md +4 -4
  218. package/docs/llms/27-revalidate-when.md +16 -15
  219. package/docs/llms/28-submit-and-reset.md +278 -128
  220. package/docs/llms/29-async-preload.md +41 -31
  221. package/docs/llms/30-type-safety-recipes.md +105 -51
  222. package/docs/llms/31-async-validator-debounce.md +96 -53
  223. package/docs/llms/33-validation-strategy.md +178 -66
  224. package/docs/llms/34-i18n.md +298 -0
  225. package/docs/llms/35-unified-contract.md +309 -0
  226. package/docs/llms/36-issue-messages.md +284 -0
  227. package/docs/llms/37-compiled-schema.md +332 -0
  228. package/docs/llms/38-form-flow.md +396 -0
  229. package/llms-index.json +1 -0
  230. package/llms.txt +10690 -3089
  231. package/package.json +48 -9
  232. package/dist/behaviors-node-DGEFMTQu.js +0 -18
  233. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  234. package/dist/date-utils-dTT_x_Be.js +0 -31
  235. package/dist/derived-registry-CluZCQ7U.js +0 -16
  236. package/dist/form/behaviors.d.ts +0 -133
  237. package/dist/form/error-handler.d.ts +0 -166
  238. package/dist/form/factories/index.d.ts +0 -6
  239. package/dist/form/factories/node-factory.d.ts +0 -188
  240. package/dist/form/form-submitter.d.ts +0 -121
  241. package/dist/form/nodes/array-node.d.ts +0 -276
  242. package/dist/form/nodes/field-node.d.ts +0 -294
  243. package/dist/form/nodes/form-node.d.ts +0 -326
  244. package/dist/form/nodes/group-node.d.ts +0 -284
  245. package/dist/form/nodes/model-array-node.d.ts +0 -121
  246. package/dist/form/status-machine.d.ts +0 -115
  247. package/dist/form/types/deep-schema.d.ts +0 -107
  248. package/dist/form/types/form-proxy.d.ts +0 -141
  249. package/dist/form/unique-id.d.ts +0 -45
  250. package/dist/form/validation/validators/date-utils.d.ts +0 -30
  251. package/dist/form/validation/validators/email.d.ts +0 -23
  252. package/dist/form/validation/validators/future-date.d.ts +0 -23
  253. package/dist/form/validation/validators/integer.d.ts +0 -22
  254. package/dist/form/validation/validators/is-date.d.ts +0 -23
  255. package/dist/form/validation/validators/is-number.d.ts +0 -24
  256. package/dist/form/validation/validators/max-age.d.ts +0 -25
  257. package/dist/form/validation/validators/max-date.d.ts +0 -25
  258. package/dist/form/validation/validators/max-files.d.ts +0 -26
  259. package/dist/form/validation/validators/max-length.d.ts +0 -27
  260. package/dist/form/validation/validators/max-total-file-size.d.ts +0 -26
  261. package/dist/form/validation/validators/max.d.ts +0 -25
  262. package/dist/form/validation/validators/min-age.d.ts +0 -25
  263. package/dist/form/validation/validators/min-date.d.ts +0 -25
  264. package/dist/form/validation/validators/min-files.d.ts +0 -26
  265. package/dist/form/validation/validators/min-length.d.ts +0 -27
  266. package/dist/form/validation/validators/min.d.ts +0 -25
  267. package/dist/form/validation/validators/multiple-of.d.ts +0 -24
  268. package/dist/form/validation/validators/non-negative.d.ts +0 -22
  269. package/dist/form/validation/validators/non-zero.d.ts +0 -22
  270. package/dist/form/validation/validators/past-date.d.ts +0 -23
  271. package/dist/form/validation/validators/pattern.d.ts +0 -33
  272. package/dist/form/validation/validators/required.d.ts +0 -32
  273. package/dist/form/validation/validators/url.d.ts +0 -43
  274. package/dist/form/validation-schema.d.ts +0 -106
  275. package/dist/form/validation-strategy.d.ts +0 -55
  276. package/dist/form/validators.d.ts +0 -11
  277. package/dist/form-model-DYCzGGKF.js +0 -286
  278. package/dist/state/index.d.ts +0 -31
  279. package/dist/state/subscription-manager.d.ts +0 -180
  280. package/dist/state/types.d.ts +0 -144
  281. package/dist/state.d.ts +0 -2
  282. package/dist/state.js +0 -21
  283. package/dist/subscription-manager-705bls97.js +0 -171
@@ -2,42 +2,59 @@
2
2
 
3
3
  One form is one module folder. **Default layout — flat minimalist:** every concern is a single
4
4
  file at the module root, and the whole form (entry + all wizard steps inline) lives in one
5
- `index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
6
- carried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a
7
- render-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add
8
- `renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`
9
- nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
10
- and rendered with `FormArraySection` — no per-step component files.
5
+ `index.tsx`. Naming rule: **`form.<role>` is a form artefact, the suffix names its role** —
6
+ `form.schema.*` (layout), `form.behavior.ts` (form behavior: model operators AND schema-node rules
7
+ such as `hideWhen` / `onComponentEvent`), `form.validation.ts` (validation rules), and the optional
8
+ `form.flow.ts` (the steps of a wizard) and `form.messages.ts` (the issue-message dictionary); every
9
+ other file has no prefix. There is one behavior per form:
10
+ the former render-layer file `form.render.ts` (and `renderer.behavior.ts`) is retired — its node
11
+ rules move into `form.behavior.ts`. The old `renderer.schema.*` / `validation.ts` names, and the
12
+ retired `form.render.ts` / `wizard.tsx` / `renderer.wizard.tsx`, are accepted with a warning. No
13
+ folders until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts` and
14
+ rendered with `FieldArray` / `FieldArraySection` — no per-step component files.
11
15
 
12
16
  ```
13
17
  src/
14
- ├── components/ui/ # App-wide reusable UI (FormField, FormArraySection, ...)
18
+ ├── components/ui/ # App-wide reusable UI (FormField, FieldArraySection, ...)
15
19
  │
16
20
  ├── forms/
17
21
  │ └── [form-name]/ # Form module — flat, one file per concern
18
- │ ├── index.tsx # entry + whole form: createModel→createForm→<FormWizard> with all steps inline; arrays via FormArraySection
19
- │ ├── types.ts # form type + enums + { value, label } option type + constant dictionaries
20
- │ ├── model.ts # createModel + initial values + empty-array-element factories
21
- │ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
22
- │ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
23
- │ ├── validation.ts # ALL validation → { validateStep, validateAll }
22
+ │ ├── index.tsx # entry + whole form: ONE createForm call → <FormWizard flow={bundle.flow}> with all steps inline
23
+ │ ├── types.ts # form type (interface or type) + enums + { value, label } option type + constant dictionaries
24
+ │ ├── model.ts # createModel + initial values + arrayOf(blank) templates of array rows
25
+ │ ├── form.schema.ts # schema builder (model) => node tree: { model: model.$.x, component, componentProps }
26
+ │ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / copyFrom / onChange + hideWhen / onComponentEvent
27
+ │ ├── form.validation.ts # rules of each step (exported) + extras
28
+ │ ├── form.flow.ts # optional, wizard: flow { steps: [{ id, rules }] }
29
+ │ ├── form.messages.ts # optional: defineIssueMessages dictionary (issue texts by code)
24
30
  │ ├── data-sources.ts # options + async loaders (dataSources)
25
31
  │ └── api.ts # submit + prefill/load
26
32
  ```
27
33
 
34
+ Optionally a wizard may keep its steps in folders: `steps/index.ts` (step order) and one
35
+ `steps/<slug>/` folder per step (`<slug>` — kebab-case of the step title, no number) with the same
36
+ file names — `form.schema.ts`, `form.validation.ts`, `form.behavior.ts` — for the step only;
37
+ everything that crosses steps stays in the root files.
38
+
28
39
  Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
29
- `index.tsx`; validation is a single `validation.ts`.** This is the working default for
40
+ `index.tsx`; validation rules are a single `form.validation.ts`, the steps — `form.flow.ts`.** This is the working default for
30
41
  almost every form — reach for folders only when a file stops fitting on a screen.
31
42
 
32
43
  ### Key Files
33
44
 
34
45
  ```typescript
35
- // forms/credit-application/types.ts
36
- export type CreditApplicationForm = {
46
+ // forms/credit-application/types.ts — `interface` and `type` both work
47
+ export interface CreditApplication {
37
48
  loanType: LoanType;
38
- loanAmount: number | null;
49
+ loanAmount: number | null; // an empty number input is `null`
39
50
  // ...
40
- };
51
+ }
52
+ // computed fields (targets of `compute`) — a separate type; the form is the intersection
53
+ export interface CreditApplicationDerived {
54
+ monthlyPayment: number;
55
+ // ...
56
+ }
57
+ export type CreditApplicationForm = CreditApplication & CreditApplicationDerived;
41
58
 
42
59
  // forms/credit-application/model.ts
43
60
  import { createModel, type FormModel } from '@reformer/core';
@@ -45,81 +62,218 @@ export const createCreditApplicationModel = (): FormModel<CreditApplicationForm>
45
62
  createModel<CreditApplicationForm>(createInitialCreditApplication());
46
63
 
47
64
  // forms/credit-application/form.schema.ts
48
- import type { FormModel } from '@reformer/core';
49
- export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
50
- loanType: { value: model.$.loanType, component: Select, componentProps: { /* ... */ } },
51
- personalData: personalDataNodes(model.$.personalData),
52
- properties: { array: model.properties, item: propertyItem },
65
+ import type { FormModel, FormSchemaNode } from '@reformer/core';
66
+ export const creditApplicationSchema = (
67
+ model: FormModel<CreditApplicationForm>
68
+ ): FormSchemaNode => ({
69
+ // Nested nodes live in `children` only — not in componentProps, not under arbitrary keys.
70
+ children: [
71
+ { model: model.$.loanType, component: SelectAsync, componentProps: { /* ... */ } }, // field
72
+ { model: model.$.registrationAddress, part: address }, // sub-form: group + part
73
+ { model: model.$.properties, component: FieldArray, item: propertyRow }, // array of sub-forms
74
+ ],
53
75
  });
54
76
 
55
- // forms/credit-application/form.behavior.ts
56
- import { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';
57
- export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(({ model }) => {
58
- compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
59
- enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
77
+ // forms/credit-application/form.behavior.ts — ONE behavior: model operators and node rules
78
+ import { defineFormBehavior, compute, enableWhen, hideWhen } from '@reformer/core/behaviors';
79
+ export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(
80
+ ({ model, schema }) => {
81
+ compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
82
+ enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
83
+ hideWhen(schema.node('mortgage'), () => model.loanType !== 'mortgage'); // executed by the renderer
84
+ }
85
+ );
86
+
87
+ // forms/credit-application/form.validation.ts — step rules (exported) and the extras
88
+ import type { FormValidation } from '@reformer/core';
89
+ import { defineValidation } from '@reformer/core/validation';
90
+ export const loanRules = defineValidation<CreditApplicationForm>(({ model }) => { /* ... */ });
91
+ export const contactsRules = defineValidation<CreditApplicationForm>(({ model }) => { /* ... */ });
92
+ export const creditApplicationValidation: FormValidation<CreditApplicationForm> = {
93
+ extras: crossStepRules, // checked only as a whole (submit)
94
+ };
95
+
96
+ // forms/credit-application/form.flow.ts — the steps of the wizard
97
+ import type { FormFlowConfig } from '@reformer/core';
98
+ import { loanRules, contactsRules } from './form.validation';
99
+ export const creditApplicationFlow: FormFlowConfig<CreditApplicationForm> = {
100
+ steps: [
101
+ { id: 'loan', rules: loanRules }, // id = selector of the step node in form.schema.ts
102
+ { id: 'contacts', rules: contactsRules },
103
+ ],
104
+ };
105
+
106
+ // forms/credit-application/form.messages.ts — the issue texts by code
107
+ import { defineIssueMessages } from '@reformer/core/i18n';
108
+ export const creditApplicationMessages = defineIssueMessages({
109
+ 'loanAmount.required': 'Укажите сумму кредита',
60
110
  });
61
111
 
62
112
  // forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline
63
113
  import { createForm } from '@reformer/core';
64
- export const createCreditApplicationForm = () => {
65
- const model = createCreditApplicationModel();
66
- return createForm({ model, schema: creditApplicationSchema(model), behavior: creditApplicationBehavior });
67
- };
114
+ // ONE call: model + form + behavior + validation + flow. In the page: useFormBundle(createCreditApplicationForm).
115
+ export const createCreditApplicationForm = () =>
116
+ createForm<CreditApplicationForm>({
117
+ model: createCreditApplicationModel(),
118
+ schema: creditApplicationSchema, // builder (model) => tree
119
+ behavior: creditApplicationBehavior,
120
+ validation: creditApplicationValidation, // { extras } — rules outside the steps
121
+ flow: creditApplicationFlow, // steps → bundle.flow, bundle.validateStep(id)
122
+ });
123
+ // The page: <ValidationMessagesProvider messages={creditApplicationMessages}> around the form,
124
+ // <FormWizard form={bundle.form} flow={bundle.flow} … onSubmit={() => bundle.submission.submit(api.send)} />.
68
125
  ```
69
126
 
70
127
  ### Scaling up: folders (large forms)
71
128
 
72
- When the flat module gets unwieldy — steps you want in their own files, sub-forms reused across
73
- steps, calc/validators that outgrow one file — promote it to a three-folder module: `lib/`
74
- (domain raw material), `schema/` (the form definition), `components/` (React layout), plus the
75
- entry component and `index.ts`. Keep the module root to just the entry + `index.ts`; everything
76
- else lives in a folder.
129
+ When the flat module gets unwieldy — a multi-step wizard, sub-forms reused across steps, rules
130
+ that outgrow one file — split it by **concern**, one folder per layer. Each layer answers one
131
+ question about the form:
132
+
133
+ | Folder | Question it answers |
134
+ | -------------- | ------------------------------------------------------- |
135
+ | `model/` | What data does the form hold, and what does it start as |
136
+ | `schema/` | How is the form laid out |
137
+ | `validation/` | What is a valid value |
138
+ | `behavior/` | How do values and field state react to each other |
139
+ | `flow/` | Which steps does the user walk through, in what order |
140
+ | `application/` | How is the form assembled, loaded and submitted |
77
141
 
78
142
  ```
79
143
  forms/
80
- └── [form-name]/ # Form module — folders (scale-up)
81
- ├── [FormName]Form.tsx # Entry: builds model+form, renders FormWizard + step components
82
- ├── index.ts # Public re-exports of the module
144
+ └── [form-name]/
145
+ ├── [FormName]Form.tsx # Entry: builds the form, renders FormWizard + step components
146
+ │
147
+ ├── model/
148
+ │ ├── model.ts # create[FormName]Model — the reactive model
149
+ │ ├── initial-value.ts # initial values; arrays via arrayOf(blank)
150
+ │ ├── predicates.ts # conditions shared by behavior, validation and JSX
151
+ │ └── factories/ # blank values of sub-forms and array rows
152
+ │ ├── address.ts
153
+ │ └── property.ts
154
+ │
155
+ ├── schema/
156
+ │ ├── form.ts # the tree: loading → wizard → steps taken from the flow
157
+ │ ├── steps/ # content of each step (an array of nodes)
158
+ │ │ ├── loan.ts
159
+ │ │ └── contacts.ts
160
+ │ └── sections/ # sections, sub-forms (part) and array rows (item)
161
+ │ ├── address.ts
162
+ │ └── property.ts
83
163
  │
84
- ├── lib/ # Domain helpers (target-agnostic)
85
- │ ├── types.ts # Form interface + field enums + { value, label } option type
86
- │ ├── constants.ts # Option dictionaries (LOAN_TYPES, GENDERS, ...)
87
- │ ├── calc.ts # Pure fns for derived fields (age, monthlyPayment, ...)
88
- │ ├── custom-validators.ts # Reusable validator factories
89
- │ └── api.ts # Data sources + submit
164
+ ├── validation/
165
+ │ ├── form.ts # FormValidation: { extras } — rules outside the steps
166
+ │ ├── messages.ts # defineIssueMessages: issue code → text
167
+ │ ├── common/ # shared rule sets, rules of sub-forms and array rows
168
+ │ │ ├── rules.ts
169
+ │ │ └── address.ts
170
+ │ ├── loan.ts # rules of one step — one file per step
171
+ │ ├── contacts.ts
172
+ │ └── cross-step.ts # rules of the whole form (checked on submit)
90
173
  │
91
- ├── schema/ # The form definition
92
- │ ├── model.ts # createModel factory + initial values + array-element factories
93
- │ ├── schema.ts # createForm schema tree ({ value: model.$.x, component })
94
- │ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
95
- │ ├── behavior.ts # defineFormBehavior(...)
96
- │ └── create-form.ts # Assembly: createForm({ model, schema, behavior })
174
+ ├── behavior/
175
+ │ ├── index.ts # the form behavior, composed of the parts below
176
+ │ ├── derived.ts # computed fields
177
+ │ ├── conditions.ts # enabling fields and hiding sections
178
+ │ ├── synchronization.ts # copies, clearing lists
179
+ │ ├── dynamic-options.ts # options and limits that depend on values
180
+ │ └── operators.ts # custom operators built on the built-in ones
97
181
  │
98
- └── components/
99
- ├── steps/ # One component per wizard step
100
- ├── nested-forms/ # Reusable sub-forms (Address, PersonalData, ...)
101
- └── ui/ # Form-specific helper blocks (summary, warnings, sections)
182
+ ├── flow/
183
+ │ └── [form-name]-flow.ts # the list of steps: selector, title, icon, content, rules
184
+ │ # + FormFlowConfig { steps: [{ id, rules }] } for createForm
185
+ │
186
+ ├── application/
187
+ │ ├── create.ts # ONE createForm call shared by every way of rendering
188
+ │ ├── load.ts # network: fetch the entity and dictionaries
189
+ │ ├── mapping.ts # server response → model (model.patch + captureInitial)
190
+ │ ├── payload.ts # form values → request body: drops the computed fields
191
+ │ ├── submit.ts # sending and reporting the outcome (handler of bundle.submission.submit)
192
+ │ └── renderer.ts # node rules for the renderer: loading, submit, navigation (setup)
193
+ │
194
+ ├── components/ # React layout: steps/, nested-forms/, ui/
195
+ ├── api/ # HTTP calls
196
+ └── types/ constants/ utils/ # form type, option dictionaries, pure calculations
102
197
  ```
103
198
 
104
- Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
105
- `schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
106
- may co-locate each step's `schema.ts` / `validation.ts` / `behavior.ts` inside its
107
- `steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
108
- the guide below.
199
+ Dependencies point one way: `application → flow → schema/steps, validation/<step>`;
200
+ `application → behavior → model`. Nothing below `application/` knows the entity id, the network or
201
+ the wizard — the behavior is static and reusable.
202
+
203
+ **The flow is the single place a step is declared.** One entry carries the step's `selector`,
204
+ title, icon, content and rules, so the step nodes of the schema, the `flow` of the assembly and
205
+ the wizard steps of a hand-written React page are all derived from the same list — there is
206
+ nowhere to pair the layout of one step with the rules of another:
207
+
208
+ ```typescript
209
+ // flow/credit-application-flow.ts
210
+ export const creditApplicationFlow = [
211
+ { selector: 'loan', title: 'Loan', icon: '💰', content: loanStep, rules: loanRules },
212
+ { selector: 'contacts', title: 'Contacts', icon: '📞', content: contactsStep, rules: contactsRules },
213
+ ] as const satisfies readonly FlowStep[];
214
+
215
+ // the same list → the flow of createForm; the step id is the step selector
216
+ export const creditApplicationFlowSteps: FormFlowConfig<CreditApplicationForm> = {
217
+ steps: creditApplicationFlow.map((step) => ({ id: step.selector, rules: step.rules })),
218
+ };
219
+
220
+ // schema/form.ts — step nodes come from the flow
221
+ children: creditApplicationFlow.map((step) => ({
222
+ selector: step.selector,
223
+ component: Step,
224
+ componentProps: { title: step.title, icon: step.icon },
225
+ children: step.content(model),
226
+ })),
227
+
228
+ // validation/form.ts — only the rules outside the steps
229
+ export const creditApplicationValidation: FormValidation<CreditApplicationForm> = {
230
+ extras: crossStepRules,
231
+ };
232
+
233
+ // application/create.ts
234
+ createForm<CreditApplicationForm>({
235
+ model: createCreditApplicationModel(),
236
+ schema: creditApplicationSchema,
237
+ behavior: creditApplicationBehavior,
238
+ validation: creditApplicationValidation,
239
+ flow: creditApplicationFlowSteps,
240
+ });
241
+ ```
242
+
243
+ **Texts of the issues live in `validation/messages.ts`.** Rules carry codes
244
+ (`required({ code: 'loanAmount.required' })`), the dictionary maps a code to its text, and every
245
+ way of rendering wraps the form in `<ValidationMessagesProvider messages={…}>` from
246
+ `@reformer/cdk` — the texts are the same in hand-written React, renderer-react and renderer-json.
247
+
248
+ **Conditions live in `model/predicates.ts`** as pure functions over values
249
+ (`isMortgage(loanType)`). Behavior (`enableWhen`, `hideWhen`), validation (`validateWhen`) and the
250
+ JSX of a step call the same function — a condition is written once.
251
+
252
+ **Loading and submitting are not behavior.** They belong to `application/`: the page (or, for the
253
+ renderer, `application/renderer.ts` passed as `setup`) calls `load`, hands the response to
254
+ `mapping`, and sends through `bundle.submission.submit(handler)` — one run of all the rules, the
255
+ handler is called only when the form is valid. Written into the model with `model.patch` followed
256
+ by `model.captureInitial()`, the loaded values become the form's starting point: the form is not
257
+ dirty, and `model.reset()` returns to them. The handler receives `model.get()` — the whole model,
258
+ disabled fields included; `application/payload.ts` drops the fields the server does not expect
259
+ (computed ones) before the request.
260
+
261
+ Rule of thumb for this layout: **one folder per question; a step is declared once, in the flow;
262
+ the root holds only the entry component.**
109
263
 
110
264
  ### Scaling
111
265
 
112
266
  | Complexity | Structure |
113
267
  | ---------- | --------------------------------------------------------------------------------------- |
114
268
  | Simple | Single file: `index.tsx` (model + schema + behavior + component) |
115
- | **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |
116
- | Folders (`lib/` + `schema/` + `components/`) | Large forms: split concerns into folders, one component per step, reusable `nested-forms/` |
269
+ | **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `form.validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |
270
+ | Folders (`model/` + `schema/` + `validation/` + `behavior/` + `flow/` + `application/`) | Large forms: one folder per layer, a step declared once in the flow, reusable sections and rules |
117
271
 
118
- > The leading layout is configurable: set `REFORMER_FORM_LAYOUT` (`minimalist` | `folders`)
119
- > when registering the MCP server to choose which structure the generators lead with. Default
120
- > is `minimalist`.
272
+ > The leading layout of the MCP generators is configurable: set `REFORMER_FORM_LAYOUT`
273
+ > (`minimalist` | `folders`) when registering the MCP server. Default is `minimalist`.
121
274
 
122
- > Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
123
- > renderer-json `renderer.schema.json` + `renderer.behavior.ts` + `registry.ts`), the
124
- > centralized-vs-co-located choice, and the full reuse map live in the
125
- > **form-directory-layout** guide (`@reformer/mcp`).
275
+ > The MCP layout check (`validate_form` with `kind: "layout"`) verifies the flat layout file names
276
+ > (and the optional `steps/<slug>/` folders); the folders layout is recognized by its top-level
277
+ > folders only. Cross-target variants (renderer-react: the same files; renderer-json: plus
278
+ > `registry.ts`, and `form.schema.ts` holds the JSON-DSL document) are described in the
279
+ > **form-directory-layout** guide (`find_recipe directory-layout`).
@@ -2,12 +2,13 @@
2
2
 
3
3
  > **Default rule (read first)**: для UI используй `FormField` из
4
4
  > [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
5
- > строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если
5
+ > строкой `<FormField control={form.x} />`. Свои обёртки поля пиши ТОЛЬКО если
6
6
  > ui-kit не подходит (другая design system, особый low-level input).
7
7
  >
8
8
  > Канонический schema-driven подход:
9
9
  >
10
- > - **компонент** объявляется в схеме как `component: Input` (или `Select`, `Checkbox`, etc.)
10
+ > - **компонент** объявляется в схеме как `component: Input` (или `SelectAsync`, `CheckboxWithLabel`, …) —
11
+ > сам компонент кита, отдельных «field-версий» нет: связывает его с полем обёртка (`FormField`)
11
12
  > - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
12
13
  > - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
13
14
  >
@@ -17,44 +18,48 @@
17
18
  ### Default — FormField из ui-kit (canonical)
18
19
 
19
20
  ```tsx
20
- import { useMemo } from 'react';
21
- import { createModel, createForm } from '@reformer/core';
22
- import { FormField, Input, Select, Checkbox, Button } from '@reformer/ui-kit';
21
+ import { createForm, type FormModel, type FormSchemaNode } from '@reformer/core';
22
+ import { useFormBundle } from '@reformer/core/react';
23
+ import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
23
24
 
24
- type RegistrationForm = {
25
+ interface RegistrationForm {
25
26
  email: string;
26
27
  country: string;
27
28
  agree: boolean;
28
- };
29
+ }
29
30
 
30
31
  function RegistrationPage() {
31
- const form = useMemo(() => {
32
- const model = createModel<RegistrationForm>({ email: '', country: 'ru', agree: false });
33
- const schema = {
34
- email: {
35
- value: model.$.email,
36
- component: Input,
37
- componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
38
- },
39
- country: {
40
- value: model.$.country,
41
- component: Select,
42
- componentProps: {
43
- label: 'Country',
44
- options: [
45
- { value: 'ru', label: 'Россия' },
46
- { value: 'by', label: 'Беларусь' },
47
- ],
48
- },
49
- },
50
- agree: {
51
- value: model.$.agree,
52
- component: Checkbox,
53
- componentProps: { label: 'I agree to terms' },
54
- },
55
- };
56
- return createForm<RegistrationForm>({ model, schema });
57
- }, []);
32
+ // useFormBundle вызывает фабрику ровно один раз и держит сборку стабильной между рендерами
33
+ const { form } = useFormBundle(() =>
34
+ createForm<RegistrationForm>({
35
+ initial: { email: '', country: 'ru', agree: false },
36
+ schema: (model: FormModel<RegistrationForm>): FormSchemaNode => ({
37
+ children: [
38
+ {
39
+ model: model.$.email,
40
+ component: Input,
41
+ componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
42
+ },
43
+ {
44
+ model: model.$.country,
45
+ component: SelectAsync,
46
+ componentProps: {
47
+ label: 'Country',
48
+ options: [
49
+ { value: 'ru', label: 'Россия' },
50
+ { value: 'by', label: 'Беларусь' },
51
+ ],
52
+ },
53
+ },
54
+ {
55
+ model: model.$.agree,
56
+ component: CheckboxWithLabel,
57
+ componentProps: { label: 'I agree to terms' },
58
+ },
59
+ ],
60
+ }),
61
+ })
62
+ );
58
63
 
59
64
  return (
60
65
  <form>
@@ -69,7 +74,8 @@ function RegistrationPage() {
69
74
 
70
75
  `FormField` сам читает `componentProps.label`, `componentProps.placeholder`,
71
76
  `componentProps.options` через `useFormControl(...).componentProps` и применяет
72
- их к нужному `<input>`/`<select>`/etc. Error rendering, `pending` для async-валидаций,
77
+ их к нужному `<input>`/`<select>`/etc. Текст замечания (по коду — словарь формы из
78
+ `ValidationMessagesProvider messages`, затем локаль), `pending` для async-валидаций,
73
79
  `data-testid` для e2e — всё из коробки.
74
80
 
75
81
  ### Anti-patterns (не делай так)
@@ -92,7 +98,7 @@ function RegistrationPage() {
92
98
  ✅ Всё это в схеме:
93
99
 
94
100
  ```ts
95
- { email: { component: Input, componentProps: { label: 'Email' } } }
101
+ { model: model.$.email, component: Input, componentProps: { label: 'Email' } }
96
102
  ```
97
103
 
98
104
  ```tsx
@@ -113,7 +119,8 @@ import { InputMask } from 'react-input-mask';
113
119
  ```
114
120
 
115
121
  `children` оборачивается в `CdkFormField.Control asChild` и получает все нужные
116
- props (`value`, `onChange`, `onBlur`, `aria-invalid`).
122
+ props (`value`, `onChange`, `onBlur`, `aria-invalid`) — в диалекте ребёнка, если тот объявил
123
+ статику `reformerAdapter` (`defineFieldControl` из `@reformer/ui-kit/fields`), иначе value-based.
117
124
 
118
125
  ### Advanced — write your own from scratch (rare)
119
126
 
@@ -122,13 +129,15 @@ props (`value`, `onChange`, `onBlur`, `aria-invalid`).
122
129
  из `componentProps`, не из JSX-props.
123
130
 
124
131
  ```tsx
125
- import type { FieldNode } from '@reformer/core';
126
- import { useFormControl } from '@reformer/core';
132
+ import type { FormControl } from '@reformer/core';
133
+ import { useFormControl, useIssueMessage } from '@reformer/core/react';
134
+ import { formMessages } from './form.validation'; // defineIssueMessages — константа модуля
127
135
 
128
- type MyFormFieldProps<T> = { control: FieldNode<T> }; // ← ОДИН prop
136
+ type MyFormFieldProps<T> = { control: FormControl<T> }; // ← ОДИН prop
129
137
 
130
138
  function MyFormField<T>({ control }: MyFormFieldProps<T>) {
131
- const { value, errors, disabled, shouldShowError, componentProps } = useFormControl(control);
139
+ const { value, issues, disabled, shouldShowIssues, componentProps } = useFormControl(control);
140
+ const message = useIssueMessage(formMessages); // текст замечания по коду
132
141
  // componentProps = { label, placeholder, type, options, ... } — из СХЕМЫ
133
142
  const cp = (componentProps ?? {}) as Record<string, unknown>;
134
143
 
@@ -143,24 +152,54 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
143
152
  onChange={(e) => (control.setValue as (v: unknown) => void)(e.target.value)}
144
153
  onBlur={() => control.markAsTouched()}
145
154
  />
146
- {shouldShowError && errors[0] && <span>{errors[0].message}</span>}
155
+ {/* shouldShowIssues — ошибки и предупреждения; shouldShowError — для aria-invalid */}
156
+ {shouldShowIssues && issues[0] && <span>{message(issues[0])}</span>}
147
157
  </label>
148
158
  );
149
159
  }
150
160
  ```
151
161
 
162
+ Внутри дерева с `ValidationMessagesProvider` (cdk) вместо `useIssueMessage(formMessages)` берут
163
+ `useIssueMessageResolver()` из `@reformer/cdk` — он учитывает словари всех провайдеров выше. См.
164
+ `36-issue-messages.md`.
165
+
152
166
  Использование — как у `FormField`:
153
167
 
154
168
  ```tsx
155
169
  <MyFormField control={form.email} /> // ← без label-prop
156
170
  ```
157
171
 
172
+ ### Связывание поля с контролом — `FieldAdapter` (`@reformer/core/react`)
173
+
174
+ Контролы говорят на разных диалектах (`checked` + `onCheckedChange`, `onValueChange`, DOM-событие
175
+ в `onChange`), форма — на value-based seam (`value` + `onChange(value)` + `onBlur`). Перевод
176
+ описывает `FieldAdapter` (`valueProp`, `changeProp`, `fromEmit`, `toValue`, `bindBlur`, `strip`,
177
+ `passControl` — все необязательны). Компонент объявляет его статикой `reformerAdapter`, а обёртка
178
+ поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`) читает статику
179
+ и связывает поле сама — отдельные «field-версии» компонентов не нужны.
180
+
181
+ | Символ | Назначение |
182
+ | ----------------------------- | --------------------------------------------------------------------------- |
183
+ | `FieldAdapter` | Тип адаптера: диалект контрола |
184
+ | `getFieldAdapter(component)` | Статика `component.reformerAdapter` либо `undefined` |
185
+ | `bindFieldProps(adapter, seam, props)` | Props контрола под адаптер (без адаптера — seam как есть) |
186
+ | `FieldHandle` | Императивный handle поля: `focus` / `blur` / `scrollIntoView` / `getElement` |
187
+ | `makeElementFieldHandle(ref)` | Базовый `FieldHandle` из DOM-узла |
188
+ | `useFieldHandleRef(outerRef)` | Ref для контрола + публикация handle потребителю (строит ОБЁРТКА поля) |
189
+
190
+ В приложении обычно достаточно `defineFieldControl(MyControl, { adapter })` из
191
+ `@reformer/ui-kit/fields` (плюс готовые пресеты `checkedAdapter`, `valueChangeAdapter`, …); для
192
+ компонентов чужой библиотеки без статики — `RendererSettings.resolveFieldAdapter`.
193
+
158
194
  ### Integration with UI libraries (shadcn etc.)
159
195
 
160
196
  > **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
161
197
  > `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
162
198
  > компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
163
- > контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
199
+ > контрола. Свой компонент можно подготовить и один раз — статикой
200
+ > `defineFieldControl(MyInput, { adapter })` из `@reformer/ui-kit/fields`: его поймут и рендерер,
201
+ > и `FormField.Control` из `@reformer/cdk`. Ручная обёртка на `useFormControl` (ниже) нужна для
202
+ > прямого JSX без `FormField` и рендерера.
164
203
 
165
204
  Если есть существующая design system — оборачивай её компоненты в один
166
205
  `MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
@@ -170,8 +209,9 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
170
209
  import { Input } from '@/components/ui/input';
171
210
  import { Label } from '@/components/ui/label';
172
211
 
173
- function ShadcnFormField({ control }: { control: FieldNode<string> }) {
174
- const { value, errors, disabled, componentProps } = useFormControl(control);
212
+ function ShadcnFormField({ control }: { control: FormControl<string> }) {
213
+ const { value, issues, disabled, shouldShowIssues, componentProps } = useFormControl(control);
214
+ const message = useIssueMessage(formMessages);
175
215
  const cp = (componentProps ?? {}) as Record<string, unknown>;
176
216
 
177
217
  return (
@@ -182,7 +222,7 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
182
222
  onChange={(e) => control.setValue(e.target.value)}
183
223
  disabled={disabled}
184
224
  />
185
- {errors[0] && <p className="text-red-500">{errors[0].message}</p>}
225
+ {shouldShowIssues && issues[0] && <p className="text-red-500">{message(issues[0])}</p>}
186
226
  </div>
187
227
  );
188
228
  }