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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +90 -68
  2. package/dist/behaviors-value-HA_Q03hc.js +108 -0
  3. package/dist/behaviors.js +382 -217
  4. package/dist/create-model-BvcbNPgL.js +30 -0
  5. package/dist/date-utils-90-qZbse.js +43 -0
  6. package/dist/{en-CjAqn7h0.js → en-HP8XEych.js} +6 -3
  7. package/dist/form/aggregate-signals.d.ts +21 -63
  8. package/dist/form/behaviors/collections.d.ts +42 -8
  9. package/dist/form/behaviors/context.d.ts +37 -4
  10. package/dist/form/behaviors/index.d.ts +6 -4
  11. package/dist/form/behaviors/internals.d.ts +9 -3
  12. package/dist/form/behaviors/node.d.ts +25 -2
  13. package/dist/form/behaviors/operators.d.ts +17 -9
  14. package/dist/form/behaviors/schema-operators.d.ts +78 -0
  15. package/dist/form/behaviors/types.d.ts +25 -5
  16. package/dist/form/create-form.d.ts +57 -70
  17. package/dist/form/flow/flow.d.ts +87 -0
  18. package/dist/form/form-bundle.d.ts +153 -0
  19. package/dist/form/form-config-base.d.ts +46 -0
  20. package/dist/form/form-level.d.ts +121 -0
  21. package/dist/form/form-proxy-builder.d.ts +6 -22
  22. package/dist/form/index.d.ts +20 -20
  23. package/dist/form/nodes/form-array-node.d.ts +100 -0
  24. package/dist/form/nodes/form-control-node.d.ts +73 -0
  25. package/dist/form/nodes/form-group-node.d.ts +66 -0
  26. package/dist/form/nodes/node-kind.d.ts +29 -0
  27. package/dist/form/nodes/state/availability.d.ts +6 -0
  28. package/dist/form/nodes/state/component.d.ts +6 -0
  29. package/dist/form/nodes/state/index.d.ts +12 -0
  30. package/dist/form/nodes/state/interaction.d.ts +13 -0
  31. package/dist/form/nodes/state/issues.d.ts +9 -0
  32. package/dist/form/nodes/state/pending.d.ts +6 -0
  33. package/dist/form/schema/compile.d.ts +59 -0
  34. package/dist/form/schema/compiled.d.ts +143 -0
  35. package/dist/form/schema/row-key.d.ts +11 -0
  36. package/dist/form/schema-controller.d.ts +153 -0
  37. package/dist/form/schema-subtree.d.ts +28 -0
  38. package/dist/form/signal-node-registry.d.ts +27 -22
  39. package/dist/form/submission.d.ts +93 -0
  40. package/dist/form/type-guards.d.ts +14 -73
  41. package/dist/form/types/contracts.d.ts +19 -92
  42. package/dist/form/types/field-config.d.ts +27 -0
  43. package/dist/form/types/index.d.ts +5 -57
  44. package/dist/form/types/nodes.d.ts +159 -0
  45. package/dist/form/types/schema-node.d.ts +147 -46
  46. package/dist/form/types/validation-schema.d.ts +23 -27
  47. package/dist/form/validation/context.d.ts +46 -6
  48. package/dist/form/validation/index.d.ts +9 -8
  49. package/dist/form/validation/operators.d.ts +71 -17
  50. package/dist/form/validation/run.d.ts +93 -18
  51. package/dist/form/validation/strategy.d.ts +41 -13
  52. package/dist/form/validation/types.d.ts +74 -19
  53. package/dist/form/validators/date-utils.d.ts +13 -1
  54. package/dist/form/validators/email.d.ts +8 -7
  55. package/dist/form/validators/file-type.d.ts +8 -7
  56. package/dist/form/validators/file-utils.d.ts +2 -1
  57. package/dist/form/validators/future-date.d.ts +8 -7
  58. package/dist/form/validators/index.d.ts +1 -1
  59. package/dist/form/validators/integer.d.ts +8 -7
  60. package/dist/form/validators/is-date.d.ts +11 -9
  61. package/dist/form/validators/is-number.d.ts +11 -9
  62. package/dist/form/validators/max-age.d.ts +13 -9
  63. package/dist/form/validators/max-date.d.ts +8 -7
  64. package/dist/form/validators/max-file-size.d.ts +8 -7
  65. package/dist/form/validators/max-files.d.ts +8 -7
  66. package/dist/form/validators/max-length.d.ts +14 -7
  67. package/dist/form/validators/max-total-file-size.d.ts +8 -7
  68. package/dist/form/validators/max.d.ts +10 -8
  69. package/dist/form/validators/min-age.d.ts +12 -9
  70. package/dist/form/validators/min-date.d.ts +8 -7
  71. package/dist/form/validators/min-file-size.d.ts +8 -7
  72. package/dist/form/validators/min-files.d.ts +11 -9
  73. package/dist/form/validators/min-length.d.ts +14 -7
  74. package/dist/form/validators/min.d.ts +10 -8
  75. package/dist/form/validators/multiple-of.d.ts +10 -8
  76. package/dist/form/validators/non-negative.d.ts +8 -7
  77. package/dist/form/validators/non-zero.d.ts +8 -7
  78. package/dist/form/validators/past-date.d.ts +8 -7
  79. package/dist/form/validators/pattern.d.ts +9 -8
  80. package/dist/form/validators/phone.d.ts +8 -7
  81. package/dist/form/validators/required.d.ts +11 -9
  82. package/dist/form/validators/url.d.ts +20 -13
  83. package/dist/form/validators/validation-issue.d.ts +8 -0
  84. package/dist/i18n/descriptor.d.ts +1 -1
  85. package/dist/i18n/en.json.d.ts +5 -1
  86. package/dist/i18n/locale-store.d.ts +7 -0
  87. package/dist/i18n/message-format.d.ts +24 -4
  88. package/dist/i18n/ru.json.d.ts +5 -1
  89. package/dist/i18n/translator.d.ts +11 -0
  90. package/dist/i18n/validation-message.d.ts +42 -7
  91. package/dist/i18n.d.ts +7 -12
  92. package/dist/i18n.js +66 -283
  93. package/dist/index.d.ts +16 -7
  94. package/dist/index.js +958 -2798
  95. package/dist/loader-BWkigpVe.js +52 -0
  96. package/dist/locale/en.js +1 -1
  97. package/dist/locale/en.json +4 -1
  98. package/dist/locale/ru.js +4 -1
  99. package/dist/locale/ru.json +4 -1
  100. package/dist/locale-CG54AU2Q.js +219 -0
  101. package/dist/locale.js +1 -1
  102. package/dist/model/behaviors-value.d.ts +24 -6
  103. package/dist/model/create-model.d.ts +2 -2
  104. package/dist/model/derived-registry.d.ts +1 -1
  105. package/dist/model/index.d.ts +16 -5
  106. package/dist/model/model-nodes.d.ts +138 -8
  107. package/dist/model/model-signals-proxy.d.ts +11 -5
  108. package/dist/model/model-value-proxy.d.ts +84 -3
  109. package/dist/model/safe-effect.d.ts +33 -1
  110. package/dist/model/types.d.ts +70 -17
  111. package/dist/model.js +21 -17
  112. package/dist/node-kind-C-r6ZMdo.js +15 -0
  113. package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
  114. package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
  115. package/dist/platforms/react/form-bundle-context.d.ts +27 -0
  116. package/dist/platforms/react/hooks/types.d.ts +57 -38
  117. package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
  118. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  119. package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
  120. package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
  121. package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
  122. package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
  123. package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
  124. package/dist/platforms/react/i18n/context.d.ts +6 -1
  125. package/dist/platforms/react/i18n/hooks.d.ts +16 -10
  126. package/dist/platforms/react/i18n/provider.d.ts +9 -2
  127. package/dist/platforms/react/index.d.ts +7 -4
  128. package/dist/react.d.ts +22 -0
  129. package/dist/react.js +505 -0
  130. package/dist/runtime-token.d.ts +1 -2
  131. package/dist/safe-effect-BbtX3KuN.js +643 -0
  132. package/dist/schema-controller-CpiO2JoK.js +143 -0
  133. package/dist/signals.d.ts +15 -9
  134. package/dist/signals.js +11 -6
  135. package/dist/strategy-CoFSemuP.js +375 -0
  136. package/dist/type-guards-D-N_5Ddh.js +19 -0
  137. package/dist/validation-issue-B9mjI8jz.js +7 -0
  138. package/dist/validation-message-Ced9MGxs.js +194 -0
  139. package/dist/validation.js +9 -8
  140. package/dist/validators/email.js +5 -5
  141. package/dist/validators/future-date.js +8 -8
  142. package/dist/validators/integer.js +3 -3
  143. package/dist/validators/is-date.js +4 -4
  144. package/dist/validators/is-number.js +4 -4
  145. package/dist/validators/max-age.js +8 -9
  146. package/dist/validators/max-date.js +8 -8
  147. package/dist/validators/max-length.js +7 -7
  148. package/dist/validators/max.js +4 -4
  149. package/dist/validators/min-age.js +10 -8
  150. package/dist/validators/min-date.js +3 -3
  151. package/dist/validators/min-length.js +5 -5
  152. package/dist/validators/min.js +4 -4
  153. package/dist/validators/multiple-of.js +11 -7
  154. package/dist/validators/non-negative.js +3 -3
  155. package/dist/validators/non-zero.js +3 -3
  156. package/dist/validators/past-date.js +8 -8
  157. package/dist/validators/pattern.js +5 -5
  158. package/dist/validators/phone.js +11 -9
  159. package/dist/validators/required.js +2 -2
  160. package/dist/validators/url.js +48 -12
  161. package/dist/validators.js +17 -17
  162. package/docs/llms/01-api-reference.md +60 -34
  163. package/docs/llms/02-quick-start.md +111 -80
  164. package/docs/llms/03-api-signatures.md +368 -153
  165. package/docs/llms/04-common-patterns.md +58 -56
  166. package/docs/llms/05-common-mistakes.md +69 -44
  167. package/docs/llms/06-troubleshooting.md +14 -9
  168. package/docs/llms/07-complete-import.md +74 -43
  169. package/docs/llms/08-form-types.md +11 -4
  170. package/docs/llms/09-formschema.md +131 -83
  171. package/docs/llms/10-arrays.md +119 -69
  172. package/docs/llms/11-async-watchfield.md +2 -2
  173. package/docs/llms/13-multi-step.md +134 -63
  174. package/docs/llms/14-extended-mistakes.md +20 -17
  175. package/docs/llms/15-project-structure.md +221 -74
  176. package/docs/llms/16-ui-components.md +55 -42
  177. package/docs/llms/17-nonexistent-api.md +199 -32
  178. package/docs/llms/18-conditional-fields.md +66 -32
  179. package/docs/llms/19-reading-values.md +15 -3
  180. package/docs/llms/20-compute-vs-watch.md +16 -10
  181. package/docs/llms/21-array-operations.md +76 -26
  182. package/docs/llms/22-cycle-detection.md +1 -1
  183. package/docs/llms/23-copy-from.md +2 -2
  184. package/docs/llms/24-sync-fields.md +6 -5
  185. package/docs/llms/25-reset-when.md +14 -7
  186. package/docs/llms/26-transform-value.md +4 -4
  187. package/docs/llms/27-revalidate-when.md +16 -15
  188. package/docs/llms/28-submit-and-reset.md +276 -128
  189. package/docs/llms/29-async-preload.md +40 -30
  190. package/docs/llms/30-type-safety-recipes.md +105 -51
  191. package/docs/llms/31-async-validator-debounce.md +96 -53
  192. package/docs/llms/33-validation-strategy.md +178 -66
  193. package/docs/llms/34-i18n.md +69 -43
  194. package/docs/llms/35-unified-contract.md +307 -0
  195. package/docs/llms/36-issue-messages.md +284 -0
  196. package/docs/llms/37-compiled-schema.md +332 -0
  197. package/docs/llms/38-form-flow.md +397 -0
  198. package/llms-index.json +1 -1
  199. package/llms.txt +6968 -3278
  200. package/package.json +23 -4
  201. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  202. package/dist/create-model-B8U-COv-.js +0 -103
  203. package/dist/date-utils-dTT_x_Be.js +0 -31
  204. package/dist/form/create-core-form.d.ts +0 -70
  205. package/dist/form/factories/index.d.ts +0 -6
  206. package/dist/form/factories/node-factory.d.ts +0 -189
  207. package/dist/form/form-submitter.d.ts +0 -121
  208. package/dist/form/nodes/array-node.d.ts +0 -276
  209. package/dist/form/nodes/field-node.d.ts +0 -297
  210. package/dist/form/nodes/form-node.d.ts +0 -326
  211. package/dist/form/nodes/group-node.d.ts +0 -284
  212. package/dist/form/nodes/model-array-node.d.ts +0 -121
  213. package/dist/form/nodes/subscription-manager.d.ts +0 -180
  214. package/dist/form/status-machine.d.ts +0 -115
  215. package/dist/form/types/deep-schema.d.ts +0 -107
  216. package/dist/form/types/form-proxy.d.ts +0 -141
  217. package/dist/form/unique-id.d.ts +0 -45
  218. package/dist/form/validation/config.d.ts +0 -73
  219. package/dist/form/validation/error-handler.d.ts +0 -166
  220. package/dist/form/validators/validation-error.d.ts +0 -8
  221. package/dist/index-BrNkQj1y.js +0 -105
  222. package/dist/loader-Dv_Hl7JC.js +0 -216
  223. package/dist/model-signals-proxy-2vqS243J.js +0 -250
  224. package/dist/node-DGEFMTQu.js +0 -18
  225. package/dist/strategy-BXS_gBfz.js +0 -170
  226. package/dist/validation-error-B-U3RsV0.js +0 -11
