@reformer/core 10.2.0 → 11.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +90 -68
  2. package/dist/behaviors-value-D05z3my6.js +109 -0
  3. package/dist/behaviors.d.ts +1 -1
  4. package/dist/behaviors.js +376 -211
  5. package/dist/create-model-DPSG3I0O.js +30 -0
  6. package/dist/date-utils-90-qZbse.js +43 -0
  7. package/dist/en-HP8XEych.js +44 -0
  8. package/dist/form/aggregate-signals.d.ts +21 -63
  9. package/dist/form/behaviors/collections.d.ts +84 -0
  10. package/dist/form/behaviors/context.d.ts +56 -0
  11. package/dist/form/behaviors/index.d.ts +30 -0
  12. package/dist/form/behaviors/internals.d.ts +41 -0
  13. package/dist/form/{behaviors-node.d.ts → behaviors/node.d.ts} +28 -5
  14. package/dist/form/behaviors/operators.d.ts +59 -0
  15. package/dist/form/behaviors/schema-operators.d.ts +83 -0
  16. package/dist/form/behaviors/types.d.ts +37 -0
  17. package/dist/form/create-form.d.ts +59 -70
  18. package/dist/form/flow/flow.d.ts +87 -0
  19. package/dist/form/form-bundle.d.ts +153 -0
  20. package/dist/form/form-level.d.ts +121 -0
  21. package/dist/form/form-proxy-builder.d.ts +6 -22
  22. package/dist/form/index.d.ts +23 -27
  23. package/dist/form/nodes/form-array-node.d.ts +100 -0
  24. package/dist/form/nodes/form-control-node.d.ts +73 -0
  25. package/dist/form/nodes/form-group-node.d.ts +66 -0
  26. package/dist/form/nodes/node-kind.d.ts +29 -0
  27. package/dist/form/nodes/state/availability.d.ts +6 -0
  28. package/dist/form/nodes/state/component.d.ts +6 -0
  29. package/dist/form/nodes/state/index.d.ts +12 -0
  30. package/dist/form/nodes/state/interaction.d.ts +13 -0
  31. package/dist/form/nodes/state/issues.d.ts +9 -0
  32. package/dist/form/nodes/state/pending.d.ts +6 -0
  33. package/dist/form/schema/compile.d.ts +59 -0
  34. package/dist/form/schema/compiled.d.ts +143 -0
  35. package/dist/form/schema/row-key.d.ts +11 -0
  36. package/dist/form/schema-controller.d.ts +159 -0
  37. package/dist/form/schema-subtree.d.ts +28 -0
  38. package/dist/form/signal-node-registry.d.ts +27 -22
  39. package/dist/form/submission.d.ts +93 -0
  40. package/dist/form/type-guards.d.ts +14 -73
  41. package/dist/form/types/contracts.d.ts +21 -91
  42. package/dist/form/types/field-config.d.ts +27 -0
  43. package/dist/form/types/index.d.ts +5 -91
  44. package/dist/form/types/nodes.d.ts +159 -0
  45. package/dist/form/types/schema-node.d.ts +148 -50
  46. package/dist/form/types/validation-schema.d.ts +24 -41
  47. package/dist/form/validation/context.d.ts +67 -0
  48. package/dist/form/validation/index.d.ts +24 -28
  49. package/dist/form/validation/operators.d.ts +101 -0
  50. package/dist/form/validation/run.d.ts +120 -0
  51. package/dist/form/validation/strategy.d.ts +89 -0
  52. package/dist/form/validation/types.d.ts +83 -0
  53. package/dist/form/validators/date-utils.d.ts +42 -0
  54. package/dist/form/validators/email.d.ts +23 -0
  55. package/dist/form/{validation/validators → validators}/file-type.d.ts +10 -10
  56. package/dist/form/{validation/validators → validators}/file-utils.d.ts +3 -2
  57. package/dist/form/validators/future-date.d.ts +23 -0
  58. package/dist/form/{validation/validators → validators}/index.d.ts +7 -4
  59. package/dist/form/validators/integer.d.ts +22 -0
  60. package/dist/form/validators/is-date.d.ts +24 -0
  61. package/dist/form/validators/is-number.d.ts +25 -0
  62. package/dist/form/validators/max-age.d.ts +28 -0
  63. package/dist/form/validators/max-date.d.ts +25 -0
  64. package/dist/form/{validation/validators → validators}/max-file-size.d.ts +10 -10
  65. package/dist/form/validators/max-files.d.ts +26 -0
  66. package/dist/form/validators/max-length.d.ts +33 -0
  67. package/dist/form/validators/max-total-file-size.d.ts +26 -0
  68. package/dist/form/validators/max.d.ts +26 -0
  69. package/dist/form/validators/min-age.d.ts +27 -0
  70. package/dist/form/validators/min-date.d.ts +25 -0
  71. package/dist/form/{validation/validators → validators}/min-file-size.d.ts +10 -10
  72. package/dist/form/validators/min-files.d.ts +27 -0
  73. package/dist/form/validators/min-length.d.ts +33 -0
  74. package/dist/form/validators/min.d.ts +26 -0
  75. package/dist/form/validators/multiple-of.d.ts +25 -0
  76. package/dist/form/validators/non-negative.d.ts +22 -0
  77. package/dist/form/validators/non-zero.d.ts +22 -0
  78. package/dist/form/validators/past-date.d.ts +23 -0
  79. package/dist/form/validators/pattern.d.ts +26 -0
  80. package/dist/form/{validation/validators → validators}/phone.d.ts +11 -11
  81. package/dist/form/validators/required.d.ts +29 -0
  82. package/dist/form/validators/url.d.ts +45 -0
  83. package/dist/form/validators/validation-issue.d.ts +8 -0
  84. package/dist/i18n/descriptor.d.ts +54 -0
  85. package/dist/i18n/en.json.d.ts +49 -0
  86. package/dist/i18n/loader.d.ts +71 -0
  87. package/dist/i18n/locale-store.d.ts +46 -0
  88. package/dist/i18n/locale.d.ts +90 -0
  89. package/dist/i18n/message-format.d.ts +136 -0
  90. package/dist/i18n/resolve-localized.d.ts +17 -0
  91. package/dist/i18n/ru.json.d.ts +49 -0
  92. package/dist/i18n/translator.d.ts +61 -0
  93. package/dist/i18n/validation-message.d.ts +53 -0
  94. package/dist/i18n.d.ts +26 -0
  95. package/dist/i18n.js +86 -0
  96. package/dist/index.d.ts +18 -6
  97. package/dist/index.js +932 -2779
  98. package/dist/loader-BWkigpVe.js +52 -0
  99. package/dist/locale/en.d.ts +2 -0
  100. package/dist/locale/en.js +5 -0
  101. package/dist/locale/en.json +45 -0
  102. package/dist/locale/index.d.ts +18 -0
  103. package/dist/locale/ru.d.ts +2 -0
  104. package/dist/locale/ru.js +44 -0
  105. package/dist/locale/ru.json +45 -0
  106. package/dist/locale-CG54AU2Q.js +219 -0
  107. package/dist/locale.d.ts +2 -0
  108. package/dist/locale.js +9 -0
  109. package/dist/{state → model}/behaviors-value.d.ts +24 -6
  110. package/dist/{state/form-model.d.ts → model/create-model.d.ts} +18 -4
  111. package/dist/{state → model}/derived-registry.d.ts +1 -1
  112. package/dist/model/index.d.ts +42 -0
  113. package/dist/model/model-nodes.d.ts +190 -0
  114. package/dist/model/model-signals-proxy.d.ts +63 -0
  115. package/dist/model/model-value-proxy.d.ts +84 -0
  116. package/dist/{state → model}/safe-effect.d.ts +50 -2
  117. package/dist/model/types.d.ts +239 -0
  118. package/dist/model.d.ts +2 -0
  119. package/dist/model.js +27 -0
  120. package/dist/node-kind-C-r6ZMdo.js +15 -0
  121. package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
  122. package/dist/platforms/react/field-binding/field-handle.d.ts +53 -0
  123. package/dist/platforms/react/form-bundle-context.d.ts +27 -0
  124. package/dist/{form → platforms/react}/hooks/types.d.ts +60 -38
  125. package/dist/platforms/react/hooks/use-form-bundle.d.ts +41 -0
  126. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  127. package/dist/{form → platforms/react}/hooks/use-form-validation.d.ts +13 -7
  128. package/dist/{form → platforms/react}/hooks/useArrayLength.d.ts +6 -6
  129. package/dist/{form → platforms/react}/hooks/useFormControl.d.ts +7 -9
  130. package/dist/{form → platforms/react}/hooks/useFormControlValue.d.ts +18 -18
  131. package/dist/{form → platforms/react}/hooks/useSignalSubscription.d.ts +9 -1
  132. package/dist/platforms/react/i18n/context.d.ts +18 -0
  133. package/dist/platforms/react/i18n/hooks.d.ts +65 -0
  134. package/dist/platforms/react/i18n/provider.d.ts +59 -0
  135. package/dist/platforms/react/index.d.ts +30 -0
  136. package/dist/react.d.ts +22 -0
  137. package/dist/react.js +505 -0
  138. package/dist/runtime-token.d.ts +22 -0
  139. package/dist/safe-effect-BazLOv1g.js +669 -0
  140. package/dist/schema-controller-CpiO2JoK.js +143 -0
  141. package/dist/signals.d.ts +15 -9
  142. package/dist/signals.js +11 -6
  143. package/dist/strategy-DzvnsWLh.js +375 -0
  144. package/dist/type-guards-D-N_5Ddh.js +19 -0
  145. package/dist/validation-issue-B9mjI8jz.js +7 -0
  146. package/dist/validation-message-Ced9MGxs.js +194 -0
  147. package/dist/validation.d.ts +1 -1
  148. package/dist/validation.js +11 -165
  149. package/dist/validators/email.d.ts +1 -1
  150. package/dist/validators/email.js +5 -8
  151. package/dist/validators/future-date.d.ts +1 -1
  152. package/dist/validators/future-date.js +10 -13
  153. package/dist/validators/integer.d.ts +1 -1
  154. package/dist/validators/integer.js +4 -7
  155. package/dist/validators/is-date.d.ts +1 -1
  156. package/dist/validators/is-date.js +5 -8
  157. package/dist/validators/is-number.d.ts +1 -1
  158. package/dist/validators/is-number.js +4 -7
  159. package/dist/validators/max-age.d.ts +1 -1
  160. package/dist/validators/max-age.js +8 -12
  161. package/dist/validators/max-date.d.ts +1 -1
  162. package/dist/validators/max-date.js +10 -13
  163. package/dist/validators/max-length.d.ts +1 -1
  164. package/dist/validators/max-length.js +7 -10
  165. package/dist/validators/max.d.ts +1 -1
  166. package/dist/validators/max.js +4 -7
  167. package/dist/validators/min-age.d.ts +1 -1
  168. package/dist/validators/min-age.js +10 -11
  169. package/dist/validators/min-date.d.ts +1 -1
  170. package/dist/validators/min-date.js +8 -11
  171. package/dist/validators/min-length.d.ts +1 -1
  172. package/dist/validators/min-length.js +4 -7
  173. package/dist/validators/min.d.ts +1 -1
  174. package/dist/validators/min.js +4 -7
  175. package/dist/validators/multiple-of.d.ts +1 -1
  176. package/dist/validators/multiple-of.js +11 -10
  177. package/dist/validators/non-negative.d.ts +1 -1
  178. package/dist/validators/non-negative.js +2 -5
  179. package/dist/validators/non-zero.d.ts +1 -1
  180. package/dist/validators/non-zero.js +4 -7
  181. package/dist/validators/past-date.d.ts +1 -1
  182. package/dist/validators/past-date.js +10 -13
  183. package/dist/validators/pattern.d.ts +1 -1
  184. package/dist/validators/pattern.js +5 -8
  185. package/dist/validators/phone.d.ts +1 -1
  186. package/dist/validators/phone.js +10 -11
  187. package/dist/validators/required.d.ts +1 -1
  188. package/dist/validators/required.js +4 -7
  189. package/dist/validators/url.d.ts +1 -1
  190. package/dist/validators/url.js +48 -17
  191. package/dist/validators.d.ts +1 -5
  192. package/dist/validators.js +98 -154
  193. package/docs/llms/01-api-reference.md +89 -0
  194. package/docs/llms/02-quick-start.md +181 -0
  195. package/docs/llms/03-api-signatures.md +494 -0
  196. package/docs/llms/04-common-patterns.md +204 -0
  197. package/docs/llms/05-common-mistakes.md +192 -0
  198. package/docs/llms/06-troubleshooting.md +16 -0
  199. package/docs/llms/07-complete-import.md +130 -0
  200. package/docs/llms/08-form-types.md +54 -0
  201. package/docs/llms/09-formschema.md +191 -0
  202. package/docs/llms/10-arrays.md +241 -0
  203. package/docs/llms/11-async-watchfield.md +53 -0
  204. package/docs/llms/12-array-cleanup.md +31 -0
  205. package/docs/llms/13-multi-step.md +162 -0
  206. package/docs/llms/14-extended-mistakes.md +101 -0
  207. package/docs/llms/15-project-structure.md +279 -0
  208. package/docs/llms/16-ui-components.md +229 -0
  209. package/docs/llms/17-nonexistent-api.md +278 -0
  210. package/docs/llms/18-conditional-fields.md +222 -0
  211. package/docs/llms/19-reading-values.md +100 -0
  212. package/docs/llms/20-compute-vs-watch.md +153 -0
  213. package/docs/llms/21-array-operations.md +163 -0
  214. package/docs/llms/22-cycle-detection.md +86 -0
  215. package/docs/llms/23-copy-from.md +141 -0
  216. package/docs/llms/24-sync-fields.md +115 -0
  217. package/docs/llms/25-reset-when.md +122 -0
  218. package/docs/llms/26-transform-value.md +136 -0
  219. package/docs/llms/27-revalidate-when.md +133 -0
  220. package/docs/llms/28-submit-and-reset.md +409 -0
  221. package/docs/llms/29-async-preload.md +203 -0
  222. package/docs/llms/30-type-safety-recipes.md +213 -0
  223. package/docs/llms/31-async-validator-debounce.md +178 -0
  224. package/docs/llms/32-async-options-loading.md +108 -0
  225. package/docs/llms/33-validation-strategy.md +305 -0
  226. package/docs/llms/34-i18n.md +298 -0
  227. package/docs/llms/35-unified-contract.md +309 -0
  228. package/docs/llms/36-issue-messages.md +284 -0
  229. package/docs/llms/37-compiled-schema.md +332 -0
  230. package/docs/llms/38-form-flow.md +396 -0
  231. package/llms-index.json +1 -0
  232. package/llms.txt +10014 -3826
  233. package/package.json +77 -15
  234. package/dist/behaviors-node-DGEFMTQu.js +0 -18
  235. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  236. package/dist/date-utils-dTT_x_Be.js +0 -31
  237. package/dist/derived-registry-CluZCQ7U.js +0 -16
  238. package/dist/form/behaviors.d.ts +0 -133
  239. package/dist/form/error-handler.d.ts +0 -166
  240. package/dist/form/factories/index.d.ts +0 -6
  241. package/dist/form/factories/node-factory.d.ts +0 -188
  242. package/dist/form/form-submitter.d.ts +0 -125
  243. package/dist/form/nodes/array-node.d.ts +0 -360
  244. package/dist/form/nodes/field-node.d.ts +0 -294
  245. package/dist/form/nodes/form-node.d.ts +0 -340
  246. package/dist/form/nodes/group-node.d.ts +0 -288
  247. package/dist/form/nodes/model-array-node.d.ts +0 -121
  248. package/dist/form/status-machine.d.ts +0 -153
  249. package/dist/form/types/deep-schema.d.ts +0 -116
  250. package/dist/form/types/form-proxy.d.ts +0 -141
  251. package/dist/form/unique-id.d.ts +0 -53
  252. package/dist/form/validation/validators/date-utils.d.ts +0 -30
  253. package/dist/form/validation/validators/email.d.ts +0 -23
  254. package/dist/form/validation/validators/future-date.d.ts +0 -23
  255. package/dist/form/validation/validators/integer.d.ts +0 -22
  256. package/dist/form/validation/validators/is-date.d.ts +0 -23
  257. package/dist/form/validation/validators/is-number.d.ts +0 -24
  258. package/dist/form/validation/validators/max-age.d.ts +0 -25
  259. package/dist/form/validation/validators/max-date.d.ts +0 -25
  260. package/dist/form/validation/validators/max-files.d.ts +0 -26
  261. package/dist/form/validation/validators/max-length.d.ts +0 -27
  262. package/dist/form/validation/validators/max-total-file-size.d.ts +0 -26
  263. package/dist/form/validation/validators/max.d.ts +0 -25
  264. package/dist/form/validation/validators/min-age.d.ts +0 -25
  265. package/dist/form/validation/validators/min-date.d.ts +0 -25
  266. package/dist/form/validation/validators/min-files.d.ts +0 -26
  267. package/dist/form/validation/validators/min-length.d.ts +0 -27
  268. package/dist/form/validation/validators/min.d.ts +0 -25
  269. package/dist/form/validation/validators/multiple-of.d.ts +0 -24
  270. package/dist/form/validation/validators/non-negative.d.ts +0 -22
  271. package/dist/form/validation/validators/non-zero.d.ts +0 -22
  272. package/dist/form/validation/validators/past-date.d.ts +0 -23
  273. package/dist/form/validation/validators/pattern.d.ts +0 -33
  274. package/dist/form/validation/validators/required.d.ts +0 -32
  275. package/dist/form/validation/validators/url.d.ts +0 -43
  276. package/dist/form/validation-schema.d.ts +0 -106
  277. package/dist/form/validation-strategy.d.ts +0 -55
  278. package/dist/form/validators.d.ts +0 -6
  279. package/dist/form-model-DYCzGGKF.js +0 -286
  280. package/dist/state/index.d.ts +0 -31
  281. package/dist/state/subscription-manager.d.ts +0 -180
  282. package/dist/state/types.d.ts +0 -144
  283. package/dist/state.d.ts +0 -2
  284. package/dist/state.js +0 -21
  285. package/dist/subscription-manager-705bls97.js +0 -171
