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