@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,167 @@
|
|
|
1
|
+
## 4. COMMON MISTAKES
|
|
2
|
+
|
|
3
|
+
### Imports rule (#1 cause of cascading errors — read first)
|
|
4
|
+
|
|
5
|
+
- Модель/форма/хуки/типы (`FormModel`, `ValidationError`)/**примитивы behaviors** — из `@reformer/core`.
|
|
6
|
+
- Схема валидации (`defineValidationSchema` + операторы `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`) и раннер `validateModel` — из `@reformer/core/validation`.
|
|
7
|
+
- Чистые фабрики валидаторов (`required`/`min`/`email`/…) — из `@reformer/core/validators`.
|
|
8
|
+
- Декларативный DSL поведения (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// ✅ CORRECT
|
|
12
|
+
import {
|
|
13
|
+
createModel,
|
|
14
|
+
createForm,
|
|
15
|
+
useFormControl,
|
|
16
|
+
useFormControlValue,
|
|
17
|
+
type FormProxy,
|
|
18
|
+
type FieldConfig,
|
|
19
|
+
type FormModel,
|
|
20
|
+
type ValidationError,
|
|
21
|
+
} from '@reformer/core';
|
|
22
|
+
// схема валидации + раннер — отдельный слой:
|
|
23
|
+
import {
|
|
24
|
+
defineValidationSchema,
|
|
25
|
+
validate,
|
|
26
|
+
validateAsync,
|
|
27
|
+
validateWhen,
|
|
28
|
+
cross,
|
|
29
|
+
each,
|
|
30
|
+
apply,
|
|
31
|
+
validateModel,
|
|
32
|
+
type Rule,
|
|
33
|
+
type AsyncRule,
|
|
34
|
+
type ValidationSchema,
|
|
35
|
+
} from '@reformer/core/validation';
|
|
36
|
+
import { required, min, max, email } from '@reformer/core/validators';
|
|
37
|
+
// либо примитивы behaviors из основного пакета:
|
|
38
|
+
import { computeFrom, enableWhen, copyFrom } from '@reformer/core';
|
|
39
|
+
// либо декларативный DSL поведения:
|
|
40
|
+
import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> **watchField живёт в `@reformer/core`** (низкоуровневый примитив), НЕ в `@reformer/core/behaviors`.
|
|
44
|
+
> В DSL для реакции на изменения используется `onChange`.
|
|
45
|
+
|
|
46
|
+
### Значения — в модели, не в форме
|
|
47
|
+
|
|
48
|
+
Под M1 источник истины значений — модель. Форма (ноды) отражает их.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// ✅ читаем/пишем значение
|
|
52
|
+
model.email = 'a@b.c'; // value-доступ
|
|
53
|
+
model.$.email.value; // сигнал
|
|
54
|
+
model.get(); // весь снимок для submit
|
|
55
|
+
|
|
56
|
+
// В компоненте — реактивно через хуки
|
|
57
|
+
const email = useFormControlValue(form.email);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### useFormControlValue (CRITICAL)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// WRONG - useFormControlValue returns T directly, NOT { value: T }
|
|
64
|
+
const { value: loanType } = useFormControlValue(control.loanType);
|
|
65
|
+
// Result: loanType is ALWAYS undefined!
|
|
66
|
+
|
|
67
|
+
// CORRECT
|
|
68
|
+
const loanType = useFormControlValue(control.loanType);
|
|
69
|
+
|
|
70
|
+
// ALSO CORRECT - useFormControl returns object
|
|
71
|
+
const { value, errors } = useFormControl(control.loanType);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Behaviors принимают СИГНАЛЫ, а не пути/форму
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// ❌ WRONG - строковые пути и (form) => ... это старый API, удалён
|
|
78
|
+
enableWhen(path.city, (form) => Boolean(form.country));
|
|
79
|
+
computeFrom(['price', 'quantity'], 'total', (values) => values.price * values.quantity);
|
|
80
|
+
|
|
81
|
+
// ✅ CORRECT - сигналы модели, условие читает model напрямую
|
|
82
|
+
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
|
|
83
|
+
computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Валидаторы — оператор `validate`, а не layout-нода
|
|
87
|
+
|
|
88
|
+
Валидация — **отдельный слой** (`@reformer/core/validation`), а не поле layout-ноды. Layout
|
|
89
|
+
(схема `createForm` / JSON-DSL) больше **не несёт** `validators`: правила живут в `ValidationSchema`,
|
|
90
|
+
а прогоняет их внешний раннер `validateModel`. ⚠️ `form.validate()`/`submit()` schema-валидацию
|
|
91
|
+
**НЕ запускают** — прогон только через `validateModel(model, schema)`.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// ❌ WRONG - дерево { value, validators } и позиционная строка удалены
|
|
95
|
+
const schema = {
|
|
96
|
+
email: { value: model.$.email, component: Input, validators: [required(), email()] },
|
|
97
|
+
};
|
|
98
|
+
validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
|
|
99
|
+
|
|
100
|
+
// ✅ CORRECT - операторы validate/validateAsync внутри defineValidationSchema; фабрики с options
|
|
101
|
+
const emailSchema = defineValidationSchema<MyForm>(({ model }) => {
|
|
102
|
+
validate(model.$.email, [required({ message: 'Email is required' }), email()]);
|
|
103
|
+
validateAsync(model.$.email, [
|
|
104
|
+
async (value, { signal }) => {
|
|
105
|
+
const res = await fetch(`/api/free?email=${value}`, { signal });
|
|
106
|
+
return (await res.json()).free ? null : { code: 'taken', message: 'Email занят' };
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// прогон по требованию (submit/шаг): ошибки сами доезжают до нод формы, warnings не блокируют
|
|
112
|
+
const ok = await validateModel(model, emailSchema);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Cross-field — оператор `cross`, а не `ctx.form`
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// ❌ WRONG - ctx.form / ctx.setFieldValue / value.value — старый API; ModelValidator(value,scope,root) удалён
|
|
119
|
+
watchField(path.amount, (amount, ctx) => {
|
|
120
|
+
const rate = ctx.form.rate.value.value;
|
|
121
|
+
ctx.setFieldValue('total', amount * rate);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ✅ CORRECT - compute (поведение) на сигналах; cross-field (валидация) через оператор cross
|
|
125
|
+
compute(model.$.total, () => (model.amount ?? 0) * (model.rate ?? 0));
|
|
126
|
+
|
|
127
|
+
// cross-field правило — обычная функция над снапшотом Root (`model.get()`), навешивается через cross(sig, fn):
|
|
128
|
+
const amountVsMax = (f: MyForm): ValidationError | null =>
|
|
129
|
+
f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
|
|
130
|
+
? { code: 'tooBig', message: 'Превышает лимит' }
|
|
131
|
+
: null;
|
|
132
|
+
|
|
133
|
+
const amountSchema = defineValidationSchema<MyForm>(({ model }) => {
|
|
134
|
+
cross(model.$.amount, amountVsMax); // fn получает model.get(), вешает ошибку на model.$.amount
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Form-shape types — `type` over `interface`
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// ❌ interface лишён неявной index signature; конструкции ArrayNode<T> его отвергают
|
|
142
|
+
export interface PropertyItem {
|
|
143
|
+
type: PropertyType;
|
|
144
|
+
description: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ✅ type alias структурно совместим с Record<string, FormValue>
|
|
148
|
+
export type PropertyItem = {
|
|
149
|
+
type: PropertyType;
|
|
150
|
+
description: string;
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`number | null` — конвенциональный тип для очищенного поля; встроенные валидаторы
|
|
155
|
+
(`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`, `maxAge`) пропускают
|
|
156
|
+
пустые значения внутри.
|
|
157
|
+
|
|
158
|
+
### Proxy не проходит instanceof
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// ❌ instanceof на Proxy не работает
|
|
162
|
+
if (node instanceof FieldNode) { ... }
|
|
163
|
+
|
|
164
|
+
// ✅ type guards
|
|
165
|
+
import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
|
|
166
|
+
if (isFieldNode(node)) { ... }
|
|
167
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## 5. TROUBLESHOOTING
|
|
2
|
+
|
|
3
|
+
| Error | Cause | Solution |
|
|
4
|
+
| ------------------------------------------------------ | -------------------------------------------------------- | ------------------------------------------------- |
|
|
5
|
+
| `'string' is not assignable to '{ message?: string }'` | Валидатору передали строку вместо options | Используй `required({ message: 'text' })` |
|
|
6
|
+
| `Module has no exported member` | Неверный источник импорта | `watchField`/примитивы — из `@reformer/core`; DSL — из `@reformer/core/behaviors`; фабрики — из `@reformer/core/validators` |
|
|
7
|
+
| `undefined` из `useFormControlValue` | Деструктурировали хук | `const v = useFormControlValue(...)` — без деструктуризации |
|
|
8
|
+
| `enableWhen`/`disableWhen` не срабатывает | Поле не материализовано в форме (элемент массива) | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |
|
|
9
|
+
| `Cycle detected` | Взаимные `compute`/`computeFrom` без стабилизации | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |
|
|
10
|
+
| Значение поля не пишется | Пишем в форму вместо модели | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` |
|
|
11
|
+
| Ошибки не появляются после submit | Не вызвали `validateModel` (`form.submit()` НЕ гоняет schema-валидацию) | `await validateModel(model, schema)` из `@reformer/core/validation` — он роутит ошибки в ноды |
|
|
@@ -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")`
|