package/README.md CHANGED
@@ -4,7 +4,8 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@reformer/core.svg)](https://www.npmjs.com/package/@reformer/core)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- Reactive form state management library for React with signals-based architecture.
7
+ Reactive form state management on signals: a UI-agnostic core with React bindings in
8
+ `@reformer/core/react`.
8
9
 
9
10
  ## Playground
10
11
 
@@ -17,10 +18,12 @@ Full documentation is available at [https://alexandrbukhtatyy.github.io/ReFormer
17
18
  ## Features
18
19
 
19
20
  - Signals-based reactive state management
20
- - Declarative form validation
21
+ - Declarative form validation with typed rules and status results
22
+ - Validation issues without text: codes and params, texts from a form dictionary and the locale
21
23
  - Dynamic form behaviors
24
+ - Multi-step flows: step rules, transitions, `submit` with a result object
22
25
  - TypeScript support
23
- - Tree-shakeable exports
26
+ - Tree-shakeable exports; the root entry has no React dependency
24
27
 
25
28
  ## Installation
26
29
 
@@ -30,27 +33,45 @@ npm install @reformer/core@beta # Active development is underway, so you can try
30
33
 
31
34
  ## Quick Start
32
35
 
33
- ReFormer is built around the **M1 architecture**: a reactive `FormModel` owns the values, a layout
34
- schema binds field config (component / props) to the model's signals, and `createForm({ model, schema })`
35
- wires them into a typed form. Validation is a **separate** ambient contract — `defineValidationSchema`,
36
- run on demand by `validateModel(model, schema)` — so the layout schema carries no validators.
36
+ A reactive `FormModel` owns the values, a form schema — a tree of nodes — binds field config
37
+ (component / props) to the model's handles (`model.$.<field>`), and `createForm` assembles the
38
+ model, the form and the form level (`validate`, `submission`, step flow) in one call. Validation is a
39
+ **separate** contract — `defineValidation` — so the form schema carries no validators. A rule
40
+ reports a code, not a text: the text comes from a form dictionary (`defineIssueMessages`) on top of
41
+ the locale.
37
42
 
38
43
  ```tsx
39
- import { useMemo } from 'react';
40
44
  import {
41
- createModel,
42
45
  createForm,
43
- useFormControl,
44
- type FieldNode,
45
- type ValidationError,
46
+ type FormControl,
47
+ type FormModel,
48
+ type FormSchemaNode,
49
+ type ValidationIssue,
46
50
  } from '@reformer/core';
47
- import { validate, cross, defineValidationSchema, validateModel } from '@reformer/core/validation';
51
+ import { useFormBundle, useFormControl, useIssueMessage } from '@reformer/core/react';
52
+ import { defineIssueMessages } from '@reformer/core/i18n';
53
+ import { validate, defineValidation } from '@reformer/core/validation';
48
54
  import { required, email, minLength } from '@reformer/core/validators';
49
55
  import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
50
56
 
51
- // 0. Simple FormField component
52
- function FormField({ label, control }: { label: string; control: FieldNode<string> }) {
53
- const { value, errors, shouldShowError } = useFormControl(control);
57
+ // 1. Form data type — `interface` or `type`, both work
58
+ interface RegistrationForm {
59
+ username: string;
60
+ email: string;
61
+ password: string;
62
+ confirmPassword: string;
63
+ }
64
+
65
+ // 2. Issue texts by code — the form dictionary (built-in codes fall back to the locale)
66
+ const messages = defineIssueMessages({
67
+ 'username.required': 'Enter a username',
68
+ 'confirmPassword.mismatch': 'Passwords do not match',
69
+ });
70
+
71
+ // 3. Simple FormField component: the issue text comes from the resolver
72
+ function FormField({ label, control }: { label: string; control: FormControl<string> }) {
73
+ const { value, issues, shouldShowError } = useFormControl(control);
74
+ const message = useIssueMessage(messages);
54
75
 
55
76
  return (
56
77
  <div>
@@ -60,78 +81,69 @@ function FormField({ label, control }: { label: string; control: FieldNode<strin
60
81
  onChange={(e) => control.setValue(e.target.value)}
61
82
  onBlur={() => control.markAsTouched()}
62
83
  />
63
- {shouldShowError && <span className="error">{errors[0].message}</span>}
84
+ {shouldShowError && issues[0] && <span className="error">{message(issues[0])}</span>}
64
85
  </div>
65
86
  );
66
87
  }
67
88
 
68
- // 1. Define form interface
69
- interface RegistrationForm {
70
- username: string;
71
- email: string;
72
- password: string;
73
- confirmPassword: string;
74
- }
75
-
76
- // 2. Reactive model — the source of truth for values
77
- const model = createModel<RegistrationForm>({
78
- username: '',
79
- email: '',
80
- password: '',
81
- confirmPassword: '',
82
- });
83
-
84
- // 3. Layout schema — binds field config (component / props) to model signals
85
- // (`model.$.<field>`). Layout carries NO validators; rules live in the
86
- // validation schema below.
87
- const schema = {
89
+ // 4. Form schema — a tree of nodes. A field node binds field config (component / props)
90
+ // to a model handle (`model: model.$.<field>`); nested nodes live in `children`.
91
+ // The schema carries NO validators; rules live in the validation schema below.
92
+ const schema = (model: FormModel<RegistrationForm>): FormSchemaNode => ({
88
93
  children: [
89
- { value: model.$.username, component: Input },
90
- { value: model.$.email, component: Input },
91
- { value: model.$.password, component: Input },
92
- { value: model.$.confirmPassword, component: Input },
94
+ { model: model.$.username, component: 'input' },
95
+ { model: model.$.email, component: 'input' },
96
+ { model: model.$.password, component: 'input' },
97
+ { model: model.$.confirmPassword, component: 'input' },
93
98
  ],
94
- };
95
-
96
- // 4. Validation schema — a separate ambient contract from `@reformer/core/validation`,
97
- // run on demand (not reactive). Field rules go through `validate(sig, [rules])`;
98
- // cross-field rules through `cross(sig, snapshot => error | null)` where the snapshot
99
- // is `model.get()`. Rule factories (`required()`/…) come from `@reformer/core/validators`.
100
- const passwordsMatch = (f: RegistrationForm): ValidationError | null =>
101
- f.confirmPassword && f.password && f.confirmPassword !== f.password
102
- ? { code: 'mismatch', message: 'Passwords do not match' }
99
+ });
100
+
101
+ // 5. Validation schema — a separate contract from `@reformer/core/validation`.
102
+ // Field rules go through `validate(sig, [rules])`; cross-field rules through
103
+ // `cross(sig, snapshot => issue | null)` taken from the schema argument — the snapshot is the
104
+ // model value, its type is inferred. Rule factories come from `@reformer/core/validators`.
105
+ const passwordsMatch = (form: RegistrationForm): ValidationIssue | null =>
106
+ form.confirmPassword && form.password && form.confirmPassword !== form.password
107
+ ? { code: 'confirmPassword.mismatch' }
103
108
  : null;
104
109
 
105
- const validationSchema = defineValidationSchema<RegistrationForm>(({ model }) => {
106
- validate(model.$.username, [required(), minLength(2)]);
110
+ const validation = defineValidation<RegistrationForm>(({ model, cross }) => {
111
+ validate(model.$.username, [required({ code: 'username.required' }), minLength(2)]);
107
112
  validate(model.$.email, [required(), email()]);
108
113
  validate(model.$.password, [required(), minLength(8)]);
109
114
  validate(model.$.confirmPassword, [required()]);
110
115
  cross(model.$.confirmPassword, passwordsMatch);
111
116
  });
112
117
 
113
- // 5. Behavior — declarative reactive logic, run by `createForm({ behavior })`.
114
- // Validation runs on demand, so the stale cross-field error on `confirmPassword`
115
- // is cleared the moment `password` changes — a behavior→validation bridge.
118
+ // 6. Behavior — declarative reactive logic, run by `createForm({ behavior })`.
119
+ // Validation runs on demand, so the stale cross-field issue on `confirmPassword`
120
+ // is cleared the moment `password` changes.
116
121
  const behavior = defineFormBehavior<RegistrationForm>(({ model, form }) => {
117
122
  onChange(model.$.password, () => {
118
- form.confirmPassword.clearErrors();
123
+ form.confirmPassword.clearIssues();
119
124
  });
120
125
  });
121
126
 
122
- // 6. Registration form component
127
+ // 7. One assembly: model, form, behavior, validation and the form level
128
+ const createRegistration = () =>
129
+ createForm<RegistrationForm>({
130
+ initial: { username: '', email: '', password: '', confirmPassword: '' },
131
+ schema,
132
+ behavior,
133
+ validation,
134
+ });
135
+
123
136
  function RegistrationFormExample() {
124
- const form = useMemo(() => createForm<RegistrationForm>({ model, schema, behavior }), []);
125
-
126
- const handleSubmit = async (e: React.FormEvent) => {
127
- e.preventDefault();
128
- form.touchAll();
129
- // Validate the whole model against the validation schema (sync + async) on demand;
130
- // errors route into the form nodes, so the UI highlights the offending fields.
131
- const valid = await validateModel(model, validationSchema);
132
- if (valid) {
133
- console.log('Form data:', model.get());
134
- }
137
+ // useFormBundle calls the factory exactly once and keeps the bundle stable between renders
138
+ const bundle = useFormBundle(createRegistration);
139
+ const { form } = bundle;
140
+
141
+ const handleSubmit = async (event: React.FormEvent) => {
142
+ event.preventDefault();
143
+ // One run of all the rules (sync + async). Issues land on the form nodes and, on a refusal,
144
+ // the whole form is marked touched; the handler is called only when the status is 'valid'.
145
+ const outcome = await bundle.submission.submit((values) => console.log('Form data:', values));
146
+ if (outcome.status === 'error') console.warn('Could not check the form, try again');
135
147
  };
136
148
 
137
149
  return (
@@ -146,6 +158,16 @@ function RegistrationFormExample() {
146
158
  }
147
159
  ```
148
160
 
161
+ Submission is a separate object of the bundle: `bundle.submission.submit(handler)` returns a
162
+ `SubmitResult`: `submitted` (with the handler's result), `invalid` / `error` / `cancelled` (with
163
+ the validation result; the handler was not called) or `failed` (the handler threw). The handler
164
+ receives the whole model, disabled fields included; `bundle.submission.submitting` is `true` while
165
+ it runs. Multi-step forms declare their steps in the same call —
166
+ `createForm({ flow: { steps: [{ id, rules }] } })` — and navigate with `bundle.flow.next()`.
167
+ Outside React, the owner of the bundle releases it with `bundle.dispose()` when the form is no
168
+ longer needed: the behavior, the node subscriptions and the live validation stop, the model keeps
169
+ its values.
170
+
149
171
  ## License
150
172
 
151
173
  MIT
@@ -0,0 +1,109 @@
1
+ import { effect as s } from "@preact/signals-core";
2
+ import { h as f } from "./safe-effect-BazLOv1g.js";
3
+ function m(t, r, l, e) {
4
+ return s(() => {
5
+ const n = t.map((c) => c.value);
6
+ if (e?.when && !e.when(...n)) return;
7
+ const u = l(...n);
8
+ r.peek() !== u && (r.value = u);
9
+ });
10
+ }
11
+ function h(t, r, l) {
12
+ return s(() => {
13
+ const e = t.value;
14
+ if (l?.when && !l.when()) return;
15
+ const n = l?.transform ? l.transform(e) : e;
16
+ r.peek() !== n && (r.value = n);
17
+ });
18
+ }
19
+ function w(t, r, l) {
20
+ let e = !0;
21
+ return s(() => {
22
+ const n = t.value;
23
+ e && (e = !1, !l?.immediate) || r(n);
24
+ });
25
+ }
26
+ function k(t, r) {
27
+ let l = !1;
28
+ const e = f(), n = s(() => {
29
+ t.value, !l && e.schedule(() => {
30
+ const u = r(t.peek());
31
+ if (t.peek() !== u) {
32
+ l = !0;
33
+ try {
34
+ t.value = u;
35
+ } finally {
36
+ l = !1;
37
+ }
38
+ }
39
+ });
40
+ });
41
+ return () => {
42
+ n(), e.cancelAll();
43
+ };
44
+ }
45
+ function y(t, r, ...[l]) {
46
+ const e = l?.resetValue ?? null, n = f(), u = s(() => {
47
+ r(), n.schedule(() => {
48
+ r() && t.peek() !== e && (t.value = e);
49
+ });
50
+ });
51
+ return () => {
52
+ u(), n.cancelAll();
53
+ };
54
+ }
55
+ function A(t, r, l) {
56
+ const e = l?.transform;
57
+ let n = !1, u = !0;
58
+ const c = f(), o = s(() => {
59
+ t.value, !n && (c.cancelAll(), c.schedule(() => {
60
+ n = !0;
61
+ try {
62
+ const a = t.peek(), i = e ? e(a) : a;
63
+ r.peek() !== i && (r.value = i);
64
+ } finally {
65
+ n = !1;
66
+ }
67
+ }));
68
+ }), v = s(() => {
69
+ if (r.value, u) {
70
+ u = !1;
71
+ return;
72
+ }
73
+ n || (c.cancelAll(), c.schedule(() => {
74
+ n = !0;
75
+ try {
76
+ const a = r.peek(), i = t.peek();
77
+ if ((e ? e(i) : i) === a) return;
78
+ i !== a && (t.value = a);
79
+ } finally {
80
+ n = !1;
81
+ }
82
+ }));
83
+ });
84
+ return () => {
85
+ o(), v(), c.cancelAll();
86
+ };
87
+ }
88
+ function x(t, r) {
89
+ let l = !0;
90
+ const e = f(), n = s(() => {
91
+ if (t.forEach((u) => u.value), l) {
92
+ l = !1;
93
+ return;
94
+ }
95
+ e.schedule(() => r());
96
+ });
97
+ return () => {
98
+ n(), e.cancelAll();
99
+ };
100
+ }
101
+ export {
102
+ h as a,
103
+ x as b,
104
+ m as c,
105
+ y as r,
106
+ A as s,
107
+ k as t,
108
+ w
109
+ };
@@ -1,2 +1,2 @@
1
- export * from './form/behaviors'
1
+ export * from './form/behaviors/index'
2
2
  export {}