@@ -1,7 +1,8 @@
1
1
  ## 18. ARRAY OPERATIONS
2
2
 
3
- Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);
4
- рендер — через ноду формы (`form.items.map` / `.at`).
3
+ Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`) либо через
4
+ ноду формы `FormArray` (`form.items`) — она данными не владеет и передаёт мутацию в массив модели;
5
+ рендер — через ноду формы (`form.items.map` / `.at` / `.keyAt`).
5
6
 
6
7
  ### Array Access
7
8
 
@@ -13,51 +14,73 @@ model.items.length; // реактивная длина
13
14
  model.items.toArray(); // снимок значений
14
15
 
15
16
  // Через ноду формы (рендер / доступ к нодам полей)
16
- form.items.at(0); // FormProxy<Item> | undefined
17
- form.items.map((item, i) => …); // item — FormProxy<Item>
17
+ form.items.at(0); // FormGroup<Item> | undefined
18
+ form.items.map((item, i) => …); // item — FormGroup<Item>
19
+ form.items.length; // число строк (число, не сигнал; в эффекте подписывает)
20
+ form.items.keyAt(0); // стабильный ключ строки — не меняется при move/swap
18
21
  ```
19
22
 
20
- ### Array Methods (на модели)
23
+ ### Array Methods
21
24
 
