@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.
Files changed (63) hide show
  1. package/dist/behaviors.js +134 -134
  2. package/dist/form/form-submitter.d.ts +0 -4
  3. package/dist/form/hooks/types.d.ts +3 -3
  4. package/dist/form/index.d.ts +0 -3
  5. package/dist/form/nodes/array-node.d.ts +0 -84
  6. package/dist/form/nodes/form-node.d.ts +0 -14
  7. package/dist/form/nodes/group-node.d.ts +0 -4
  8. package/dist/form/status-machine.d.ts +0 -38
  9. package/dist/form/types/contracts.d.ts +0 -6
  10. package/dist/form/types/deep-schema.d.ts +0 -9
  11. package/dist/form/types/index.d.ts +4 -38
  12. package/dist/form/types/schema-node.d.ts +13 -16
  13. package/dist/form/types/validation-schema.d.ts +0 -25
  14. package/dist/form/unique-id.d.ts +3 -11
  15. package/dist/form/validators.d.ts +8 -3
  16. package/dist/form-model-DhW2HZOB.js +323 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +448 -636
  19. package/dist/runtime-token.d.ts +23 -0
  20. package/dist/state/form-model.d.ts +14 -0
  21. package/dist/state/index.d.ts +2 -1
  22. package/dist/state/types.d.ts +39 -11
  23. package/dist/state.js +18 -18
  24. package/dist/validation.js +1 -1
  25. package/dist/validators.d.ts +0 -4
  26. package/dist/validators.js +88 -122
  27. package/docs/llms/01-api-reference.md +63 -0
  28. package/docs/llms/02-quick-start.md +143 -0
  29. package/docs/llms/03-api-signatures.md +273 -0
  30. package/docs/llms/04-common-patterns.md +202 -0
  31. package/docs/llms/05-common-mistakes.md +167 -0
  32. package/docs/llms/06-troubleshooting.md +11 -0
  33. package/docs/llms/07-complete-import.md +99 -0
  34. package/docs/llms/08-form-types.md +47 -0
  35. package/docs/llms/09-formschema.md +143 -0
  36. package/docs/llms/10-arrays.md +161 -0
  37. package/docs/llms/11-async-watchfield.md +53 -0
  38. package/docs/llms/12-array-cleanup.md +31 -0
  39. package/docs/llms/13-multi-step.md +91 -0
  40. package/docs/llms/14-extended-mistakes.md +98 -0
  41. package/docs/llms/15-project-structure.md +125 -0
  42. package/docs/llms/16-ui-components.md +189 -0
  43. package/docs/llms/17-nonexistent-api.md +93 -0
  44. package/docs/llms/18-conditional-fields.md +183 -0
  45. package/docs/llms/19-reading-values.md +88 -0
  46. package/docs/llms/20-compute-vs-watch.md +147 -0
  47. package/docs/llms/21-array-operations.md +113 -0
  48. package/docs/llms/22-cycle-detection.md +86 -0
  49. package/docs/llms/23-copy-from.md +141 -0
  50. package/docs/llms/24-sync-fields.md +114 -0
  51. package/docs/llms/25-reset-when.md +115 -0
  52. package/docs/llms/26-transform-value.md +136 -0
  53. package/docs/llms/27-revalidate-when.md +132 -0
  54. package/docs/llms/28-submit-and-reset.md +259 -0
  55. package/docs/llms/29-async-preload.md +193 -0
  56. package/docs/llms/30-type-safety-recipes.md +159 -0
  57. package/docs/llms/31-async-validator-debounce.md +135 -0
  58. package/docs/llms/32-async-options-loading.md +108 -0
  59. package/docs/llms/33-validation-strategy.md +193 -0
  60. package/llms.txt +219 -1533
  61. package/package.json +33 -10
  62. package/dist/derived-registry-CluZCQ7U.js +0 -16
  63. 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")`