@reformer/core 10.0.0-beta.3 → 10.0.0-beta.4
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/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/index.d.ts +1 -1
- package/dist/index.js +429 -617
- package/dist/runtime-token.d.ts +23 -0
- 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 +59 -0
- package/docs/llms/20-compute-vs-watch.md +138 -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 +144 -1557
- package/package.json +33 -10
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
## Import Patterns
|
|
2
|
+
|
|
3
|
+
```typescript
|
|
4
|
+
// Модель, форма, хуки, типы, примитивы behaviors — из @reformer/core
|
|
5
|
+
import {
|
|
6
|
+
// фабрики
|
|
7
|
+
createModel,
|
|
8
|
+
createForm,
|
|
9
|
+
// хуки
|
|
10
|
+
useFormControl,
|
|
11
|
+
useFormControlValue,
|
|
12
|
+
useArrayLength,
|
|
13
|
+
// примитивы behaviors (принимают сигналы, возвращают cleanup)
|
|
14
|
+
computeFrom,
|
|
15
|
+
copyFrom,
|
|
16
|
+
watchField,
|
|
17
|
+
enableWhen,
|
|
18
|
+
disableWhen,
|
|
19
|
+
transformValue,
|
|
20
|
+
resetWhen,
|
|
21
|
+
syncFields,
|
|
22
|
+
revalidateWhen,
|
|
23
|
+
} from '@reformer/core';
|
|
24
|
+
|
|
25
|
+
// Типы
|
|
26
|
+
import type {
|
|
27
|
+
FormModel, // реактивная модель данных
|
|
28
|
+
FormProxy, // тип формы для props компонентов
|
|
29
|
+
FieldNode, // узел одного поля
|
|
30
|
+
GroupNode, // узел группы
|
|
31
|
+
ArrayNode, // узел массива
|
|
32
|
+
ModelArray, // реактивный массив модели (push/removeAt/at/map/length)
|
|
33
|
+
ModelSignals, // дерево сигналов ($)
|
|
34
|
+
PathAwareSignal, // сигнал, знающий свой путь
|
|
35
|
+
ValidationError,
|
|
36
|
+
FieldConfig, // { value, component, componentProps?, ... } — layout, БЕЗ валидаторов
|
|
37
|
+
FormSchema,
|
|
38
|
+
FieldControlState,
|
|
39
|
+
} from '@reformer/core';
|
|
40
|
+
|
|
41
|
+
// Схема валидации: операторы + раннер — из /validation
|
|
42
|
+
import {
|
|
43
|
+
defineValidationSchema,
|
|
44
|
+
validate,
|
|
45
|
+
validateAsync,
|
|
46
|
+
validateWhen,
|
|
47
|
+
cross,
|
|
48
|
+
each,
|
|
49
|
+
apply,
|
|
50
|
+
validateModel, // раннер: validateModel(model, schema) => Promise<boolean>
|
|
51
|
+
} from '@reformer/core/validation';
|
|
52
|
+
import type {
|
|
53
|
+
Rule, // (value) => ValidationError | null
|
|
54
|
+
AsyncRule, // (value, { signal }) => Promise<ValidationError | null>
|
|
55
|
+
ValidationSchema,
|
|
56
|
+
} from '@reformer/core/validation';
|
|
57
|
+
|
|
58
|
+
// Валидаторы — чистые фабрики из /validators (кладутся в validate(sig, [...]))
|
|
59
|
+
import { required, min, max, email, minLength, pattern } from '@reformer/core/validators';
|
|
60
|
+
|
|
61
|
+
// Декларативный DSL behaviors — из /behaviors
|
|
62
|
+
import {
|
|
63
|
+
defineFormBehavior,
|
|
64
|
+
compute,
|
|
65
|
+
computeFrom,
|
|
66
|
+
copyFrom,
|
|
67
|
+
onChange,
|
|
68
|
+
enableWhen,
|
|
69
|
+
disableWhen,
|
|
70
|
+
transformValue,
|
|
71
|
+
resetWhen,
|
|
72
|
+
syncFields,
|
|
73
|
+
revalidateWhen,
|
|
74
|
+
apply,
|
|
75
|
+
applyEach,
|
|
76
|
+
aggregateInto,
|
|
77
|
+
exclusiveFlag,
|
|
78
|
+
} from '@reformer/core/behaviors';
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Form-shape тип должен быть `type`, а не `interface`
|
|
82
|
+
|
|
83
|
+
Прокси `createForm<T>` и типы `ArrayNode<U>` / `GroupNode<U>` требуют, чтобы form-shape
|
|
84
|
+
структурно совпадал с `Record<string, FormValue>`. У `interface` нет неявной index signature,
|
|
85
|
+
поэтому объявляй form-shape (и типы элементов массива, и вложенные группы) через `type`-alias:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
export type AddressForm = {
|
|
89
|
+
street: string;
|
|
90
|
+
city: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type CoBorrower = {
|
|
94
|
+
fullName: string;
|
|
95
|
+
phone: string;
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
См. `30-type-safety-recipes.md`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
## 7. FORM TYPE DEFINITION
|
|
2
|
+
|
|
3
|
+
Form-shape объявляй через `type`-alias (не `interface` — см. `30-type-safety-recipes.md`).
|
|
4
|
+
Тип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без узлов/сигналов.
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
// CORRECT form type definition
|
|
8
|
+
type MyForm = {
|
|
9
|
+
// Required fields
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
|
|
13
|
+
// Optional fields — конвенция для форм: null («пользователь очистил»)
|
|
14
|
+
phone: string | null;
|
|
15
|
+
age: number | null;
|
|
16
|
+
|
|
17
|
+
// Enum/union types
|
|
18
|
+
status: 'active' | 'inactive';
|
|
19
|
+
|
|
20
|
+
// Nested objects
|
|
21
|
+
address: {
|
|
22
|
+
street: string;
|
|
23
|
+
city: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Arrays of objects — model-owned (см. 10-arrays.md)
|
|
27
|
+
items: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Модель создаётся из initial-значений этого типа
|
|
34
|
+
const model = createModel<MyForm>({
|
|
35
|
+
name: '',
|
|
36
|
+
email: '',
|
|
37
|
+
phone: null,
|
|
38
|
+
age: null,
|
|
39
|
+
status: 'active',
|
|
40
|
+
address: { street: '', city: '' },
|
|
41
|
+
items: [],
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`number | null` / `string | null` работают со встроенными валидаторами напрямую —
|
|
46
|
+
`min`/`max`/`minLength`/`minDate`/`minAge` пропускают пустые значения внутри, guard `if (v != null)`
|
|
47
|
+
не нужен.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
## 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)
|
|
2
|
+
|
|
3
|
+
Под M1 схема **привязывает поле к сигналу модели** (`value: model.$.field`) и держит
|
|
4
|
+
UI-конфиг (`component`/`componentProps`). Значения принадлежат модели. Валидаторов в
|
|
5
|
+
layout-схеме НЕТ — правила живут в отдельной схеме `defineValidationSchema` из
|
|
6
|
+
`@reformer/core/validation` и запускаются раннером `validateModel(model, schema)`.
|
|
7
|
+
|
|
8
|
+
### Field node
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
{
|
|
12
|
+
value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
|
|
13
|
+
component: Input, // React-компонент
|
|
14
|
+
componentProps?: object, // пропсы (label, placeholder, options, type, ...)
|
|
15
|
+
disabled?: boolean,
|
|
16
|
+
updateOn?: 'change' | 'blur' | 'submit',
|
|
17
|
+
debounce?: number,
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> `disabled` — **top-level поле узла** (начальное состояние); дальше управляется методами
|
|
22
|
+
> `form.field.disable()`/`enable()` и оператором `enableWhen`. `componentProps.disabled` — это
|
|
23
|
+
> UI-проп и на состояние узла **не влияет** (частая причина «computed-поле остаётся редактируемым»).
|
|
24
|
+
|
|
25
|
+
### Primitive Fields
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createModel, createForm } from '@reformer/core';
|
|
29
|
+
import { defineValidationSchema, validate } from '@reformer/core/validation';
|
|
30
|
+
import { required } from '@reformer/core/validators';
|
|
31
|
+
import { Input, Select, Checkbox } from '@reformer/ui-kit';
|
|
32
|
+
|
|
33
|
+
const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
|
|
34
|
+
|
|
35
|
+
// Валидация — отдельным слоем (запуск: await validateModel(model, myValidation))
|
|
36
|
+
const myValidation = defineValidationSchema<MyForm>(({ model }) => {
|
|
37
|
+
validate(model.$.name, [required()]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const schema = {
|
|
41
|
+
name: {
|
|
42
|
+
value: model.$.name,
|
|
43
|
+
component: Input,
|
|
44
|
+
componentProps: { label: 'Name', placeholder: 'Enter name' },
|
|
45
|
+
},
|
|
46
|
+
age: {
|
|
47
|
+
value: model.$.age,
|
|
48
|
+
component: Input,
|
|
49
|
+
componentProps: { type: 'number', label: 'Age' },
|
|
50
|
+
},
|
|
51
|
+
agree: {
|
|
52
|
+
value: model.$.agree,
|
|
53
|
+
component: Checkbox,
|
|
54
|
+
componentProps: { label: 'I agree to terms' },
|
|
55
|
+
},
|
|
56
|
+
status: {
|
|
57
|
+
value: model.$.status,
|
|
58
|
+
component: Select,
|
|
59
|
+
componentProps: {
|
|
60
|
+
label: 'Status',
|
|
61
|
+
options: [
|
|
62
|
+
{ value: 'active', label: 'Active' },
|
|
63
|
+
{ value: 'inactive', label: 'Inactive' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const form = createForm<MyForm>({ model, schema });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Nested Objects
|
|
73
|
+
|
|
74
|
+
Вложенная группа модели — это сама по себе под-модель `FormModel<Sub>` (доступна как `model.<group>`,
|
|
75
|
+
с `.$`/API). Удобно вынести в builder, принимающий `FormModel<Sub>` — симметрично builder'у элемента
|
|
76
|
+
массива. Сигналы под-модели идентичны корневым: `model.address.$.city === model.$.address.city`.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import type { FormModel } from '@reformer/core';
|
|
80
|
+
|
|
81
|
+
const addressNodes = (m: FormModel<Address>) => ({
|
|
82
|
+
street: { value: m.$.street, component: Input, componentProps: { label: 'Street' } },
|
|
83
|
+
city: { value: m.$.city, component: Input, componentProps: { label: 'City' } },
|
|
84
|
+
zip: { value: m.$.zip, component: Input, componentProps: { label: 'ZIP' } },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const schema = {
|
|
88
|
+
address: addressNodes(model.address),
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Arrays — `{ array, item }` node
|
|
93
|
+
|
|
94
|
+
Массив объектов объявляется узлом `{ array: model.<path>, item: (itemModel) => subSchema }`.
|
|
95
|
+
`item` строит под-схему из под-модели элемента (`FormModel<Item>`):
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import type { FormModel } from '@reformer/core';
|
|
99
|
+
|
|
100
|
+
const itemSchema = (item: FormModel<Item>) => ({
|
|
101
|
+
id: { value: item.$.id, component: Input, componentProps: { label: 'ID' } },
|
|
102
|
+
name: { value: item.$.name, component: Input, componentProps: { label: 'Name' } },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const schema = {
|
|
106
|
+
items: { array: model.items, item: itemSchema },
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### createForm API
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// M1: данные из модели + схема (+ опциональный декларативный behavior)
|
|
114
|
+
const form = createForm<MyForm>({
|
|
115
|
+
model, // FormModel<MyForm> — обязателен
|
|
116
|
+
schema, // дерево узлов, привязанных к сигналам
|
|
117
|
+
behavior: myBehavior, // опционально: defineFormBehavior(...) из @reformer/core/behaviors
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Доступ к нодам через Proxy
|
|
121
|
+
form.name.setValue('John');
|
|
122
|
+
form.address.city.value.value; // текущее значение (через сигнал)
|
|
123
|
+
model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> **Тип `schema` в M1 — `unknown`, не `FormSchema<T>`.** Это осознанно: интерпретатор обходит
|
|
127
|
+
> произвольную структуру и собирает листья `{ value: signal, component?, ... }`, поэтому обёртки
|
|
128
|
+
> (`{ children: [...] }`, wizard/steps) компилируются. Строгий `FormSchema<T>` (keyed-map по полям
|
|
129
|
+
> `T`) применяется только на legacy-пути `createForm(schema)` без модели.
|
|
130
|
+
|
|
131
|
+
### createForm Returns a Proxy
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const form = createForm<MyForm>({ model, schema });
|
|
135
|
+
|
|
136
|
+
form.email; // FieldNode<string> — TypeScript знает тип
|
|
137
|
+
form.address.city; // FieldNode<string> — вложенный доступ
|
|
138
|
+
form.items.at(0); // FormProxy<ItemType> — элемент массива
|
|
139
|
+
|
|
140
|
+
// IMPORTANT: Proxy не проходит instanceof! Используй type guards:
|
|
141
|
+
import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
|
|
142
|
+
if (isFieldNode(node)) { /* ... */ }
|
|
143
|
+
```
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
## 9. ARRAY SCHEMA FORMAT
|
|
2
|
+
|
|
3
|
+
Массивы объектов — **model-owned**: данные принадлежат модели (`model.arrayField` — это
|
|
4
|
+
`ModelArray<Item>` с реактивными `push`/`removeAt`/`length`). В схеме массив объявляется узлом
|
|
5
|
+
`{ array: model.<path>, item: (itemModel) => subSchema }`, где `item` строит под-схему одного
|
|
6
|
+
элемента из его под-модели (`FormModel<Item>`).
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import { createModel, createForm, type FormModel } from '@reformer/core';
|
|
10
|
+
import { Input } from '@reformer/ui-kit';
|
|
11
|
+
|
|
12
|
+
type Item = { id: string; name: string; price: number };
|
|
13
|
+
type MyForm = { items: Item[] };
|
|
14
|
+
|
|
15
|
+
const model = createModel<MyForm>({ items: [] });
|
|
16
|
+
|
|
17
|
+
// под-схема одного элемента (item.$.field — сигнал под-модели)
|
|
18
|
+
const itemSchema = (item: FormModel<Item>) => ({
|
|
19
|
+
id: { value: item.$.id, component: Input },
|
|
20
|
+
name: { value: item.$.name, component: Input },
|
|
21
|
+
price: { value: item.$.price, component: Input, componentProps: { type: 'number' } },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const schema = {
|
|
25
|
+
items: { array: model.items, item: itemSchema },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const form = createForm<MyForm>({ model, schema });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> **Type constraint:** тип элемента `Item` объявляй через `type`-alias (не `interface`) — иначе
|
|
32
|
+
> он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
|
|
33
|
+
> См. `30-type-safety-recipes.md`.
|
|
34
|
+
|
|
35
|
+
### Один массив — три слоя (три разных движка)
|
|
36
|
+
|
|
37
|
+
Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
|
|
38
|
+
перепутать, но каждая корректна только в своём контексте:
|
|
39
|
+
|
|
40
|
+
1. **Layout-схема `createForm`** — единственная форма, которую ест `createForm`:
|
|
41
|
+
`{ array: model.<path>, item: (itemModel) => subSchema }`. Массив связывается через
|
|
42
|
+
**value-proxy** `model.properties` (он несёт `__path`), а **не** через сигнальный
|
|
43
|
+
`model.$.properties`.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// узел схемы для createForm({ model, schema })
|
|
47
|
+
properties: { array: model.properties, item: propertyItem },
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. **Validation-схема (`@reformer/core/validation`)** — per-item правила пишутся оператором
|
|
51
|
+
`each(arr, (im) => {...})` внутри `defineValidationSchema`; `im` — под-модель элемента
|
|
52
|
+
(`im.$.field` — его сигналы). Запуск — внешним `validateModel(model, schema)`.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// внутри defineValidationSchema<MyForm>(({ model }) => { ... })
|
|
56
|
+
each(model.properties, (im) => {
|
|
57
|
+
validate(im.$.description, [required()]);
|
|
58
|
+
validate(im.$.estimatedValue, [required(), min(1)]);
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`ModelArrayNode`),
|
|
63
|
+
а не со схемой: `<FormArray.Root control={form.properties}>` (CDK) или `FormArraySection` из
|
|
64
|
+
`@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).
|
|
65
|
+
|
|
66
|
+
> **Не путай форму по движку.** `createForm` принимает **только** `{ array, item }`;
|
|
67
|
+
> per-item валидация — это `each(model.<array>, (im) => ...)` в validation-схеме; `FormArray.Root
|
|
68
|
+
> control={form.x}` (или `FormArraySection`) — рендер. `each` в layout-схеме `createForm`
|
|
69
|
+
> не подхватится, а `{ array, item }` в validation-схеме не обходится.
|
|
70
|
+
|
|
71
|
+
### Array operations — на модели
|
|
72
|
+
|
|
73
|
+
Мутации массива делаются через `ModelArray` (`model.items`), а не через ноду формы:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
model.items.push({ id: '1', name: '', price: 0 }); // добавить в конец (плоские значения!)
|
|
77
|
+
model.items.insertAt(0, { id: '2', name: '', price: 0 });
|
|
78
|
+
model.items.removeAt(index);
|
|
79
|
+
model.items.move(from, to);
|
|
80
|
+
model.items.swap(a, b);
|
|
81
|
+
model.items.clear();
|
|
82
|
+
model.items.length; // реактивная длина
|
|
83
|
+
model.items.at(0); // под-модель элемента (FormModel<Item>)
|
|
84
|
+
model.items.map((item, i) => item.name); // item — FormModel<Item>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> **Плоские значения при push.** В `push`/`insertAt` передавай payload из **плоских значений**
|
|
88
|
+
> (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
|
|
89
|
+
> берутся из `item`-фабрики схемы автоматически.
|
|
90
|
+
|
|
91
|
+
> **Очистка массива в behavior.** Тот же `model.<array>.clear()` (см. список операций выше) —
|
|
92
|
+
> способ очистить коллекцию **вне React**, из behavior: он мутирует модель напрямую, минуя ноды
|
|
93
|
+
> формы. Типичный случай — сбросить массив при выключении флага:
|
|
94
|
+
>
|
|
95
|
+
> ```typescript
|
|
96
|
+
> onChange(model.$.hasProperty, (on) => {
|
|
97
|
+
> if (!on) model.properties.clear();
|
|
98
|
+
> });
|
|
99
|
+
> ```
|
|
100
|
+
|
|
101
|
+
### Rendering Arrays
|
|
102
|
+
|
|
103
|
+
Каждый элемент массива — под-форма (`FormProxy<Item>`). Итерируй через `form.items.map`:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { useArrayLength } from '@reformer/core';
|
|
107
|
+
|
|
108
|
+
function ItemsList({ form }: { form: FormProxy<MyForm> }) {
|
|
109
|
+
const length = useArrayLength(form.items);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div>
|
|
113
|
+
{form.items.map((item, index) => (
|
|
114
|
+
<div key={index}>
|
|
115
|
+
<FormField control={item.name} />
|
|
116
|
+
<FormField control={item.price} />
|
|
117
|
+
<button onClick={() => model.items.removeAt(index)}>Remove</button>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
|
|
121
|
+
{length === 0 && <p>No items yet</p>}
|
|
122
|
+
|
|
123
|
+
<button onClick={() => model.items.push({ id: crypto.randomUUID(), name: '', price: 0 })}>
|
|
124
|
+
Add Item
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> В монорепо для массивов используется готовый `FormArraySection` из `@reformer/ui-kit`
|
|
132
|
+
> (`control={form.items}`, `itemComponent`, `initialValue`, add/remove/reorder из коробки).
|
|
133
|
+
> См. `find_recipe(topic="form-array")`.
|
|
134
|
+
|
|
135
|
+
### Array Cross-Validation
|
|
136
|
+
|
|
137
|
+
Whole-array правило пишется оператором `cross(sig, (f) => ...)`: `f` — снапшот `model.get()`
|
|
138
|
+
(плоские значения), ошибка вешается на поле-носитель `sig`. Per-item правила — `each`
|
|
139
|
+
(см. выше, оба — из `@reformer/core/validation`).
|
|
140
|
+
|
|
141
|
+
Носитель — любое **скалярное** поле формы (флаг `hasItems`, итоговая сумма и т.п.), у которого
|
|
142
|
+
есть сигнал `model.$.<field>` и нода в форме:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { cross } from '@reformer/core/validation';
|
|
146
|
+
|
|
147
|
+
type MyForm = { hasItems: boolean; items: Item[] };
|
|
148
|
+
|
|
149
|
+
// внутри defineValidationSchema<MyForm>(({ model }) => { ... })
|
|
150
|
+
cross(model.$.hasItems, (f: MyForm) => {
|
|
151
|
+
const names = f.items.map((i) => i.name);
|
|
152
|
+
return names.length !== new Set(names).size
|
|
153
|
+
? { code: 'duplicate', message: 'Item names must be unique' }
|
|
154
|
+
: null;
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## See also
|
|
159
|
+
|
|
160
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item валидация `each`
|
|
161
|
+
- CDK / ui-kit form-array (`FormArray.Root`, `FormArraySection`) — `find_recipe(topic="form-array")`
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
## 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)
|
|
2
|
+
|
|
3
|
+
Для async-реакции на изменение поля (загрузка зависимых опций, справочников) используй
|
|
4
|
+
`onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно
|
|
5
|
+
безопасно писать сигналы/ноды), а 2-й аргумент — `{ signal }` (AbortSignal): при следующей
|
|
6
|
+
смене значения предыдущий вызов аннулируется — передавай `signal` в `fetch`.
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
|
|
10
|
+
|
|
11
|
+
const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
|
|
12
|
+
// CORRECT — async onChange со всеми safeguards
|
|
13
|
+
onChange(
|
|
14
|
+
model.$.parentField,
|
|
15
|
+
async (value, { signal }) => {
|
|
16
|
+
if (!value) {
|
|
17
|
+
form.dependentField.updateComponentProps({ options: [] });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const data = await fetchData(value, { signal }); // отмена устаревших запросов
|
|
22
|
+
form.dependentField.updateComponentProps({ options: data });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if ((error as Error).name === 'AbortError') return;
|
|
25
|
+
form.dependentField.updateComponentProps({ options: [] });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{ debounce: 300 } // не fetch на каждое нажатие
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Опции onChange
|
|
34
|
+
|
|
35
|
+
- `debounce: 300` — не дёргать сеть на каждый keystroke (300–500 мс рекомендуется).
|
|
36
|
+
- `immediate: true` — вызвать колбэк сразу при регистрации (по умолчанию `false`).
|
|
37
|
+
- Guard-clause — пропустить пустое значение.
|
|
38
|
+
- try/catch + проверка `AbortError` — обработка отмены и ошибок.
|
|
39
|
+
|
|
40
|
+
### Низкоуровневый примитив watchField
|
|
41
|
+
|
|
42
|
+
`watchField` из `@reformer/core` — базовая подписка на изменение сигнала (без debounce и
|
|
43
|
+
AbortSignal). `onChange` построен поверх него. Для простых синхронных реакций:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { watchField } from '@reformer/core';
|
|
47
|
+
|
|
48
|
+
// вызывается при каждом изменении (по умолчанию НЕ на инициализации)
|
|
49
|
+
const stop = watchField(model.$.country, (country) => {
|
|
50
|
+
model.city = ''; // сброс зависимого поля
|
|
51
|
+
});
|
|
52
|
+
// stop() — отписаться
|
|
53
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
## 11. ARRAY CLEANUP PATTERN
|
|
2
|
+
|
|
3
|
+
Очистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на
|
|
4
|
+
модели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
|
|
8
|
+
|
|
9
|
+
const behavior = defineFormBehavior<MyForm>(({ model }) => {
|
|
10
|
+
// при снятии флага — очистить массив
|
|
11
|
+
onChange(model.$.hasItems, (hasItems) => {
|
|
12
|
+
if (!hasItems) model.items.clear();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Переиспользуемый оператор (как в монорепо):
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { onChange } from '@reformer/core/behaviors';
|
|
21
|
+
import type { ReadonlySignal } from '@reformer/core/behaviors';
|
|
22
|
+
|
|
23
|
+
function clearWhenOff(flag: ReadonlySignal<boolean>, array: { clear(): void }): void {
|
|
24
|
+
onChange(flag, (on) => {
|
|
25
|
+
if (!on) array.clear();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// в схеме поведения:
|
|
30
|
+
clearWhenOff(model.$.hasProperty, model.properties);
|
|
31
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
## 12. MULTI-STEP FORM VALIDATION
|
|
2
|
+
|
|
3
|
+
Каждый шаг — своя `ValidationSchema<Form>` (обычная функция `({ model }) => void`, обёрнутая
|
|
4
|
+
`defineValidationSchema`). Переход к следующему шагу проверяется `validateModel(model, stepSchema)`;
|
|
5
|
+
полный submit — по общей схеме (композиция шагов через `apply(...)`). `validateModel` сам разносит
|
|
6
|
+
ошибки по нодам формы (`getNodeForSignal(sig).setErrors(...)`), поэтому UI подсветит проблемные поля
|
|
7
|
+
текущего шага автоматически. Валидация живёт ОТДЕЛЬНО от layout: сам `RenderNode`/JSON-узел валидаторов
|
|
8
|
+
не несёт — схема инъектируется в рантайме и роутит ошибки по тем же нодам.
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { type FormModel } from '@reformer/core';
|
|
12
|
+
import {
|
|
13
|
+
validate,
|
|
14
|
+
apply,
|
|
15
|
+
defineValidationSchema,
|
|
16
|
+
validateModel,
|
|
17
|
+
type ValidationSchema,
|
|
18
|
+
} from '@reformer/core/validation';
|
|
19
|
+
import { required, min } from '@reformer/core/validators';
|
|
20
|
+
|
|
21
|
+
// Под-схема шага — обычная функция ({ model }) => void. Значения проверяет оператор validate(sig, [rules]).
|
|
22
|
+
const step1Schema = defineValidationSchema<Form>(({ model }) => {
|
|
23
|
+
validate(model.$.loanType, [required()]);
|
|
24
|
+
validate(model.$.loanAmount, [required(), min(50000)]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const step2Schema = defineValidationSchema<Form>(({ model }) => {
|
|
28
|
+
validate(model.$.personalData.firstName, [required()]);
|
|
29
|
+
validate(model.$.personalData.lastName, [required()]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Карта шагов + полная схема (композиция под-схем через apply — заменяет пошаговую группировку деревом)
|
|
33
|
+
const STEP_SCHEMAS: readonly ValidationSchema<Form>[] = [step1Schema, step2Schema];
|
|
34
|
+
const fullSchema = defineValidationSchema<Form>(() => apply(...STEP_SCHEMAS));
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// Переход к следующему шагу. validateModel возвращает Promise<boolean> (true = нет блокирующих ошибок;
|
|
39
|
+
// severity:'warning' не блокирует). Устаревшие прогоны той же (model, schema) отменяются автоматически.
|
|
40
|
+
const goToNextStep = async () => {
|
|
41
|
+
const ok = await validateModel(model, STEP_SCHEMAS[currentStep - 1]);
|
|
42
|
+
if (!ok) return; // ошибки уже проставлены в ноды текущего шага
|
|
43
|
+
setCurrentStep(currentStep + 1);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Полный submit — по общей схеме
|
|
47
|
+
const handleSubmit = async () => {
|
|
48
|
+
const ok = await validateModel(model, fullSchema);
|
|
49
|
+
if (ok) {
|
|
50
|
+
await onSubmit(model.get());
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Слой-потребитель (`FormWizard`) обычно оборачивает это в конфиг с per-step и полной валидацией:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
function makeValidationConfig(model: FormModel<Form>) {
|
|
59
|
+
return {
|
|
60
|
+
validateStep: (n: number): Promise<boolean> => validateModel(model, STEP_SCHEMAS[n - 1]),
|
|
61
|
+
validateAll: (): Promise<boolean> => validateModel(model, fullSchema),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Multi-Step Component Example
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
function MultiStepForm() {
|
|
70
|
+
const [step, setStep] = useState(1);
|
|
71
|
+
|
|
72
|
+
const nextStep = async () => {
|
|
73
|
+
const ok = await validateModel(model, STEP_SCHEMAS[step - 1]);
|
|
74
|
+
if (ok) setStep(step + 1);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
{step === 1 && <Step1Fields form={form} />}
|
|
80
|
+
{step === 2 && <Step2Fields form={form} />}
|
|
81
|
+
|
|
82
|
+
<button onClick={() => setStep(step - 1)} disabled={step === 1}>
|
|
83
|
+
Back
|
|
84
|
+
</button>
|
|
85
|
+
<button onClick={step === 2 ? handleSubmit : nextStep}>
|
|
86
|
+
{step === 2 ? 'Submit' : 'Next'}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|