@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.
- package/README.md +90 -68
- package/dist/behaviors-value-HA_Q03hc.js +108 -0
- package/dist/behaviors.js +382 -217
- package/dist/create-model-BvcbNPgL.js +30 -0
- package/dist/date-utils-90-qZbse.js +43 -0
- package/dist/{en-CjAqn7h0.js → en-HP8XEych.js} +6 -3
- package/dist/form/aggregate-signals.d.ts +21 -63
- package/dist/form/behaviors/collections.d.ts +42 -8
- package/dist/form/behaviors/context.d.ts +37 -4
- package/dist/form/behaviors/index.d.ts +6 -4
- package/dist/form/behaviors/internals.d.ts +9 -3
- package/dist/form/behaviors/node.d.ts +25 -2
- package/dist/form/behaviors/operators.d.ts +17 -9
- package/dist/form/behaviors/schema-operators.d.ts +78 -0
- package/dist/form/behaviors/types.d.ts +25 -5
- package/dist/form/create-form.d.ts +57 -70
- package/dist/form/flow/flow.d.ts +87 -0
- package/dist/form/form-bundle.d.ts +153 -0
- package/dist/form/form-config-base.d.ts +46 -0
- package/dist/form/form-level.d.ts +121 -0
- package/dist/form/form-proxy-builder.d.ts +6 -22
- package/dist/form/index.d.ts +20 -20
- package/dist/form/nodes/form-array-node.d.ts +100 -0
- package/dist/form/nodes/form-control-node.d.ts +73 -0
- package/dist/form/nodes/form-group-node.d.ts +66 -0
- package/dist/form/nodes/node-kind.d.ts +29 -0
- package/dist/form/nodes/state/availability.d.ts +6 -0
- package/dist/form/nodes/state/component.d.ts +6 -0
- package/dist/form/nodes/state/index.d.ts +12 -0
- package/dist/form/nodes/state/interaction.d.ts +13 -0
- package/dist/form/nodes/state/issues.d.ts +9 -0
- package/dist/form/nodes/state/pending.d.ts +6 -0
- package/dist/form/schema/compile.d.ts +59 -0
- package/dist/form/schema/compiled.d.ts +143 -0
- package/dist/form/schema/row-key.d.ts +11 -0
- package/dist/form/schema-controller.d.ts +153 -0
- package/dist/form/schema-subtree.d.ts +28 -0
- package/dist/form/signal-node-registry.d.ts +27 -22
- package/dist/form/submission.d.ts +93 -0
- package/dist/form/type-guards.d.ts +14 -73
- package/dist/form/types/contracts.d.ts +19 -92
- package/dist/form/types/field-config.d.ts +27 -0
- package/dist/form/types/index.d.ts +5 -57
- package/dist/form/types/nodes.d.ts +159 -0
- package/dist/form/types/schema-node.d.ts +147 -46
- package/dist/form/types/validation-schema.d.ts +23 -27
- package/dist/form/validation/context.d.ts +46 -6
- package/dist/form/validation/index.d.ts +9 -8
- package/dist/form/validation/operators.d.ts +71 -17
- package/dist/form/validation/run.d.ts +93 -18
- package/dist/form/validation/strategy.d.ts +41 -13
- package/dist/form/validation/types.d.ts +74 -19
- package/dist/form/validators/date-utils.d.ts +13 -1
- package/dist/form/validators/email.d.ts +8 -7
- package/dist/form/validators/file-type.d.ts +8 -7
- package/dist/form/validators/file-utils.d.ts +2 -1
- package/dist/form/validators/future-date.d.ts +8 -7
- package/dist/form/validators/index.d.ts +1 -1
- package/dist/form/validators/integer.d.ts +8 -7
- package/dist/form/validators/is-date.d.ts +11 -9
- package/dist/form/validators/is-number.d.ts +11 -9
- package/dist/form/validators/max-age.d.ts +13 -9
- package/dist/form/validators/max-date.d.ts +8 -7
- package/dist/form/validators/max-file-size.d.ts +8 -7
- package/dist/form/validators/max-files.d.ts +8 -7
- package/dist/form/validators/max-length.d.ts +14 -7
- package/dist/form/validators/max-total-file-size.d.ts +8 -7
- package/dist/form/validators/max.d.ts +10 -8
- package/dist/form/validators/min-age.d.ts +12 -9
- package/dist/form/validators/min-date.d.ts +8 -7
- package/dist/form/validators/min-file-size.d.ts +8 -7
- package/dist/form/validators/min-files.d.ts +11 -9
- package/dist/form/validators/min-length.d.ts +14 -7
- package/dist/form/validators/min.d.ts +10 -8
- package/dist/form/validators/multiple-of.d.ts +10 -8
- package/dist/form/validators/non-negative.d.ts +8 -7
- package/dist/form/validators/non-zero.d.ts +8 -7
- package/dist/form/validators/past-date.d.ts +8 -7
- package/dist/form/validators/pattern.d.ts +9 -8
- package/dist/form/validators/phone.d.ts +8 -7
- package/dist/form/validators/required.d.ts +11 -9
- package/dist/form/validators/url.d.ts +20 -13
- package/dist/form/validators/validation-issue.d.ts +8 -0
- package/dist/i18n/descriptor.d.ts +1 -1
- package/dist/i18n/en.json.d.ts +5 -1
- package/dist/i18n/locale-store.d.ts +7 -0
- package/dist/i18n/message-format.d.ts +24 -4
- package/dist/i18n/ru.json.d.ts +5 -1
- package/dist/i18n/translator.d.ts +11 -0
- package/dist/i18n/validation-message.d.ts +42 -7
- package/dist/i18n.d.ts +7 -12
- package/dist/i18n.js +66 -283
- package/dist/index.d.ts +16 -7
- package/dist/index.js +958 -2798
- package/dist/loader-BWkigpVe.js +52 -0
- package/dist/locale/en.js +1 -1
- package/dist/locale/en.json +4 -1
- package/dist/locale/ru.js +4 -1
- package/dist/locale/ru.json +4 -1
- package/dist/locale-CG54AU2Q.js +219 -0
- package/dist/locale.js +1 -1
- package/dist/model/behaviors-value.d.ts +24 -6
- package/dist/model/create-model.d.ts +2 -2
- package/dist/model/derived-registry.d.ts +1 -1
- package/dist/model/index.d.ts +16 -5
- package/dist/model/model-nodes.d.ts +138 -8
- package/dist/model/model-signals-proxy.d.ts +11 -5
- package/dist/model/model-value-proxy.d.ts +84 -3
- package/dist/model/safe-effect.d.ts +33 -1
- package/dist/model/types.d.ts +70 -17
- package/dist/model.js +21 -17
- package/dist/node-kind-C-r6ZMdo.js +15 -0
- package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
- package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
- package/dist/platforms/react/form-bundle-context.d.ts +27 -0
- package/dist/platforms/react/hooks/types.d.ts +57 -38
- package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
- package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
- package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
- package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
- package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
- package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
- package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
- package/dist/platforms/react/i18n/context.d.ts +6 -1
- package/dist/platforms/react/i18n/hooks.d.ts +16 -10
- package/dist/platforms/react/i18n/provider.d.ts +9 -2
- package/dist/platforms/react/index.d.ts +7 -4
- package/dist/react.d.ts +22 -0
- package/dist/react.js +505 -0
- package/dist/runtime-token.d.ts +1 -2
- package/dist/safe-effect-BbtX3KuN.js +643 -0
- package/dist/schema-controller-CpiO2JoK.js +143 -0
- package/dist/signals.d.ts +15 -9
- package/dist/signals.js +11 -6
- package/dist/strategy-CoFSemuP.js +375 -0
- package/dist/type-guards-D-N_5Ddh.js +19 -0
- package/dist/validation-issue-B9mjI8jz.js +7 -0
- package/dist/validation-message-Ced9MGxs.js +194 -0
- package/dist/validation.js +9 -8
- package/dist/validators/email.js +5 -5
- package/dist/validators/future-date.js +8 -8
- package/dist/validators/integer.js +3 -3
- package/dist/validators/is-date.js +4 -4
- package/dist/validators/is-number.js +4 -4
- package/dist/validators/max-age.js +8 -9
- package/dist/validators/max-date.js +8 -8
- package/dist/validators/max-length.js +7 -7
- package/dist/validators/max.js +4 -4
- package/dist/validators/min-age.js +10 -8
- package/dist/validators/min-date.js +3 -3
- package/dist/validators/min-length.js +5 -5
- package/dist/validators/min.js +4 -4
- package/dist/validators/multiple-of.js +11 -7
- package/dist/validators/non-negative.js +3 -3
- package/dist/validators/non-zero.js +3 -3
- package/dist/validators/past-date.js +8 -8
- package/dist/validators/pattern.js +5 -5
- package/dist/validators/phone.js +11 -9
- package/dist/validators/required.js +2 -2
- package/dist/validators/url.js +48 -12
- package/dist/validators.js +17 -17
- package/docs/llms/01-api-reference.md +60 -34
- package/docs/llms/02-quick-start.md +111 -80
- package/docs/llms/03-api-signatures.md +368 -153
- package/docs/llms/04-common-patterns.md +58 -56
- package/docs/llms/05-common-mistakes.md +69 -44
- package/docs/llms/06-troubleshooting.md +14 -9
- package/docs/llms/07-complete-import.md +74 -43
- package/docs/llms/08-form-types.md +11 -4
- package/docs/llms/09-formschema.md +131 -83
- package/docs/llms/10-arrays.md +119 -69
- package/docs/llms/11-async-watchfield.md +2 -2
- package/docs/llms/13-multi-step.md +134 -63
- package/docs/llms/14-extended-mistakes.md +20 -17
- package/docs/llms/15-project-structure.md +221 -74
- package/docs/llms/16-ui-components.md +55 -42
- package/docs/llms/17-nonexistent-api.md +199 -32
- package/docs/llms/18-conditional-fields.md +66 -32
- package/docs/llms/19-reading-values.md +15 -3
- package/docs/llms/20-compute-vs-watch.md +16 -10
- package/docs/llms/21-array-operations.md +76 -26
- package/docs/llms/22-cycle-detection.md +1 -1
- package/docs/llms/23-copy-from.md +2 -2
- package/docs/llms/24-sync-fields.md +6 -5
- package/docs/llms/25-reset-when.md +14 -7
- package/docs/llms/26-transform-value.md +4 -4
- package/docs/llms/27-revalidate-when.md +16 -15
- package/docs/llms/28-submit-and-reset.md +276 -128
- package/docs/llms/29-async-preload.md +40 -30
- package/docs/llms/30-type-safety-recipes.md +105 -51
- package/docs/llms/31-async-validator-debounce.md +96 -53
- package/docs/llms/33-validation-strategy.md +178 -66
- package/docs/llms/34-i18n.md +69 -43
- package/docs/llms/35-unified-contract.md +307 -0
- package/docs/llms/36-issue-messages.md +284 -0
- package/docs/llms/37-compiled-schema.md +332 -0
- package/docs/llms/38-form-flow.md +397 -0
- package/llms-index.json +1 -1
- package/llms.txt +6968 -3278
- package/package.json +23 -4
- package/dist/behaviors-value-C7dSEhCc.js +0 -134
- package/dist/create-model-B8U-COv-.js +0 -103
- package/dist/date-utils-dTT_x_Be.js +0 -31
- package/dist/form/create-core-form.d.ts +0 -70
- package/dist/form/factories/index.d.ts +0 -6
- package/dist/form/factories/node-factory.d.ts +0 -189
- package/dist/form/form-submitter.d.ts +0 -121
- package/dist/form/nodes/array-node.d.ts +0 -276
- package/dist/form/nodes/field-node.d.ts +0 -297
- package/dist/form/nodes/form-node.d.ts +0 -326
- package/dist/form/nodes/group-node.d.ts +0 -284
- package/dist/form/nodes/model-array-node.d.ts +0 -121
- package/dist/form/nodes/subscription-manager.d.ts +0 -180
- package/dist/form/status-machine.d.ts +0 -115
- package/dist/form/types/deep-schema.d.ts +0 -107
- package/dist/form/types/form-proxy.d.ts +0 -141
- package/dist/form/unique-id.d.ts +0 -45
- package/dist/form/validation/config.d.ts +0 -73
- package/dist/form/validation/error-handler.d.ts +0 -166
- package/dist/form/validators/validation-error.d.ts +0 -8
- package/dist/index-BrNkQj1y.js +0 -105
- package/dist/loader-Dv_Hl7JC.js +0 -216
- package/dist/model-signals-proxy-2vqS243J.js +0 -250
- package/dist/node-DGEFMTQu.js +0 -18
- package/dist/strategy-BXS_gBfz.js +0 -170
- 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
|
-
|
|
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); //
|
|
17
|
-
form.items.map((item, i) => …); // 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.
|
|
25
|
-
model.items.
|
|
26
|
-
model.items.
|
|
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 }`).
|
|
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:
|
|
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={() =>
|
|
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={() =>
|
|
75
|
+
<button onClick={() => form.items.push({ name: '', price: 0 })}>Add Item</button>
|
|
54
76
|
</div>
|
|
55
77
|
);
|
|
56
78
|
}
|
|
57
79
|
```
|
|
58
80
|
|
|
59
|
-
> В монорепо используется `
|
|
60
|
-
> `itemComponent`,
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
Правило над всем массивом — оператор `cross(sig, check)` из аргумента схемы валидации: `check`
|
|
146
|
+
получает снимок модели, ошибка вешается на скалярное поле-носитель `sig`. Правила строк —
|
|
147
|
+
`applyEach(model.$.<массив>, схема строки)`.
|
|
99
148
|
|
|
100
149
|
```typescript
|
|
101
|
-
import {
|
|
102
|
-
import type {
|
|
150
|
+
import { defineValidation } from '@reformer/core/validation';
|
|
151
|
+
import type { ValidationIssue } from '@reformer/core';
|
|
103
152
|
|
|
104
|
-
const percentagesSumTo100 = (
|
|
105
|
-
const total =
|
|
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: '
|
|
156
|
+
? { code: 'items.percentTotal', params: { total } } // текст кода — в словаре формы
|
|
108
157
|
: null;
|
|
109
158
|
};
|
|
110
159
|
|
|
111
|
-
|
|
112
|
-
cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
|
|
160
|
+
const itemsRules = defineValidation<MyForm>(({ model, cross }) => {
|
|
161
|
+
cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
|
|
162
|
+
});
|
|
113
163
|
```
|
|
@@ -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
|
|
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
|
|
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`. `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
и что форма не пересоздаётся на каждый рендер (
|
|
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` — `
|
|
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 навешано
|
|
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
|
|
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 правило (
|
|
104
|
-
const amountVsMax = (
|
|
105
|
-
|
|
106
|
-
? { code: 'tooBig'
|
|
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 =
|
|
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,
|
|
119
|
-
где `
|
|
120
|
-
(3) колбэк вызывает `validateModel` (
|
|
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 =
|
|
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
|
|
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) — реактивные производные значения
|