@reformer/core 10.2.0 → 11.0.0-beta.2
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/dist/behaviors.js +134 -134
- package/dist/form/form-submitter.d.ts +0 -4
- package/dist/form/hooks/types.d.ts +3 -3
- package/dist/form/index.d.ts +0 -3
- package/dist/form/nodes/array-node.d.ts +0 -84
- package/dist/form/nodes/form-node.d.ts +0 -14
- package/dist/form/nodes/group-node.d.ts +0 -4
- package/dist/form/status-machine.d.ts +0 -38
- package/dist/form/types/contracts.d.ts +0 -6
- package/dist/form/types/deep-schema.d.ts +0 -9
- package/dist/form/types/index.d.ts +4 -38
- package/dist/form/types/schema-node.d.ts +13 -16
- package/dist/form/types/validation-schema.d.ts +0 -25
- package/dist/form/unique-id.d.ts +3 -11
- package/dist/form/validators.d.ts +8 -3
- package/dist/form-model-DhW2HZOB.js +323 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +448 -636
- package/dist/runtime-token.d.ts +23 -0
- package/dist/state/form-model.d.ts +14 -0
- package/dist/state/index.d.ts +2 -1
- package/dist/state/types.d.ts +39 -11
- package/dist/state.js +18 -18
- package/dist/validation.js +1 -1
- package/dist/validators.d.ts +0 -4
- package/dist/validators.js +88 -122
- package/docs/llms/01-api-reference.md +63 -0
- package/docs/llms/02-quick-start.md +143 -0
- package/docs/llms/03-api-signatures.md +273 -0
- package/docs/llms/04-common-patterns.md +202 -0
- package/docs/llms/05-common-mistakes.md +167 -0
- package/docs/llms/06-troubleshooting.md +11 -0
- package/docs/llms/07-complete-import.md +99 -0
- package/docs/llms/08-form-types.md +47 -0
- package/docs/llms/09-formschema.md +143 -0
- package/docs/llms/10-arrays.md +161 -0
- package/docs/llms/11-async-watchfield.md +53 -0
- package/docs/llms/12-array-cleanup.md +31 -0
- package/docs/llms/13-multi-step.md +91 -0
- package/docs/llms/14-extended-mistakes.md +98 -0
- package/docs/llms/15-project-structure.md +125 -0
- package/docs/llms/16-ui-components.md +189 -0
- package/docs/llms/17-nonexistent-api.md +93 -0
- package/docs/llms/18-conditional-fields.md +183 -0
- package/docs/llms/19-reading-values.md +88 -0
- package/docs/llms/20-compute-vs-watch.md +147 -0
- package/docs/llms/21-array-operations.md +113 -0
- package/docs/llms/22-cycle-detection.md +86 -0
- package/docs/llms/23-copy-from.md +141 -0
- package/docs/llms/24-sync-fields.md +114 -0
- package/docs/llms/25-reset-when.md +115 -0
- package/docs/llms/26-transform-value.md +136 -0
- package/docs/llms/27-revalidate-when.md +132 -0
- package/docs/llms/28-submit-and-reset.md +259 -0
- package/docs/llms/29-async-preload.md +193 -0
- package/docs/llms/30-type-safety-recipes.md +159 -0
- package/docs/llms/31-async-validator-debounce.md +135 -0
- package/docs/llms/32-async-options-loading.md +108 -0
- package/docs/llms/33-validation-strategy.md +193 -0
- package/llms.txt +219 -1533
- package/package.json +33 -10
- package/dist/derived-registry-CluZCQ7U.js +0 -16
- package/dist/form-model-DYCzGGKF.js +0 -286
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Cycle Detection — предотвращение «Cycle detected»
|
|
2
|
+
|
|
3
|
+
## Как устроено под M1
|
|
4
|
+
|
|
5
|
+
Behaviors на сигналах уже защищены от типичных циклов:
|
|
6
|
+
|
|
7
|
+
- `compute`/`computeFrom` пишут в цель только если значение изменилось (**peek-guard**), а цель
|
|
8
|
+
не входит в источники → сходящийся пересчёт не зацикливается;
|
|
9
|
+
- `transformValue`/`resetWhen`/`syncFields`/`enableWhen` откладывают запись состояния вне
|
|
10
|
+
effect-контекста (`runOutsideEffect` / микротаск) — эффект «читает и пишет один сигнал» не падает;
|
|
11
|
+
- `onChange`-колбэк выполняется вне effect-контекста, поэтому в нём можно свободно писать сигналы/ноды.
|
|
12
|
+
|
|
13
|
+
Тебе НЕ нужно вручную ставить `{ immediate: false }`, guard'ить `disabled.value` перед
|
|
14
|
+
`disable()` или сравнивать значения перед записью — это делается внутри операторов.
|
|
15
|
+
|
|
16
|
+
## Когда всё-таки возникает «Cycle detected»
|
|
17
|
+
|
|
18
|
+
### 1. Расходящийся взаимный compute
|
|
19
|
+
|
|
20
|
+
Два вычисления, которые бесконечно гоняют значение друг у друга без стабилизации:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ❌ расходится → preact бросает «Cycle detected»
|
|
24
|
+
compute(model.$.a, () => model.b + 1);
|
|
25
|
+
compute(model.$.b, () => model.a + 1);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
DSL перехватывает это и заменяет понятной ошибкой с именем поля и подсказкой. Решение —
|
|
29
|
+
однонаправленная зависимость или стабилизирующее условие `when`:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// ✅ одно направление
|
|
33
|
+
compute(model.$.total, () => model.price * model.qty);
|
|
34
|
+
|
|
35
|
+
// ✅ стабилизация условием
|
|
36
|
+
compute(model.$.a, () => model.b + 1, { when: () => model.a !== model.b + 1 });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Неидемпотентный transformValue
|
|
40
|
+
|
|
41
|
+
`transformValue` пишет обратно в то же поле. Если `f(f(x)) !== f(x)` — цикл:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// ❌ f(f(x)) = "prefix-prefix-x" ≠ f(x)
|
|
45
|
+
transformValue(model.$.field, (v) => `prefix-${v}`);
|
|
46
|
+
|
|
47
|
+
// ✅ guard «уже преобразовано»
|
|
48
|
+
transformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Условие `resetWhen`/`copyFrom`, читающее собственную цель
|
|
52
|
+
|
|
53
|
+
Если условие зависит от значения целевого поля — сброс/копия триггерит своё же условие:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// ❌ самотриггер — условие читает cardNumber (цель)
|
|
57
|
+
resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
|
|
58
|
+
|
|
59
|
+
// ✅ условие зависит только от независимого поля
|
|
60
|
+
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Prefer built-in operators over manual logic
|
|
64
|
+
|
|
65
|
+
Вместо ручной сборки reset-on-disable через `onChange` — используй `enableWhen({ resetOnDisable })`:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// ✅ SIMPLE — enableWhen с resetOnDisable (рекомендуется)
|
|
69
|
+
enableWhen(model.$.vehicleVin, () => model.insuranceType === 'casco', { resetOnDisable: true });
|
|
70
|
+
enableWhen(model.$.vehicleBrand, () => model.insuranceType === 'casco', { resetOnDisable: true });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`enableWhen` принимает и **массив** целей, и группу — можно включать несколько полей одним условием:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
enableWhen([model.$.propertyValue, model.$.initialPayment], () => model.loanType === 'mortgage', {
|
|
77
|
+
resetOnDisable: true,
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Key Rules
|
|
82
|
+
|
|
83
|
+
1. Производные значения — через `compute`/`computeFrom` (peek-guard встроен), не ручным `onChange` + запись.
|
|
84
|
+
2. `transformValue`-трансформер должен быть идемпотентным (`f(f(x)) === f(x)`).
|
|
85
|
+
3. Условие `resetWhen`/`copyFrom`/`enableWhen` НЕ должно читать собственную цель.
|
|
86
|
+
4. Расходящийся взаимный `compute` — разорви однонаправленной зависимостью или `when`.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# copyFrom — Копирование значений между полями
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`copyFrom` декларативно копирует значение одного поля (или группы) в другое при выполнении
|
|
6
|
+
условия `when`. Используется для UX-сценариев «совпадает с …»: «адрес проживания = адрес
|
|
7
|
+
регистрации», «email для уведомлений = основной email», «billing = shipping». Оператор сам
|
|
8
|
+
выходит из reactive-контекста (`runOutsideEffect`), поэтому не порождает «Cycle detected».
|
|
9
|
+
|
|
10
|
+
## API
|
|
11
|
+
|
|
12
|
+
Есть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
function copyFrom<T>(
|
|
16
|
+
source: ReadonlySignal<T>,
|
|
17
|
+
target: Signal<T>,
|
|
18
|
+
options?: { when?: () => boolean; transform?: (value: T) => T }
|
|
19
|
+
): () => void; // возвращает cleanup
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**DSL-оператор из `@reformer/core/behaviors`** (сигнал ИЛИ группа):
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
function copyFrom<T>(
|
|
26
|
+
source: ReadonlySignal<T> | object, // model.$.field или model.$.group
|
|
27
|
+
target: Signal<T> | object,
|
|
28
|
+
options?: { when?: () => boolean; transform?: (value: T) => T }
|
|
29
|
+
): void; // cleanup управляется формой
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`when` — реактивное условие (читает сигналы модели через `model.*`). `transform` применяется
|
|
33
|
+
к значению перед записью в target. Отдельной опции `fields`/`debounce` нет.
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
### Базовый сценарий — синхронизация двух адресов
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { defineFormBehavior, copyFrom } from '@reformer/core/behaviors';
|
|
41
|
+
|
|
42
|
+
type OrderForm = {
|
|
43
|
+
useShippingAsBilling: boolean;
|
|
44
|
+
shippingAddress: string;
|
|
45
|
+
billingAddress: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const orderBehavior = defineFormBehavior<OrderForm>(({ model }) => {
|
|
49
|
+
copyFrom(model.$.shippingAddress, model.$.billingAddress, {
|
|
50
|
+
when: () => model.useShippingAsBilling === true,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Пример: `copyFrom(model.$.shippingAddress, model.$.billingAddress, { when: () => model.useShippingAsBilling })`.
|
|
56
|
+
|
|
57
|
+
### Копирование группы целиком
|
|
58
|
+
|
|
59
|
+
Когда source/target — группы (`model.$.registrationAddress`), копируется всё значение группы:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { defineFormBehavior, copyFrom } from '@reformer/core/behaviors';
|
|
63
|
+
|
|
64
|
+
type ProfileForm = {
|
|
65
|
+
sameAsRegistration: boolean;
|
|
66
|
+
registrationAddress: Address;
|
|
67
|
+
residenceAddress: Address;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const profileBehavior = defineFormBehavior<ProfileForm>(({ model }) => {
|
|
71
|
+
copyFrom(model.$.registrationAddress, model.$.residenceAddress, {
|
|
72
|
+
when: () => model.sameAsRegistration === true,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Copy + transform — нормализация при копировании
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { defineFormBehavior, copyFrom } from '@reformer/core/behaviors';
|
|
81
|
+
|
|
82
|
+
type ContactForm = { sameEmail: boolean; email: string; emailAdditional: string };
|
|
83
|
+
|
|
84
|
+
export const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {
|
|
85
|
+
copyFrom(model.$.email, model.$.emailAdditional, {
|
|
86
|
+
when: () => model.sameEmail === true,
|
|
87
|
+
transform: (value) => (typeof value === 'string' ? value.trim().toLowerCase() : value),
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Как примитив (вне defineFormBehavior)
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { copyFrom } from '@reformer/core';
|
|
96
|
+
|
|
97
|
+
const cleanups = [
|
|
98
|
+
copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
|
|
99
|
+
];
|
|
100
|
+
// teardown: cleanups.forEach((c) => c());
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Anti-patterns
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// ❌ Двусторонняя связь через два copyFrom — конфликт направлений
|
|
107
|
+
copyFrom(model.$.a, model.$.b);
|
|
108
|
+
copyFrom(model.$.b, model.$.a);
|
|
109
|
+
|
|
110
|
+
// ✅ Двусторонняя синхронизация — это работа syncFields
|
|
111
|
+
syncFields(model.$.a, model.$.b);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// ❌ when, читающий саму target — лишний триггер при перезаписи target
|
|
116
|
+
copyFrom(model.$.source, model.$.target, { when: () => model.target === '' });
|
|
117
|
+
|
|
118
|
+
// ✅ when опирается на независимый флаг
|
|
119
|
+
copyFrom(model.$.source, model.$.target, { when: () => model.copyEnabled === true });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Troubleshooting
|
|
123
|
+
|
|
124
|
+
**Q: Скопированное значение не появляется в target.**
|
|
125
|
+
A: Проверьте, что (1) behavior передан в `createForm({ behavior })` (или примитив вызван и не
|
|
126
|
+
отписан); (2) `when` возвращает `true`; (3) типы source/target совместимы (`copyFrom` пишет как есть).
|
|
127
|
+
|
|
128
|
+
**Q: «Cycle detected» при копировании.**
|
|
129
|
+
A: Обычно `when` читает значение target (см. anti-pattern). Условие должно зависеть только от
|
|
130
|
+
source и независимых флагов.
|
|
131
|
+
|
|
132
|
+
**Q: Как откатить копию при снятии флага `when`?**
|
|
133
|
+
A: `copyFrom` при `when === false` просто не пишет (не сбрасывает). Для сброса используй параллельно
|
|
134
|
+
`resetWhen(model.$.target, () => !model.copyEnabled)` (см. `25-reset-when.md`).
|
|
135
|
+
|
|
136
|
+
## See also
|
|
137
|
+
|
|
138
|
+
- [24-sync-fields.md](./24-sync-fields.md) — двусторонняя синхронизация
|
|
139
|
+
- [25-reset-when.md](./25-reset-when.md) — сброс target при выключенном условии
|
|
140
|
+
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
|
|
141
|
+
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `runOutsideEffect` защищает от циклов
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# syncFields — Двусторонняя синхронизация полей
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них
|
|
6
|
+
переписывает второе. Применяется для дублей одного значения в разных частях формы
|
|
7
|
+
(тех. поле + видимое представление, mirror-поля). Внутренний флаг + `runOutsideEffect`
|
|
8
|
+
исключают петли. Для одностороннего копирования — [`copyFrom`](./23-copy-from.md); для
|
|
9
|
+
расчётов — [`compute`](./20-compute-vs-watch.md).
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// примитив: возвращает cleanup
|
|
17
|
+
function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): () => void;
|
|
18
|
+
|
|
19
|
+
// DSL: cleanup управляется формой
|
|
20
|
+
function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): void;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:
|
|
24
|
+
применяется только при движении значения `a → b`. Опции `debounce`/`when` нет.
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
### Базовый сценарий — отзеркаливание текста
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { defineFormBehavior, syncFields } from '@reformer/core/behaviors';
|
|
32
|
+
|
|
33
|
+
type MirrorForm = { syncField1: string; syncField2: string };
|
|
34
|
+
|
|
35
|
+
export const mirrorBehavior = defineFormBehavior<MirrorForm>(({ model }) => {
|
|
36
|
+
syncFields(model.$.syncField1, model.$.syncField2);
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Пример: `syncFields(model.$.syncField1, model.$.syncField2)`.
|
|
41
|
+
|
|
42
|
+
### С трансформацией — нормализация при прямой записи
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { defineFormBehavior, syncFields } from '@reformer/core/behaviors';
|
|
46
|
+
|
|
47
|
+
type DisplayForm = { internalCode: string; displayCode: string };
|
|
48
|
+
|
|
49
|
+
export const codeBehavior = defineFormBehavior<DisplayForm>(({ model }) => {
|
|
50
|
+
// internalCode → displayCode: uppercase; обратно значение пишется как есть
|
|
51
|
+
syncFields(model.$.internalCode, model.$.displayCode, {
|
|
52
|
+
transform: (value) => (typeof value === 'string' ? value.toUpperCase() : value),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Как примитив (вне defineFormBehavior)
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { syncFields } from '@reformer/core';
|
|
61
|
+
const stop = syncFields(model.$.syncField1, model.$.syncField2);
|
|
62
|
+
// stop() — отписаться
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Anti-patterns
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// ❌ Симметрично через два copyFrom — конфликт направлений
|
|
69
|
+
copyFrom(model.$.a, model.$.b);
|
|
70
|
+
copyFrom(model.$.b, model.$.a);
|
|
71
|
+
|
|
72
|
+
// ✅ syncFields умеет двустороннюю связь без петель
|
|
73
|
+
syncFields(model.$.a, model.$.b);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// ❌ Ожидание, что transform применится в обе стороны
|
|
78
|
+
syncFields(model.$.a, model.$.b, { transform: (v) => v.trim() });
|
|
79
|
+
// при записи в b значение НЕ trim-ается
|
|
80
|
+
|
|
81
|
+
// ✅ Симметричные трансформы — syncFields + transformValue на обоих полях
|
|
82
|
+
syncFields(model.$.a, model.$.b);
|
|
83
|
+
transformValue(model.$.a, (v) => (typeof v === 'string' ? v.trim() : v));
|
|
84
|
+
transformValue(model.$.b, (v) => (typeof v === 'string' ? v.trim() : v));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// ❌ Поля разного типа — рантайм-приведение и баги
|
|
89
|
+
syncFields(model.$.amountString, model.$.amountNumber); // string ↔ number
|
|
90
|
+
|
|
91
|
+
// ✅ Для конвертации — compute в обе стороны или один канонический формат + computed отображение
|
|
92
|
+
compute(model.$.amountNumber, () => Number(model.amountString));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Troubleshooting
|
|
96
|
+
|
|
97
|
+
**Q: Поля «дёргаются», несколько перезаписей.**
|
|
98
|
+
A: Чаще всего на одном из полей висит `transformValue`/`compute`. Убедитесь, что transform
|
|
99
|
+
идемпотентен (`f(f(x)) === f(x)`).
|
|
100
|
+
|
|
101
|
+
**Q: «Cycle detected» при `syncFields`.**
|
|
102
|
+
A: Не вешайте дополнительно `onChange`/`watchField`, которые сами пишут в эти же поля.
|
|
103
|
+
`syncFields` уже занимает оба направления.
|
|
104
|
+
|
|
105
|
+
**Q: Как ограничить sync условием (как `when` у copyFrom)?**
|
|
106
|
+
A: У `syncFields` нет `when`. Эмулируй через два `copyFrom(a→b, { when })` / `copyFrom(b→a, { when })`
|
|
107
|
+
с флагами, разрешающими только одну активную сторону, либо через `apply` под условием.
|
|
108
|
+
|
|
109
|
+
## See also
|
|
110
|
+
|
|
111
|
+
- [23-copy-from.md](./23-copy-from.md) — однонаправленное копирование с `when`
|
|
112
|
+
- [26-transform-value.md](./26-transform-value.md) — нормализация значений на месте
|
|
113
|
+
- [22-cycle-detection.md](./22-cycle-detection.md) — почему симметричный copy ломается
|
|
114
|
+
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# resetWhen — Условный сброс полей
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это
|
|
6
|
+
альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но
|
|
7
|
+
содержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но
|
|
8
|
+
поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение.
|
|
9
|
+
|
|
10
|
+
## API
|
|
11
|
+
|
|
12
|
+
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// примитив: возвращает cleanup
|
|
16
|
+
function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): () => void;
|
|
17
|
+
|
|
18
|
+
// DSL: cleanup управляется формой
|
|
19
|
+
function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): void;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`target` — сигнал (`model.$.field`). `condition` — реактивное условие (читает `model.*`).
|
|
23
|
+
`resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
|
|
24
|
+
поля не трогаются оператором (значения принадлежат модели).
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
### Базовый сценарий — сброс номера карты при смене способа оплаты
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { defineFormBehavior, resetWhen } from '@reformer/core/behaviors';
|
|
32
|
+
|
|
33
|
+
type CheckoutForm = { paymentType: 'card' | 'cash'; cardNumber: string };
|
|
34
|
+
|
|
35
|
+
export const checkoutBehavior = defineFormBehavior<CheckoutForm>(({ model }) => {
|
|
36
|
+
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Пример: `resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' })`.
|
|
41
|
+
|
|
42
|
+
### resetValue для числовых полей
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { defineFormBehavior, resetWhen } from '@reformer/core/behaviors';
|
|
46
|
+
|
|
47
|
+
type MortgageForm = { propertyValue: number | null; initialPayment: number };
|
|
48
|
+
|
|
49
|
+
export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) => {
|
|
50
|
+
// initialPayment теряет смысл без propertyValue — сбрасываем в 0
|
|
51
|
+
resetWhen(model.$.initialPayment, () => !model.propertyValue, { resetValue: 0 });
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Как примитив (вне defineFormBehavior)
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { resetWhen } from '@reformer/core';
|
|
59
|
+
const stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Anti-patterns
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// ❌ resetWhen вместо enableWhen для disable-сценария
|
|
66
|
+
resetWhen(model.$.field, () => !model.show);
|
|
67
|
+
// поле останется enabled и валидируемым — ошибка required всё равно прилетит
|
|
68
|
+
|
|
69
|
+
// ✅ Если поле должно «исчезнуть» — блокируем + сбрасываем
|
|
70
|
+
enableWhen(model.$.field, () => model.show, { resetOnDisable: true });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// ❌ resetValue с типом, отличным от поля
|
|
75
|
+
resetWhen(model.$.amount, () => model.skipPayment, { resetValue: 'none' }); // amount: number ← string
|
|
76
|
+
|
|
77
|
+
// ✅ resetValue совместим с типом поля
|
|
78
|
+
resetWhen(model.$.amount, () => model.skipPayment, { resetValue: 0 });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// ❌ Для строкового поля без resetValue прилетит null, а Input ждёт string
|
|
83
|
+
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card');
|
|
84
|
+
|
|
85
|
+
// ✅ Явный resetValue для строк
|
|
86
|
+
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// ❌ condition читает саму цель — самотриггер (см. 22-cycle-detection.md)
|
|
91
|
+
resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
|
|
92
|
+
|
|
93
|
+
// ✅ condition зависит только от независимого поля
|
|
94
|
+
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Troubleshooting
|
|
98
|
+
|
|
99
|
+
**Q: Сброс не срабатывает, хотя `condition` возвращает true.**
|
|
100
|
+
A: Проверьте, что behavior зарегистрирован (`createForm({ behavior })`) или примитив не отписан,
|
|
101
|
+
и что `condition` читает реактивные поля (`model.*`), а не снимок `model.get()`.
|
|
102
|
+
|
|
103
|
+
**Q: Реактивная цепочка resetWhen → compute → resetWhen ломается.**
|
|
104
|
+
A: Убедитесь, что `condition` не зависит от значения самого поля, иначе после сброса попадёте
|
|
105
|
+
в новый триггер.
|
|
106
|
+
|
|
107
|
+
**Q: Сбросить вложенную группу целиком?**
|
|
108
|
+
A: `resetWhen` рассчитан на скалярные сигналы. Для группы используй `enableWhen({ resetOnDisable: true })`
|
|
109
|
+
(проходит по поддереву) или `model.reset()` для сброса всей формы к initial-снимку.
|
|
110
|
+
|
|
111
|
+
## See also
|
|
112
|
+
|
|
113
|
+
- [04-common-patterns.md](./04-common-patterns.md) — `enableWhen({ resetOnDisable: true })` как альтернатива
|
|
114
|
+
- [23-copy-from.md](./23-copy-from.md) — копирование, у которого нет встроенного отката
|
|
115
|
+
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `condition` не должен читать целевое поле
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# transformValue — Автоматическая трансформация значений
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`transformValue` подписывается на изменение поля и переписывает его трансформированной
|
|
6
|
+
версией: uppercase для кодов, trim+toLowerCase для email, округление для чисел. Применяется
|
|
7
|
+
единообразно ко всем источникам изменения (пользователь, `model.field = …`, `model.set/patch`,
|
|
8
|
+
`copyFrom`). **Идемпотентность (`f(f(x)) === f(x)`) обязательна** — иначе бесконечный цикл.
|
|
9
|
+
Оператор откладывает запись вне effect-контекста (`runOutsideEffect`) и не пишет, если
|
|
10
|
+
`transformer(value) === value` — базовый guard от циклов.
|
|
11
|
+
|
|
12
|
+
## API
|
|
13
|
+
|
|
14
|
+
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// примитив: возвращает cleanup
|
|
18
|
+
function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): () => void;
|
|
19
|
+
|
|
20
|
+
// DSL: cleanup управляется формой
|
|
21
|
+
function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): void;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`target` — сигнал (`model.$.field`). `transformer` — чистая идемпотентная функция значения.
|
|
25
|
+
Дополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора
|
|
26
|
+
`transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
### Базовый сценарий — uppercase для кода
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { defineFormBehavior, transformValue } from '@reformer/core/behaviors';
|
|
34
|
+
|
|
35
|
+
type PromoForm = { uppercaseField: string };
|
|
36
|
+
|
|
37
|
+
export const promoBehavior = defineFormBehavior<PromoForm>(({ model }) => {
|
|
38
|
+
transformValue(model.$.uppercaseField, (value) => (value ?? '').toUpperCase());
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Пример: `transformValue(model.$.uppercaseField, (v) => (v ?? '').toUpperCase())`.
|
|
43
|
+
|
|
44
|
+
### Несколько трансформаций
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { defineFormBehavior, transformValue } from '@reformer/core/behaviors';
|
|
48
|
+
|
|
49
|
+
type ContactForm = { email: string; phone: string; amount: number };
|
|
50
|
+
|
|
51
|
+
export const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {
|
|
52
|
+
// email: trim + lowercase
|
|
53
|
+
transformValue(model.$.email, (value) => (value ?? '').trim().toLowerCase());
|
|
54
|
+
|
|
55
|
+
// телефон: только цифры → формат
|
|
56
|
+
transformValue(model.$.phone, (value) => {
|
|
57
|
+
if (!value) return value;
|
|
58
|
+
const digits = value.replace(/\D/g, '');
|
|
59
|
+
if (digits.length === 11) {
|
|
60
|
+
return `+7 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7, 9)}-${digits.slice(9)}`;
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// округление до целого
|
|
66
|
+
transformValue(model.$.amount, (value) => (typeof value === 'number' ? Math.round(value) : value));
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Как примитив (вне defineFormBehavior)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { transformValue } from '@reformer/core';
|
|
74
|
+
const stop = transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Переиспользуемые трансформеры — обычные функции
|
|
78
|
+
|
|
79
|
+
Готового набора нет, но легко собрать свои и применять их к любому сигналу:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const toUpper = (target: Signal<string>) => transformValue(target, (v) => (v ?? '').toUpperCase());
|
|
83
|
+
const trim = (target: Signal<string>) => transformValue(target, (v) => (v ?? '').trim());
|
|
84
|
+
|
|
85
|
+
// в схеме поведения:
|
|
86
|
+
toUpper(model.$.promoCode);
|
|
87
|
+
trim(model.$.username);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Anti-patterns
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// ❌ Неидемпотентный transformer — бесконечный цикл
|
|
94
|
+
transformValue(model.$.field, (v) => `prefix-${v}`); // f(f(x)) ≠ f(x)
|
|
95
|
+
|
|
96
|
+
// ✅ Guard внутри transformer
|
|
97
|
+
transformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// ❌ transformValue ОДНОВРЕМЕННО с syncFields на том же поле
|
|
102
|
+
syncFields(model.$.a, model.$.b);
|
|
103
|
+
transformValue(model.$.b, (v) => v.toUpperCase()); // взаимные перезаписи
|
|
104
|
+
|
|
105
|
+
// ✅ Трансформируй источник ДО синхронизации
|
|
106
|
+
transformValue(model.$.a, (v) => v.toUpperCase());
|
|
107
|
+
syncFields(model.$.a, model.$.b);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// ❌ transformValue для производных полей (нет доступа к другим полям)
|
|
112
|
+
transformValue(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
|
|
113
|
+
|
|
114
|
+
// ✅ Для зависимостей от других полей — compute
|
|
115
|
+
compute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Troubleshooting
|
|
119
|
+
|
|
120
|
+
**Q: «Cycle detected» при transformValue.**
|
|
121
|
+
A: 99% — неидемпотентный transformer. Проверь `transformer(transformer(x)) === transformer(x)`.
|
|
122
|
+
|
|
123
|
+
**Q: Трансформация не применяется.**
|
|
124
|
+
A: Проверь, что behavior зарегистрирован (`createForm({ behavior })`) / примитив не отписан,
|
|
125
|
+
и что форма не пересоздаётся на каждый рендер (используй `useMemo`).
|
|
126
|
+
|
|
127
|
+
**Q: Каретка input прыгает при наборе.**
|
|
128
|
+
A: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)
|
|
129
|
+
лучше делать через `<InputMask>` из `@reformer/ui-kit`, а не `transformValue`.
|
|
130
|
+
|
|
131
|
+
## See also
|
|
132
|
+
|
|
133
|
+
- [24-sync-fields.md](./24-sync-fields.md) — порядок применения с syncFields
|
|
134
|
+
- [23-copy-from.md](./23-copy-from.md) — `transform`-опция при копировании
|
|
135
|
+
- [22-cycle-detection.md](./22-cycle-detection.md) — про идемпотентность
|
|
136
|
+
- [16-ui-components.md](./16-ui-components.md) — `<InputMask>` для тяжёлого форматирования
|