22
25
  ```typescript
26
+ // на модели
23
27
  model.items.push({ name: '', price: 0 }); // добавить в конец (ПЛОСКИЕ значения!)
24
- model.items.insertAt(0, { name: '', price: 0 }); // вставить по индексу
25
- model.items.removeAt(index); // удалить по индексу
26
- model.items.move(fromIndex, toIndex); // переместить
28
+ model.items.push(); // по шаблону arrayOf(blank)
29
+ model.items.insertAt(0, { name: '', price: 0 }); // вставить по индексу 0…length; за концом — RangeError
30
+ model.items.removeAt(index); // удалить по индексу; нет такого — ничего
31
+ model.items.move(fromIndex, toIndex); // переместить; индекс за концом — RangeError
27
32
  model.items.swap(a, b); // поменять местами
28
33
  model.items.clear(); // очистить
34
+
35
+ // на ноде формы — те же мутации (вставка называется insert)
36
+ form.items.push();
37
+ form.items.insert(0, { name: '', price: 0 });
38
+ form.items.removeAt(index);
39
+ form.items.move(fromIndex, toIndex);
40
+ form.items.swap(a, b);
41
+ form.items.clear();
29
42
  ```
30
43
 
44
+ Формы строк держатся за под-модели элементов: после `move` / `swap` строка остаётся той же нодой
45
+ со своими `touched` и замечаниями.
46
+
47
+ Разница между путями — в `dirty`. Мутация через ноду, которая поменяла состав строк (в том числе
48
+ перестановка), делает массив и форму изменёнными (`form.items.dirty`), снимает это `reset`. Запись в
49
+ модель — нет, как и у поля: загрузка данных форму не «пачкает». Кнопки добавления и удаления в
50
+ интерфейсе — через ноду.
51
+
31
52
  > **push принимает ПЛОСКИЕ значения** (`{ name, price }`), а НЕ FieldConfig-шаблон
32
- > (`{ value, component }`). Component/componentProps берутся из `item`-фабрики схемы.
53
+ > (`{ value, component }`). У массива с шаблоном `arrayOf` переданный объект накладывается на
54
+ > шаблон: недостающие ключи берутся из него (так `add({ type })` из cdk даёт полный элемент). Component/componentProps берутся из `item`-фабрики схемы.
33
55
  > Передача FieldConfig-объектов сломает рендер (`[object Object]` в инпутах).
34
56
 
35
57
  ### Rendering Arrays
36
58
 
37
59
  ```tsx
38
- import { useArrayLength } from '@reformer/core';
60
+ import { useArrayLength } from '@reformer/core/react';
39
61
 
40
- function ItemsList({ form }: { form: FormProxy<MyForm> }) {
62
+ function ItemsList({ form }: { form: FormGroup<MyForm> }) {
41
63
  const length = useArrayLength(form.items);
42
64
 
43
65
  return (
44
66
  <div>
45
67
  {form.items.map((item, index) => (
46
- <div key={index}>
68
+ <div key={form.items.keyAt(index)}>
47
69
  <FormField control={item.name} />
48
70
  <FormField control={item.price} />
49
- <button onClick={() => model.items.removeAt(index)}>Remove</button>
71
+ <button onClick={() => form.items.removeAt(index)}>Remove</button>
50
72
  </div>
51
73
  ))}
52
74
  {length === 0 && <p>No items yet</p>}
53
- <button onClick={() => model.items.push({ name: '', price: 0 })}>Add Item</button>
75
+ <button onClick={() => form.items.push({ name: '', price: 0 })}>Add Item</button>
54
76
  </div>
55
77
  );
56
78
  }
57
79
  ```
58
80
 
59
- > В монорепо используется `FormArraySection` из `@reformer/ui-kit`: `control={form.items}`,
60
- > `itemComponent`, `initialValue`, add/remove/reorder из коробки. См. `find_recipe(topic="form-array")`.
81
+ > В монорепо используется `FieldArraySection` из `@reformer/ui-kit`: `control={form.items}`,
82
+ > `itemComponent`, add/remove/reorder из коробки; шаблон новой строки — `arrayOf(blank)` в модели.
83
+ > См. `find_recipe(topic="field-array")`.
61
84
 
62
85
  ### Per-item behavior — applyEach
63
86
 
@@ -76,6 +99,32 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
76
99
  });
77
100
  ```
78
101
 
102
+ ### Вложенные области — массив в группе и в строке массива
103
+
104
+ Узел `{ model: model.$.<массив>, item }` и операторы `applyEach` / `apply` / `enableWhen` работают на любой глубине:
105
+ цель оператора — **ручка** `model.$.…`, под-модель и нода находятся по её идентичности, а не по
106
+ пути.
107
+
108
+ ```typescript
109
+ const phoneBehavior = defineFormBehavior<Phone>(({ model: phone }) => {
110
+ enableWhen(phone.$.number, () => phone.primary);
111
+ });
112
+
113
+ const contactBehavior = defineFormBehavior<Contact>(({ model: contact }) => {
114
+ applyEach(contact.$.phones, phoneBehavior); // массив внутри строки
115
+ enableWhen(contact.$.address, () => contact.quantity > 0); // группа внутри строки
116
+ apply(contact.$.address, addressBehavior); // под-схема получает настоящую под-модель
117
+ });
118
+
119
+ const behavior = defineFormBehavior<MyForm>(({ model }) => {
120
+ applyEach(model.$.contacts, contactBehavior);
121
+ });
122
+ ```
123
+
124
+ Ручка → value-фасад: `modelOf(model.$.address)` — под-модель (`get` / `set` / `patch`),
125
+ `modelOf(model.$.phones)` — фасад массива (`push` / `removeAt` / `at`). Нода формы по ручке —
126
+ `getNodeForSignal(model.$.address)`.
127
+
79
128
  ### Aggregate write — aggregateInto
80
129
 
81
130
  Агрегатная запись в строки (например, «последняя строка = 100 − Σ остальных»):
@@ -93,21 +142,22 @@ aggregateInto(model.$.rows, (rows) => {
93
142
 
94
143
  ### Array Cross-Validation
95
144
 
96
- Whole-array правило — оператор `cross(sig, (f) => ...)` из `@reformer/core/validation`:
97
- `f` — снапшот `model.get()`, ошибка вешается на скалярное поле-носитель `sig`. Per-item
98
- правила — `each(model.<array>, (im) => ...)`.
145
+ Правило над всем массивом — оператор `cross(sig, check)` из аргумента схемы валидации: `check`
146
+ получает снимок модели, ошибка вешается на скалярное поле-носитель `sig`. Правила строк —
147
+ `applyEach(model.$.<массив>, схема строки)`.
99
148
 
100
149
  ```typescript
101
- import { cross } from '@reformer/core/validation';
102
- import type { ValidationError } from '@reformer/core';
150
+ import { defineValidation } from '@reformer/core/validation';
151
+ import type { ValidationIssue } from '@reformer/core';
103
152
 
104
- const percentagesSumTo100 = (f: MyForm): ValidationError | null => {
105
- const total = f.items.reduce((sum, i) => sum + (i.percentage || 0), 0);
153
+ const percentagesSumTo100 = (form: MyForm): ValidationIssue | null => {
154
+ const total = form.items.reduce((sum, item) => sum + (item.percentage || 0), 0);
106
155
  return Math.abs(total - 100) > 0.01
107
- ? { code: 'invalid_total', message: 'Percentages must sum to 100%' }
156
+ ? { code: 'items.percentTotal', params: { total } } // текст кода — в словаре формы
108
157
  : null;
109
158
  };
110
159
 
111
- // внутри defineValidationSchema<MyForm>(({ model }) => { ... })
112
- cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
160
+ const itemsRules = defineValidation<MyForm>(({ model, cross }) => {
161
+ cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
162
+ });
113
163
  ```
@@ -1,6 +1,6 @@
1
1
  # Cycle Detection — предотвращение «Cycle detected»
2
2
 
3
- ## Как устроено под M1
3
+ ## Как устроено
4
4
 
5
5
  Behaviors на сигналах уже защищены от типичных циклов:
6
6
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## API
11
11
 
12
- Есть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):
12
+ Есть две формы. **Примитив из `@reformer/core/model`** (сигнал → сигнал):
13
13
 
14
14
  ```typescript
15
15
  function copyFrom<T>(
@@ -92,7 +92,7 @@ export const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {
92
92
  ### Как примитив (вне defineFormBehavior)
93
93
 
94
94
  ```typescript
95
- import { copyFrom } from '@reformer/core';
95
+ import { copyFrom } from '@reformer/core/model';
96
96
 
97
97
  const cleanups = [
98
98
  copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
@@ -10,17 +10,18 @@
10
10
 
11
11
  ## API
12
12
 
13
- Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
13
+ Одинаково в примитиве (`@reformer/core/model`) и DSL (`@reformer/core/behaviors`):
14
14
 
15
15
  ```typescript
16
16
  // примитив: возвращает cleanup
17
- function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): () => void;
17
+ function syncFields<T>(a: Signal<T>, b: Signal<NoInfer<T>>, options?: { transform?: (value: T) => T }): () => void;
18
18
 
19
19
  // DSL: cleanup управляется формой
20
- function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): void;
20
+ function syncFields<T>(a: Signal<T>, b: Signal<NoInfer<T>>, options?: { transform?: (value: T) => T }): void;
21
21
  ```
22
22
 
23
- `a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:
23
+ `a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. Тип пары задаёт `a`: поле `number | null`
24
+ вторым аргументом к `number` не компилируется — синхронизация писала бы `null` в `number`. `transform` **асимметричен**:
24
25
  применяется только при движении значения `a → b`. Опции `debounce`/`when` нет.
25
26
 
26
27
  ## Examples
@@ -57,7 +58,7 @@ export const codeBehavior = defineFormBehavior<DisplayForm>(({ model }) => {
57
58
  ### Как примитив (вне defineFormBehavior)
58
59
 
59
60
  ```typescript
60
- import { syncFields } from '@reformer/core';
61
+ import { syncFields } from '@reformer/core/model';
61
62
  const stop = syncFields(model.$.syncField1, model.$.syncField2);
62
63
  // stop() — отписаться
63
64
  ```
@@ -5,22 +5,29 @@
5
5
  `resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это
6
6
  альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но
7
7
  содержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но
8
- поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение.
8
+ поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение
9
+ и **обязателен по типу**, если `null` не входит в тип поля.
9
10
 
10
11
  ## API
11
12
 
12
- Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
13
+ Одинаково в примитиве (`@reformer/core/model`) и DSL (`@reformer/core/behaviors`):
13
14
 
14
15
  ```typescript
16
+ // опции: поле с null — необязательны; поле без null — resetValue обязателен
17
+ type ResetWhenOptionsArg<T> = null extends T
18
+ ? [options?: { resetValue?: NoInfer<T> }]
19
+ : [options: { resetValue: NoInfer<T> }];
20
+
15
21
  // примитив: возвращает cleanup
16
- function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): () => void;
22
+ function resetWhen<T>(target: Signal<T>, condition: () => boolean, ...options: ResetWhenOptionsArg<T>): () => void;
17
23
 
18
24
  // DSL: cleanup управляется формой
19
- function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): void;
25
+ function resetWhen<T>(target: Signal<T>, condition: () => boolean, ...options: ResetWhenOptionsArg<T>): void;
20
26
  ```
21
27
 
22
28
  `target` — сигнал (`model.$.field`). `condition` — реактивное условие (читает `model.*`).
23
- `resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
29
+ `resetValue` по умолчанию `null` — поэтому полю без `null` в типе (`string`, `number`) его нужно
30
+ задать явно, иначе код не компилируется. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
24
31
  поля не трогаются оператором (значения принадлежат модели).
25
32
 
26
33
  ## Examples
@@ -55,7 +62,7 @@ export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) =>
55
62
  ### Как примитив (вне defineFormBehavior)
56
63
 
57
64
  ```typescript
58
- import { resetWhen } from '@reformer/core';
65
+ import { resetWhen } from '@reformer/core/model';
59
66
  const stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
60
67
  ```
61
68
 
@@ -79,7 +86,7 @@ resetWhen(model.$.amount, () => model.skipPayment, { resetValue: 0 });
79
86
  ```
80
87
 
81
88
  ```typescript
82
- // ❌ Для строкового поля без resetValue прилетит null, а Input ждёт string
89
+ // ❌ Для строкового поля без resetValue прилетел бы null, а Input ждёт string — не компилируется
83
90
  resetWhen(model.$.cardNumber, () => model.paymentType !== 'card');
84
91
 
85
92
  // ✅ Явный resetValue для строк
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## API
13
13
 
14
- Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
14
+ Одинаково в примитиве (`@reformer/core/model`) и DSL (`@reformer/core/behaviors`):
15
15
 
16
16
  ```typescript
17
17
  // примитив: возвращает cleanup
@@ -22,7 +22,7 @@ function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): voi
22
22
  ```
23
23
 
24
24
  `target` — сигнал (`model.$.field`). `transformer` — чистая идемпотентная функция значения.
25
- Дополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора
25
+ Дополнительных опций (`debounce`, `onUserChangeOnly`) и готового набора
26
26
  `transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.
27
27
 
28
28
  ## Examples
@@ -70,7 +70,7 @@ export const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {
70
70
  ### Как примитив (вне defineFormBehavior)
71
71
 
72
72
  ```typescript
73
- import { transformValue } from '@reformer/core';
73
+ import { transformValue } from '@reformer/core/model';
74
74
  const stop = transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
75
75
  ```
76
76
 
@@ -122,7 +122,7 @@ A: 99% — неидемпотентный transformer. Проверь `transform
122
122
 
123
123
  **Q: Трансформация не применяется.**
124
124
  A: Проверь, что behavior зарегистрирован (`createForm({ behavior })`) / примитив не отписан,
125
- и что форма не пересоздаётся на каждый рендер (используй `useMemo`).
125
+ и что форма не пересоздаётся на каждый рендер (сборка — через `useFormBundle(() => createForm(…))`).
126
126
 
127
127
  **Q: Каретка input прыгает при наборе.**
128
128
  A: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## API
13
13
 
14
- `revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core`).
14
+ `revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core/model`).
15
15
  Его сигнатура НЕ зависит от контракта валидации:
16
16
 
17
17
  ```typescript
@@ -23,7 +23,7 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
23
23
  ```
24
24
 
25
25
  `deps` — массив сигналов-триггеров (`model.$.field`). `revalidate` — колбэк (обычно
26
- `() => void validateModel(model, schema)`, где `schema` — `defineValidationSchema<T>(({ model }) => …)`).
26
+ `() => void validateModel(model, schema)`, где `schema` — `defineValidation<T>(({ model }) => …)`).
27
27
  Вызывается при изменении любой зависимости (НЕ на инициализации), вне effect-контекста.
28
28
 
29
29
  ## Examples
@@ -43,7 +43,8 @@ export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
43
43
 
44
44
  Пример: `revalidateWhen([model.$.maxAmount], () => void validateModel(model, paymentSchema))`.
45
45
  Схема `paymentSchema` — стабильный module-level `const` (важно для отмены устаревших прогонов
46
- в `validateModel`), где правило amount навешано через `cross(model.$.amount, amountVsMax)`.
46
+ в `validateModel`), где правило amount навешано оператором `cross` области схемы:
47
+ `cross(model.$.amount, amountVsMax)`.
47
48
 
48
49
  ### Несколько триггеров
49
50
 
@@ -66,7 +67,7 @@ import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
66
67
  import { validateModel } from '@reformer/core/validation';
67
68
 
68
69
  // правило совпадения — cross-field в схеме: cross(model.$.confirmPassword, passwordsMatch),
69
- // где passwordsMatch читает снапшот формы (см. 03-api-signatures.md)
70
+ // где passwordsMatch получает снимок формы (см. 03-api-signatures.md)
70
71
  export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ model }) => {
71
72
  // при смене password перевалидируем схему (confirm перепроверится)
72
73
  revalidateWhen([model.$.password], () => {
@@ -78,7 +79,7 @@ export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ mode
78
79
  ### Как примитив (вне defineFormBehavior)
79
80
 
80
81
  ```typescript
81
- import { revalidateWhen } from '@reformer/core';
82
+ import { revalidateWhen } from '@reformer/core/model';
82
83
  import { validateModel } from '@reformer/core/validation';
83
84
 
84
85
  const stop = revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
@@ -100,13 +101,13 @@ revalidateWhen([model.$.maxAmount, model.$.discount], () => validateModel(model,
100
101
  revalidateWhen([model.$.maxAmount], () => validateModel(model, schema));
101
102
  // а валидатор amount — статичный max(1000), не читает maxAmount
102
103
 
103
- // ✅ Сначала cross-field правило (читает снапшот формы) в схеме валидации, потом revalidateWhen
104
- const amountVsMax = (f: Form): ValidationError | null =>
105
- f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
106
- ? { code: 'tooBig', message: '...' }
104
+ // ✅ Сначала cross-field правило (получает снимок формы) в схеме валидации, потом revalidateWhen
105
+ const amountVsMax = (form: Form): ValidationIssue | null =>
106
+ form.amount != null && form.maxAmount != null && form.amount > form.maxAmount
107
+ ? { code: 'tooBig' }
107
108
  : null;
108
109
 
109
- const schema = defineValidationSchema<Form>(({ model }) => {
110
+ const schema = defineValidation<Form>(({ model, cross }) => {
110
111
  cross(model.$.amount, amountVsMax); // навешиваем правило на поле amount
111
112
  });
112
113
  revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
@@ -115,18 +116,18 @@ revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
115
116
  ## Troubleshooting
116
117
 
117
118
  **Q: Ошибка target не пропадает после изменения триггера.**
118
- A: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, fn)`,
119
- где `fn` берёт снапшот формы (`model.get()` / `f`); (2) в `deps` передан именно сигнал (`model.$.trigger`);
120
- (3) колбэк вызывает `validateModel` (роутит ошибки в ноды через `getNodeForSignal(sig).setErrors`).
119
+ A: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, check)`,
120
+ где `check` получает снимок формы; (2) в `deps` передан именно сигнал (`model.$.trigger`);
121
+ (3) колбэк вызывает `validateModel` (он разносит ошибки по нодам формы).
121
122
 
122
123
  **Q: Как перевалидировать только одно поле, а не всю схему?**
123
124
  A: Схема — обычная функция, поэтому опиши стабильную под-схему только для этого поля и прогони её:
124
125
  `validateModel(model, oneFieldSchema)`, где
125
- `const oneFieldSchema = defineValidationSchema<Form>(({ model }) => cross(model.$.amount, amountVsMax))`.
126
+ `const oneFieldSchema = defineValidation<Form>(({ model, cross }) => cross(model.$.amount, amountVsMax))`.
126
127
  Раннер выставит/погасит ошибки только затронутых полей (гашение накапливается на пару `(model, schema)`).
127
128
 
128
129
  ## See also
129
130
 
130
- - [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross(sig, fn)` и раннер `validateModel`
131
+ - [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross` области схемы и раннер `validateModel`
131
132
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
132
133
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения