@reformer/core 11.0.0-beta.7 → 11.0.0-beta.8

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 (56) hide show
  1. package/README.md +7 -3
  2. package/dist/behaviors.js +211 -174
  3. package/dist/create-model-BBRHhHwK.js +30 -0
  4. package/dist/form/behaviors/collections.d.ts +14 -1
  5. package/dist/form/behaviors/context.d.ts +18 -2
  6. package/dist/form/behaviors/index.d.ts +3 -1
  7. package/dist/form/behaviors/internals.d.ts +0 -1
  8. package/dist/form/behaviors/schema-operators.d.ts +76 -0
  9. package/dist/form/behaviors/types.d.ts +15 -3
  10. package/dist/form/create-core-form.d.ts +5 -3
  11. package/dist/form/create-form.d.ts +28 -53
  12. package/dist/form/form-bundle.d.ts +136 -0
  13. package/dist/form/index.d.ts +6 -1
  14. package/dist/form/nodes/field-node.d.ts +2 -2
  15. package/dist/form/nodes/group-node.d.ts +1 -1
  16. package/dist/form/nodes/model-array-node.d.ts +10 -5
  17. package/dist/form/schema-controller.d.ts +137 -0
  18. package/dist/form/schema-subtree.d.ts +28 -0
  19. package/dist/form/signal-node-registry.d.ts +22 -16
  20. package/dist/form/types/deep-schema.d.ts +2 -2
  21. package/dist/form/types/form-proxy.d.ts +1 -1
  22. package/dist/form/types/schema-node.d.ts +37 -12
  23. package/dist/form/validation/context.d.ts +6 -1
  24. package/dist/form/validation/index.d.ts +1 -1
  25. package/dist/form/validation/operators.d.ts +70 -8
  26. package/dist/form/validation/types.d.ts +2 -1
  27. package/dist/index.js +345 -254
  28. package/dist/model/index.d.ts +5 -0
  29. package/dist/model/model-nodes.d.ts +67 -4
  30. package/dist/model/model-signals-proxy.d.ts +8 -1
  31. package/dist/model/model-value-proxy.d.ts +76 -0
  32. package/dist/model/types.d.ts +7 -4
  33. package/dist/model-value-proxy-caTmAiUd.js +419 -0
  34. package/dist/model.js +19 -17
  35. package/dist/node-BeV_lV5i.js +100 -0
  36. package/dist/platforms/react/form-bundle-context.d.ts +27 -0
  37. package/dist/platforms/react/index.d.ts +1 -0
  38. package/dist/strategy-DgzELWno.js +204 -0
  39. package/dist/validation.js +7 -6
  40. package/docs/llms/03-api-signatures.md +4 -4
  41. package/docs/llms/09-formschema.md +5 -5
  42. package/docs/llms/10-arrays.md +23 -4
  43. package/docs/llms/16-ui-components.md +2 -2
  44. package/docs/llms/18-conditional-fields.md +1 -1
  45. package/docs/llms/21-array-operations.md +26 -0
  46. package/docs/llms/28-submit-and-reset.md +3 -3
  47. package/docs/llms/29-async-preload.md +4 -4
  48. package/docs/llms/33-validation-strategy.md +2 -2
  49. package/docs/llms/35-unified-contract.md +246 -0
  50. package/llms-index.json +1 -1
  51. package/llms.txt +1148 -122
  52. package/package.json +1 -1
  53. package/dist/create-model-B8U-COv-.js +0 -103
  54. package/dist/model-signals-proxy-2vqS243J.js +0 -250
  55. package/dist/node-DGEFMTQu.js +0 -18
  56. package/dist/strategy-BXS_gBfz.js +0 -170
package/llms.txt CHANGED
@@ -39,6 +39,7 @@
39
39
  - 32-async-options-loading.md — async-options-loading
40
40
  - 33-validation-strategy.md — useFormValidation — Единый выбор стратегии валидации
41
41
  - 34-i18n.md — i18n — локаль, загрузка словарей и провайдер
42
+ - 35-unified-contract.md — Единый контракт формы — одна схема, одна сборка, одно поведение
42
43
  - API Reference (auto-generated from JSDoc)
43
44
 
44
45
  ## 1. 1. API Reference
@@ -275,7 +276,7 @@ createModel<T extends object>(initial: T): FormModel<T>
275
276
  // model.arrayField → ModelArray<Item> (push/removeAt/insertAt/move/swap/clear/at/map/length)
276
277
 
277
278
  // Форма (ноды поверх сигналов модели)
278
- createForm<T>({ model, schema, behavior? }): FormProxy<T>
279
+ createFormFromModel<T>({ model, schema, behavior? }): FormProxy<T>
279
280
  // form.<field> → FieldNode / GroupNode / FormArrayProxy
280
281
  // form.<field>.setValue(v) / .value.value / .errors.value / .disabled.value
281
282
  // form.<field>.enable() / .disable() / .reset() / .markAsTouched() / .setErrors([...])
@@ -331,7 +332,7 @@ maxAge(years: number, options?: { message?: string })
331
332
  `ValidationSchema` и гоняются раннером `validateModel` по требованию.
332
333
 
333
334
  ```typescript
334
- import { createModel, createForm } from '@reformer/core';
335
+ import { createModel, createForm, createFormFromModel } from '@reformer/core';
335
336
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
336
337
  import { required, min, max, email } from '@reformer/core/validators';
337
338
 
@@ -339,7 +340,7 @@ type Loan = { email: string; age: number | null; amount: number | null };
339
340
  const model = createModel<Loan>({ email: '', age: null, amount: null });
340
341
 
341
342
  // layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
342
- const form = createForm({
343
+ const form = createFormFromModel({
343
344
  model,
344
345
  schema: {
345
346
  email: { value: model.$.email, component: Input },
@@ -526,7 +527,7 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
526
527
  });
527
528
  });
528
529
 
529
- const form = createForm({ model, schema, behavior });
530
+ const form = createFormFromModel({ model, schema, behavior });
530
531
  ```
531
532
 
532
533
  DSL-операторы: `compute` (auto-tracking, без явного списка источников), `computeFrom`, `copyFrom`,
@@ -1112,7 +1113,7 @@ layout-схеме НЕТ — правила живут в отдельной с
1112
1113
  ### Primitive Fields
1113
1114
 
1114
1115
  ```typescript
1115
- import { createModel, createForm } from '@reformer/core';
1116
+ import { createModel, createFormFromModel } from '@reformer/core';
1116
1117
  import { defineValidationSchema, validate } from '@reformer/core/validation';
1117
1118
  import { required } from '@reformer/core/validators';
1118
1119
  import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
@@ -1153,7 +1154,7 @@ const schema = {
1153
1154
  },
1154
1155
  };
1155
1156
 
1156
- const form = createForm<MyForm>({ model, schema });
1157
+ const form = createFormFromModel<MyForm>({ model, schema });
1157
1158
  ```
1158
1159
 
1159
1160
  ### Nested Objects
@@ -1198,7 +1199,7 @@ const schema = {
1198
1199
 
1199
1200
  ```typescript
1200
1201
  // M1: данные из модели + схема (+ опциональный декларативный behavior)
1201
- const form = createForm<MyForm>({
1202
+ const form = createFormFromModel<MyForm>({
1202
1203
  model, // FormModel<MyForm> — обязателен
1203
1204
  schema, // дерево узлов, привязанных к сигналам
1204
1205
  behavior: myBehavior, // опционально: defineFormBehavior(...) из @reformer/core/behaviors
@@ -1213,12 +1214,12 @@ model.items.push({ id: '1', name: 'Item' }); // операции над масс
1213
1214
  > **Тип `schema` в M1 — `unknown`, не `FormSchema<T>`.** Это осознанно: интерпретатор обходит
1214
1215
  > произвольную структуру и собирает листья `{ value: signal, component?, ... }`, поэтому обёртки
1215
1216
  > (`{ children: [...] }`, wizard/steps) компилируются. Строгий `FormSchema<T>` (keyed-map по полям
1216
- > `T`) применяется только на legacy-пути `createForm(schema)` без модели.
1217
+ > `T`) применяется только на legacy-пути `createLegacyForm(schema)` без модели.
1217
1218
 
1218
1219
  ### createForm Returns a Proxy
1219
1220
 
1220
1221
  ```typescript
1221
- const form = createForm<MyForm>({ model, schema });
1222
+ const form = createFormFromModel<MyForm>({ model, schema });
1222
1223
 
1223
1224
  form.email; // FieldNode<string> — TypeScript знает тип
1224
1225
  form.address.city; // FieldNode<string> — вложенный доступ
@@ -1239,7 +1240,7 @@ if (isFieldNode(node)) { /* ... */ }
1239
1240
  элемента из его под-модели (`FormModel<Item>`).
1240
1241
 
1241
1242
  ```typescript
1242
- import { createModel, createForm, type FormModel } from '@reformer/core';
1243
+ import { createModel, createFormFromModel, type FormModel } from '@reformer/core';
1243
1244
  import { Input, InputNumber } from '@reformer/ui-kit';
1244
1245
 
1245
1246
  type Item = { id: string; name: string; price: number };
@@ -1258,7 +1259,7 @@ const schema = {
1258
1259
  items: { array: model.items, item: itemSchema },
1259
1260
  };
1260
1261
 
1261
- const form = createForm<MyForm>({ model, schema });
1262
+ const form = createFormFromModel<MyForm>({ model, schema });
1262
1263
  ```
1263
1264
 
1264
1265
  > **Type constraint:** тип элемента `Item` объявляй через `type`-alias (не `interface`) — иначе
@@ -1280,7 +1281,7 @@ const model = createModel<MyForm>({ tags: [] });
1280
1281
  const schema = {
1281
1282
  tags: { value: model.$.tags, component: SelectMulti }, // поле над массивом целиком
1282
1283
  };
1283
- const form = createForm<MyForm>({ model, schema });
1284
+ const form = createFormFromModel<MyForm>({ model, schema });
1284
1285
 
1285
1286
  form.tags.setValue(['a', 'b']); // FieldNode<string[]>
1286
1287
  model.$.tags.value = ['c']; // ручка значения: заменяет массив целиком
@@ -1295,6 +1296,25 @@ validate(model.$.tags, [required(), maxLength(3)]); // правило получ
1295
1296
  - Запись в узел-ГРУППУ (`model.$.<группа>.value = …`) — ошибка: группа пишется по полям или через
1296
1297
  `model.patch(...)`.
1297
1298
 
1299
+ ### Массив в группе и в строке другого массива
1300
+
1301
+ Узел `{ array, item }` стоит на любой глубине: массив в группе получает ноду `form.<группа>.<массив>`,
1302
+ массив в строке — `form.<массив>.at(i).<массив>`. Под-схема строки объявляет свои массивы так же.
1303
+
1304
+ ```typescript
1305
+ const contactItem = (contact: FormModel<Contact>) => ({
1306
+ name: { value: contact.$.name, component: Input },
1307
+ phones: { array: contact.phones, item: phoneItem }, // массив в строке массива
1308
+ });
1309
+
1310
+ const schema = {
1311
+ hotlines: { array: model.details.hotlines, item: phoneItem }, // массив в группе
1312
+ contacts: { array: model.contacts, item: contactItem },
1313
+ };
1314
+ ```
1315
+
1316
+ Поведение строк вложенного массива — `applyEach` внутри схемы строки, см. `21-array-operations.md`.
1317
+
1298
1318
  ### Один массив — три слоя (три разных движка)
1299
1319
 
1300
1320
  Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
@@ -1306,7 +1326,7 @@ validate(model.$.tags, [required(), maxLength(3)]); // правило получ
1306
1326
  `model.$.properties`.
1307
1327
 
1308
1328
  ```typescript
1309
- // узел схемы для createForm({ model, schema })
1329
+ // узел схемы для createFormFromModel({ model, schema })
1310
1330
  properties: { array: model.properties, item: propertyItem },
1311
1331
  ```
1312
1332
 
@@ -1866,7 +1886,7 @@ the guide below.
1866
1886
 
1867
1887
  ```tsx
1868
1888
  import { useMemo } from 'react';
1869
- import { createModel, createForm } from '@reformer/core';
1889
+ import { createModel, createFormFromModel } from '@reformer/core';
1870
1890
  import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
1871
1891
 
1872
1892
  type RegistrationForm = {
@@ -1901,7 +1921,7 @@ function RegistrationPage() {
1901
1921
  componentProps: { label: 'I agree to terms' },
1902
1922
  },
1903
1923
  };
1904
- return createForm<RegistrationForm>({ model, schema });
1924
+ return createFormFromModel<RegistrationForm>({ model, schema });
1905
1925
  }, []);
1906
1926
 
1907
1927
  return (
@@ -2230,7 +2250,7 @@ export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
2230
2250
  - **Все поля должны быть материализованы в модели** (`createModel`), даже те, что показываются
2231
2251
  только под условием. `enableWhen`/`compute`/`copyFrom` принимают сигналы `model.$.field`, а не
2232
2252
  строковые пути; сигнала для несуществующего поля нет.
2233
- - Поведение подключается к форме через `createForm({ model, schema, behavior })` — форма владеет
2253
+ - Поведение подключается к форме через `createFormFromModel({ model, schema, behavior })` — форма владеет
2234
2254
  жизненным циклом, DSL-операторы регистрируют свой cleanup сами (ручной массив cleanup'ов не нужен).
2235
2255
  - Таргетом может быть и **группа**: `enableWhen(model.$.residenceAddress, () => model.sameAsRegistration === false)`
2236
2256
  проходит по поддереву; без `resetOnDisable` значение группы сохраняется (удобно, когда оно копируется
@@ -2677,6 +2697,32 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
2677
2697
  });
2678
2698
  ```
2679
2699
 
2700
+ ### Вложенные области — массив в группе и в строке массива
2701
+
2702
+ Узел `{ array, item }` и операторы `applyEach` / `apply` / `enableWhen` работают на любой глубине:
2703
+ цель оператора — **ручка** `model.$.…`, под-модель и нода находятся по её идентичности, а не по
2704
+ пути.
2705
+
2706
+ ```typescript
2707
+ const phoneBehavior = defineFormBehavior<Phone>(({ model: phone }) => {
2708
+ enableWhen(phone.$.number, () => phone.primary);
2709
+ });
2710
+
2711
+ const contactBehavior = defineFormBehavior<Contact>(({ model: contact }) => {
2712
+ applyEach(contact.$.phones, phoneBehavior); // массив внутри строки
2713
+ enableWhen(contact.$.address, () => contact.quantity > 0); // группа внутри строки
2714
+ apply(contact.$.address, addressBehavior); // под-схема получает настоящую под-модель
2715
+ });
2716
+
2717
+ const behavior = defineFormBehavior<MyForm>(({ model }) => {
2718
+ applyEach(model.$.contacts, contactBehavior);
2719
+ });
2720
+ ```
2721
+
2722
+ Ручка → value-фасад: `modelOf(model.$.address)` — под-модель (`get` / `set` / `patch`),
2723
+ `modelOf(model.$.phones)` — фасад массива (`push` / `removeAt` / `at`). Нода формы по ручке —
2724
+ `getNodeForSignal(model.$.address)`.
2725
+
2680
2726
  ### Aggregate write — aggregateInto
2681
2727
 
2682
2728
  Агрегатная запись в строки (например, «последняя строка = 100 − Σ остальных»):
@@ -3493,7 +3539,7 @@ form.<field>.clearErrors();
3493
3539
 
3494
3540
  ```tsx
3495
3541
  import { useMemo } from 'react';
3496
- import { createModel, createForm } from '@reformer/core';
3542
+ import { createModel, createForm, createFormFromModel } from '@reformer/core';
3497
3543
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
3498
3544
  import { required, email, minLength } from '@reformer/core/validators';
3499
3545
 
@@ -3511,7 +3557,7 @@ function RegistrationForm() {
3511
3557
  const { model, form } = useMemo(() => {
3512
3558
  const m = createModel<RegistrationFormData>({ username: '', email: '', password: '' });
3513
3559
  // schema здесь — layout-дерево нод (компоненты/раскладка), без валидаторов.
3514
- return { model: m, form: createForm({ model: m, schema: buildLayout(m) }) };
3560
+ return { model: m, form: createFormFromModel({ model: m, schema: buildLayout(m) }) };
3515
3561
  }, []);
3516
3562
 
3517
3563
  const handleSubmit = async (e: React.FormEvent) => {
@@ -3643,7 +3689,7 @@ const handleSubmit = async (e) => {
3643
3689
 
3644
3690
  ```typescript
3645
3691
  // ❌ Валидаторы в layout-дереве, отдаваемом в createForm — layout не несёт правил
3646
- createForm({ model, schema: { username: { value: '', validators: [required()] } } });
3692
+ createFormFromModel({ model, schema: { username: { value: '', validators: [required()] } } });
3647
3693
 
3648
3694
  // ✅ Валидация — отдельная схема, прогоняется раннером
3649
3695
  const schema = defineValidationSchema<T>(({ model }) => {
@@ -3735,7 +3781,7 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
3735
3781
  ### Initial values в модели
3736
3782
 
3737
3783
  ```typescript
3738
- import { createModel, createForm } from '@reformer/core';
3784
+ import { createModel, createFormFromModel } from '@reformer/core';
3739
3785
  import { Input, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
3740
3786
 
3741
3787
  type ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };
@@ -3756,7 +3802,7 @@ const schema = {
3756
3802
  },
3757
3803
  marketing: { value: model.$.marketing, component: CheckboxWithLabel, componentProps: { label: 'Рассылка' } },
3758
3804
  };
3759
- const form = createForm({ model, schema });
3805
+ const form = createFormFromModel({ model, schema });
3760
3806
  // model.get() === { username: '', language: 'ru', marketing: true }
3761
3807
  // после правок: model.reset() возвращает к этим значениям
3762
3808
  ```
@@ -3843,14 +3889,14 @@ export const addressBehavior = defineFormBehavior<AddressForm>(({ model, form })
3843
3889
  // ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз
3844
3890
  function MyForm() {
3845
3891
  const model = createModel<T>(initial); // КАЖДЫЙ рендер!
3846
- const form = createForm({ model, schema });
3892
+ const form = createFormFromModel({ model, schema });
3847
3893
  }
3848
3894
 
3849
3895
  // ✅ Стабильные ссылки через useMemo
3850
3896
  function MyForm() {
3851
3897
  const { model, form } = useMemo(() => {
3852
3898
  const m = createModel<T>(initial);
3853
- return { model: m, form: createForm({ model: m, schema: buildSchema(m) }) };
3899
+ return { model: m, form: createFormFromModel({ model: m, schema: buildSchema(m) }) };
3854
3900
  }, []);
3855
3901
  }
3856
3902
  ```
@@ -4382,7 +4428,7 @@ function createFormValidation<T>(
4382
4428
  `markAsTouched` не нужен.
4383
4429
 
4384
4430
  ```tsx
4385
- import { createModel, createForm, useFormValidation } from '@reformer/core';
4431
+ import { createModel, createFormFromModel, useFormValidation } from '@reformer/core';
4386
4432
  import { defineValidationSchema, validate } from '@reformer/core/validation';
4387
4433
  import { required, email, minLength } from '@reformer/core/validators';
4388
4434
 
@@ -4397,7 +4443,7 @@ const registrationValidation = defineValidationSchema<RegistrationData>(({ model
4397
4443
 
4398
4444
  function RegistrationForm() {
4399
4445
  const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);
4400
- const form = useMemo(() => createForm({ model }), [model]);
4446
+ const form = useMemo(() => createFormFromModel({ model }), [model]);
4401
4447
 
4402
4448
  const { submit, isValidating } = useFormValidation({
4403
4449
  model,
@@ -4775,7 +4821,254 @@ const source = () => ({ weekStartsOn: 1, messages: {} });
4775
4821
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — жизненный цикл формы; смена языка его не затрагивает
4776
4822
  - [33-validation-strategy.md](./33-validation-strategy.md) — когда запускается валидация
4777
4823
 
4778
- ## 94. API Reference
4824
+ ## 94. Purpose
4825
+
4826
+ **Единый контракт формы — одна схема, одна сборка, одно поведение**
4827
+
4828
+ Форма описывается одинаково для всех способов реализации — разметка в JSX, рендерер по TS-схеме,
4829
+ рендерер по JSON. Схема одна (дерево узлов), привязка одна (ручка `model.$.…`), сборка одна
4830
+ (`createForm`), поведение одно (`defineFormBehavior`). Способы отличаются только видом схемы
4831
+ (билдер или документ) и тем, кто рисует результат.
4832
+
4833
+ Прежние записи — ключи `value` / `array`, оператор `each`, `createCoreForm` — пока принимаются.
4834
+ Новый код пишется так, как описано здесь.
4835
+
4836
+ | Слой | поле | массив под-форм | подформа |
4837
+ | --------- | --------------------------------- | ----------------------------------------- | ------------------------------------- |
4838
+ | схема | `{ model: model.$.x, component }` | `{ model: model.$.items, item }` | `{ model: model.$.group, part }` |
4839
+ | валидация | `validate(model.$.x, rules)` | `applyEach(model.$.items, itemRules)` | `apply(model.$.group, groupRules)` |
4840
+ | поведение | `compute(model.$.x, …)` | `applyEach(model.$.items, itemBehavior)` | `apply(model.$.group, groupBehavior)` |
4841
+
4842
+ Правило привязки: `model.$.…` — привязка (схема, правило, оператор). Без `$` — чтение значения
4843
+ (`model.loanType === 'mortgage'`), мутация (`model.items.push()`) и под-модель как область.
4844
+
4845
+ ## 95. Модель — начальные значения и шаблоны элементов
4846
+
4847
+ ```typescript
4848
+ import { arrayOf, createModel } from '@reformer/core';
4849
+
4850
+ const blankPhone = (): Phone => ({ number: '' });
4851
+ const blankCoBorrower = (): CoBorrower => ({ name: '', phones: arrayOf(blankPhone) });
4852
+
4853
+ export const createCreditModel = () =>
4854
+ createModel<CreditForm>({
4855
+ loanType: 'consumer',
4856
+ registrationAddress: { city: '', street: '' },
4857
+ coBorrowers: arrayOf(blankCoBorrower), // пустой массив + шаблон нового элемента
4858
+ tags: [], // массив как одно значение поля — шаблон не нужен
4859
+ });
4860
+ ```
4861
+
4862
+ `arrayOf(blank, items?)` возвращает обычный массив `U[]` и запоминает фабрику нового элемента.
4863
+ После этого `push()` и `insertAt(i)` без значения кладут элемент по шаблону — в модели, в ноде
4864
+ массива формы и в кнопке «Добавить» секции массива.
4865
+
4866
+ - Шаблон переживает `set` / `patch` / `reset` модели.
4867
+ - Вложенный массив объявляется внутри шаблона элемента; элементы, пришедшие обычными данными
4868
+ (загрузка с сервера), получают его шаблон из шаблона родителя.
4869
+ - У массива без шаблона `push()` без значения бросает ошибку с подсказкой.
4870
+ - Шаблон привязан к самому массиву: до `createModel` его нельзя копировать. `structuredClone`,
4871
+ `JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона. Начальные значения с
4872
+ `arrayOf` держат в фабрике, а не в константе, которую клонируют.
4873
+
4874
+ ## 96. Схема — одно дерево
4875
+
4876
+ ```typescript
4877
+ import type { FormModel } from '@reformer/core';
4878
+
4879
+ // подформа: объявлена один раз, получает под-модель
4880
+ const address = (model: FormModel<Address>) => ({
4881
+ component: Box,
4882
+ children: [
4883
+ { model: model.$.city, component: Input, componentProps: { label: 'Город' } },
4884
+ { model: model.$.street, component: Input, componentProps: { label: 'Улица' } },
4885
+ ],
4886
+ });
4887
+
4888
+ // строка массива — такая же функция от под-модели
4889
+ const coBorrower = (model: FormModel<CoBorrower>) => ({
4890
+ selector: 'co-borrower',
4891
+ component: Box,
4892
+ children: [{ model: model.$.name, component: Input, componentProps: { label: 'ФИО' } }],
4893
+ });
4894
+
4895
+ export const creditSchema = (model: FormModel<CreditForm>) => ({
4896
+ component: Box,
4897
+ children: [
4898
+ { model: model.$.loanType, component: SelectAsync, componentProps: { label: 'Тип' } }, // поле
4899
+ { model: model.$.tags, component: SelectMulti }, // массив как одно значение
4900
+ {
4901
+ selector: 'registration',
4902
+ component: Section,
4903
+ componentProps: { title: 'Адрес регистрации' },
4904
+ children: [{ model: model.$.registrationAddress, part: address }], // подформа
4905
+ },
4906
+ { model: model.$.coBorrowers, component: FormArray, item: coBorrower }, // массив под-форм
4907
+ ],
4908
+ });
4909
+ ```
4910
+
4911
+ Чем узел является, решают ручка и соседние ключи:
4912
+
4913
+ | Узел | Как отличить |
4914
+ | --------------- | --------------------------------------------- |
4915
+ | поле | `model` — лист или массив; нет `item`, `part` |
4916
+ | массив под-форм | `model` — массив, `item` — функция |
4917
+ | подформа | `model` — группа, `part` — функция |
4918
+ | контейнер | есть `children`, нет `model` |
4919
+
4920
+ - Узел узнаётся по ЗНАЧЕНИЮ: в записи «имя поля → узел» поле данных может называться `model`,
4921
+ `item`, `part` или `value` — под таким ключом лежит обычный вложенный узел.
4922
+ - Поля подформы принадлежат той же форме: `form.registrationAddress.city`.
4923
+ - Билдер `item` / `part` вызывается один раз на под-модель; сборка и рендерер получают одно и то
4924
+ же поддерево (`schemaSubtree(builder, subModel)`).
4925
+ - `initialValue` узла-массива — запасной шаблон нового элемента для форм, чья модель создаётся из
4926
+ данных без кода. Шаблон модели (`arrayOf`) главнее.
4927
+
4928
+ ## 97. Валидация — подформа и массив подключаются привязкой
4929
+
4930
+ ```typescript
4931
+ import {
4932
+ apply,
4933
+ applyEach,
4934
+ cross,
4935
+ defineValidationSchema,
4936
+ validate,
4937
+ validateWhen,
4938
+ } from '@reformer/core/validation';
4939
+
4940
+ const addressRules = defineValidationSchema<Address>(({ model }) => {
4941
+ validate(model.$.city, [required()]);
4942
+ cross<Address>(model.$.street, (address) =>
4943
+ address.city !== '' && address.street === '' ? streetRequired : null
4944
+ );
4945
+ });
4946
+
4947
+ const coBorrowerRules = defineValidationSchema<CoBorrower>(({ model }) => {
4948
+ validate(model.$.name, [required()]);
4949
+ });
4950
+
4951
+ export const contactsRules = defineValidationSchema<CreditForm>(({ model }) => {
4952
+ apply(model.$.registrationAddress, addressRules);
4953
+ validateWhen(
4954
+ () => !model.sameAsRegistration,
4955
+ () => apply(model.$.residenceAddress, addressRules)
4956
+ );
4957
+ applyEach(model.$.coBorrowers, coBorrowerRules);
4958
+ });
4959
+ ```
4960
+
4961
+ - У схемы, подключённой через `apply(ручка, схема)` / `applyEach`, своя область: `model` — под-модель,
4962
+ `cross` получает её снапшот. Захватывать снапшот в замыкание больше не нужно.
4963
+ - Одна и та же схема подключается и к группе, и к элементам массива.
4964
+ - `apply(model.$.a, rules)` принимает и массив ручек: `apply([model.$.a, model.$.b], rules)`.
4965
+ - `apply(schemaA, schemaB)` — композиция схем над той же моделью — остаётся.
4966
+ - Правило получает `(value, scope, root)`: модель области и корень прогона.
4967
+ - `each(model.items, (item) => …)` — прежняя запись `applyEach`: принимает фасад и ручку, области
4968
+ не создаёт.
4969
+
4970
+ ## 98. Поведение — модель, форма и схема
4971
+
4972
+ ```typescript
4973
+ import {
4974
+ apply,
4975
+ applyEach,
4976
+ defineFormBehavior,
4977
+ enableWhen,
4978
+ hideWhen,
4979
+ onComponentEvent,
4980
+ onMount,
4981
+ } from '@reformer/core/behaviors';
4982
+
4983
+ const addressBehavior = defineFormBehavior<Address>(({ model, schema }) => {
4984
+ hideWhen(schema.node('street'), () => model.city === ''); // узел ищется внутри части
4985
+ });
4986
+
4987
+ export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, schema }) => {
4988
+ const isMortgage = () => model.loanType === 'mortgage';
4989
+
4990
+ enableWhen(model.$.propertyValue, isMortgage, { resetOnDisable: true });
4991
+ hideWhen(schema.node('mortgage'), () => !isMortgage());
4992
+ onComponentEvent(schema.node('wizard'), 'onSubmit', () => submitApplication(model.get()));
4993
+ onMount(schema.node('data-boundary'), () => void loadApplication(model));
4994
+
4995
+ apply([model.$.registrationAddress, model.$.residenceAddress], addressBehavior);
4996
+ applyEach(model.$.coBorrowers, coBorrowerBehavior);
4997
+ });
4998
+ ```
4999
+
5000
+ Операторы узлов схемы:
5001
+
5002
+ | Оператор | Что записывает |
5003
+ | ----------------------------------------- | ---------------------------------------------------------- |
5004
+ | `hideWhen(node, condition)` | узел скрыт, пока условие истинно |
5005
+ | `onComponentEvent(node, event, handler)` | обработчик пропа-события компонента |
5006
+ | `onMount(node, fn)` / `onUnmount(node, fn)` | хуки монтирования узла |
5007
+ | `onInit(node, fn)` | `fn` вызывается сразу, до первого рендера |
5008
+ | `renderEffect(schema, fn)` | реактивный эффект, пока схема области смонтирована |
5009
+ | `schema.node(sel).setHidden / patchProps` | принудительная видимость и пропсы узла |
5010
+ | `schema.node(sel).getRef()` | ref на компонент узла — объект `{ current }` |
5011
+
5012
+ - **Области изолированы.** `schema.node(selector)` ищет узел только в своей области: корневое
5013
+ поведение — в корневом дереве, поведение подформы и строки массива — в своём поддереве. До узла
5014
+ строки или части корень дотягивается через `applyEach` / `apply`. Два монтирования одной части
5015
+ не конфликтуют.
5016
+ - **Исключение — ref по пути модели**: `schema.node('phones.0.number').getRef()` ищется от корня.
5017
+ - **Операторы узлов только записывают правило — исполняет его рендерер.** При разметке в JSX они
5018
+ ничего не делают: видимость, отправка и загрузка данных остаются в JSX.
5019
+ - Правило на селектор, которого нет в дереве, ничего не делает; в dev — предупреждение.
5020
+
5021
+ ## 99. Сборка — `createForm`
5022
+
5023
+ ```tsx
5024
+ import { createForm, useFormBundle } from '@reformer/core';
5025
+
5026
+ const credit = useFormBundle(() =>
5027
+ createForm<CreditForm>({
5028
+ model: createCreditModel(), // либо initial: { … }
5029
+ schema: creditSchema, // (model) => узел; для JSON — документ + registry
5030
+ behavior: creditBehavior,
5031
+ validation: { steps: { loan: loanRules, contacts: contactsRules }, extras: crossRules },
5032
+ })
5033
+ );
5034
+ // credit = { model, form, validation, render }
5035
+
5036
+ <FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />; // рендерер
5037
+ <FormField control={credit.form.loanType} />; // разметка в JSX — та же сборка
5038
+ ```
5039
+
5040
+ - Порядок: модель → `seed` → дерево схемы → ноды формы → поведение → валидация → `setup`. Дерево
5041
+ строится один раз.
5042
+ - `validation` в бандле типизирована по конфигу: правила переданы — поле есть всегда.
5043
+ - `render` — `{ tree, controller, node(selector) }`: готовое дерево и схема-контроллер сборки.
5044
+ - JSON: `createForm({ model, schema: document, registry })` — дерево собирает
5045
+ `registry.resolveSchema`. Ядро документ не разбирает.
5046
+ - Внутри рендерера бандл доступен компонентам через `useFormBundleContext()` — так визард берёт
5047
+ форму и валидацию сам.
5048
+
5049
+ ### Низкоуровневые фабрики
5050
+
5051
+ | Задача | Фабрика |
5052
+ | ---------------------------------------------- | ---------------------------------------- |
5053
+ | форма из модели и готового дерева, без бандла | `createFormFromModel({ model, schema })` |
5054
+ | форма без модели — плоская схема полей | `createLegacyForm(schema)` |
5055
+
5056
+ `createForm` с готовым деревом вместо билдера или без модели бросает ошибку с отсылкой к нужной
5057
+ фабрике.
5058
+
5059
+ ## 100. Частые ошибки
5060
+
5061
+ | Ошибка | Как правильно |
5062
+ | -------------------------------------------------- | ------------------------------------------------------- |
5063
+ | `model: model.address` (под-модель у поля) | `model: model.$.address.city` — привязка всегда с `$` |
5064
+ | `{ model: model.$.address, component }` без `part` | группа полем не бывает — нужен `part` |
5065
+ | `createForm({ model, schema: дерево })` | `schema` — билдер `(model) => узел` |
5066
+ | `hideWhen(schema.node('row-узел'), …)` из корня | внутри `applyEach(model.$.items, itemBehavior)` |
5067
+ | `push({})` для новой строки | `arrayOf(blank)` в модели и `push()` без значения |
5068
+ | `createModel(structuredClone(INITIAL))` с `arrayOf` | фабрика начальных значений — клон теряет шаблон массива |
5069
+ | `cross` в подформе читает корень | `apply(model.$.group, rules)` — область подформы |
5070
+
5071
+ ## 101. API Reference
4779
5072
 
4780
5073
  _Auto-generated from JSDoc on public exports._
4781
5074
 
@@ -4825,14 +5118,22 @@ _Source: src/form/types/index.ts_
4825
5118
 
4826
5119
  **Kind:** `function`
4827
5120
 
4828
- Композиция под-схем в текущую (над той же моделью scope). Заменяет пошаговую группировку.
5121
+ Применить под-схему к одному или нескольким полям-группам (переиспользование).
5122
+
5123
+ Под-схема получает scope группы: `model` — настоящая под-модель (`model.$.field`, чтение и
5124
+ запись значений, `get`/`set`/`patch`), `form` — нода группы. Цель — ручка группы
5125
+ (`model.$.address`); работает и в корневой схеме, и в схеме строки массива.
4829
5126
 
4830
5127
  **Signature:**
4831
5128
  ```typescript
4832
- export function apply<T>(...schemas: ValidationSchema<T>[]): void
5129
+ export function apply<TField>(targets: object | object[], subSchema: FormBehavior<TField>): void
4833
5130
  ```
4834
5131
 
4835
- _Source: src/form/validation/operators.ts_
5132
+ **Examples:**
5133
+
5134
+ apply([model.$.registrationAddress, model.$.residenceAddress], addressBehavior);
5135
+
5136
+ _Source: src/form/behaviors/collections.ts_
4836
5137
 
4837
5138
  ### applyEach
4838
5139
 
@@ -4848,6 +5149,9 @@ _Source: src/form/validation/operators.ts_
4848
5149
  рендерится, а её сигналы зарегистрированы (`enableWhen` резолвит ноду). Без материализации доступ
4849
5150
  к `form.*` бросит понятную ошибку (см. {@link unmaterializedRowForm}).
4850
5151
 
5152
+ Работает на любой глубине: массив в корне, в группе и в строке другого массива — в схеме строки
5153
+ можно снова вызвать `applyEach`.
5154
+
4851
5155
  **Signature:**
4852
5156
  ```typescript
4853
5157
  export function applyEach<TItem>(array: object, itemSchema: FormBehavior<TItem>): void
@@ -4858,6 +5162,7 @@ export function applyEach<TItem>(array: object, itemSchema: FormBehavior<TItem>)
4858
5162
  applyEach(model.$.items, defineFormBehavior<Item>(({ model: row, form }) => {
4859
5163
  compute(row.$.lineTotal, () => row.qty * row.price); // value-op — всегда
4860
5164
  enableWhen(row.$.discount, () => row.qty > 10); // node-op — нужна материализация массива
5165
+ applyEach(row.$.phones, phoneBehavior); // массив внутри строки
4861
5166
  }));
4862
5167
 
4863
5168
  _Source: src/form/behaviors/collections.ts_
@@ -5074,6 +5379,52 @@ export interface ArrayNodeLike {
5074
5379
 
5075
5380
  _Source: src/form/types/index.ts_
5076
5381
 
5382
+ ### arrayOf
5383
+
5384
+ **Kind:** `function`
5385
+
5386
+ Массив модели с шаблоном нового элемента.
5387
+
5388
+ Шаблон — фабрика значения, которое получает `push()` / `insertAt(i)` без аргумента: кнопка
5389
+ «Добавить» секции массива, `model.items.push()` в поведении. Объявляется там же, где остальные
5390
+ начальные значения, — в модели; схеме и JSX знать его не нужно.
5391
+
5392
+ Возвращает обычный массив: тип поля остаётся `U[]`. Шаблон переживает `set` / `patch` / `reset`
5393
+ модели. Вложенные массивы объявляются внутри шаблона элемента — тогда и элементы, пришедшие
5394
+ обычными данными (загрузка с сервера), получают их шаблоны.
5395
+
5396
+ Шаблон привязан к самому массиву, поэтому до `createModel` его нельзя копировать:
5397
+ `structuredClone`, `JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона, и
5398
+ `push()` без значения бросит ошибку. Начальные значения с `arrayOf` держат в фабрике
5399
+ (`const initial = () => ({ items: arrayOf(blank) })`), а не в константе, которую клонируют.
5400
+
5401
+ **Signature:**
5402
+ ```typescript
5403
+ export function arrayOf<U>(blank: () => U, items: readonly U[] = []): U[]
5404
+ ```
5405
+
5406
+ **Parameters:**
5407
+ - `blank` — - Фабрика нового элемента. Вызывается на каждое добавление — значение не делится
5408
+ между элементами.
5409
+ - `items` — - Начальные элементы. По умолчанию массив пуст.
5410
+
5411
+ **Returns:** Массив начальных значений с шаблоном.
5412
+
5413
+ **Examples:**
5414
+
5415
+ ```typescript
5416
+ const blankPhone = () => ({ number: '' });
5417
+ const blankCoBorrower = () => ({ name: '', phones: arrayOf(blankPhone) });
5418
+
5419
+ const model = createModel({ coBorrowers: arrayOf(blankCoBorrower) });
5420
+
5421
+ model.coBorrowers.push(); // { name: '', phones: [] }
5422
+ model.coBorrowers.at(0).phones.push(); // { number: '' }
5423
+ model.coBorrowers.push({ name: 'Анна', phones: [] }); // значение целиком — как раньше
5424
+ ```
5425
+
5426
+ _Source: src/model/model-nodes.ts_
5427
+
5077
5428
  ### AsyncRule
5078
5429
 
5079
5430
  **Kind:** `type`
@@ -5167,11 +5518,19 @@ _Source: src/model/behaviors-value.ts_
5167
5518
 
5168
5519
  **Kind:** `type`
5169
5520
 
5170
- Контекст схемы поведения: модель (значения/сигналы) + форма (ноды).
5521
+ Область схемы поведения: модель (значения и сигналы), форма (ноды) и схема (узлы разметки).
5522
+
5523
+ `schema` адресует узлы СВОЕЙ области: в корневом поведении — корневое дерево, в поведении
5524
+ подформы (`apply`) и строки массива (`applyEach`) — поддерево этой части. Правила узлов
5525
+ (`hideWhen`, `onComponentEvent`, `onMount`) исполняет рендерер; без него они ничего не делают.
5171
5526
 
5172
5527
  **Signature:**
5173
5528
  ```typescript
5174
- export type BehaviorScope<T> = { model: FormModel<T>; form: FormProxy<T> };
5529
+ export type BehaviorScope<T> = {
5530
+ model: FormModel<T>;
5531
+ form: FormProxy<T>;
5532
+ schema: SchemaScope;
5533
+ };
5175
5534
  ```
5176
5535
 
5177
5536
  _Source: src/form/behaviors/types.ts_
@@ -5483,6 +5842,8 @@ const credit = useFormBundle(() =>
5483
5842
  return <FormWizard form={credit.form} config={credit.validation} steps={STEPS} />;
5484
5843
  ```
5485
5844
 
5845
+ **Deprecated:** Пишите `createForm` — та же сборка, в бандле вдобавок дерево для рендерера.
5846
+
5486
5847
  _Source: src/form/create-core-form.ts_
5487
5848
 
5488
5849
  ### CreateCoreFormConfig
@@ -5495,8 +5856,8 @@ _Source: src/form/create-core-form.ts_
5495
5856
  ```typescript
5496
5857
  export interface CreateCoreFormConfig<T> extends CreateFormConfigBase<T, CoreForm<T>> {
5497
5858
  /**
5498
- * Билдер схемы формы. Именно функция, а не готовое дерево: листья схемы держат сами сигналы
5499
- * модели (`value: model.$.email`), поэтому построить дерево до модели нечем.
5859
+ * Билдер схемы формы. Именно функция, а не готовое дерево: узлы схемы держат ручки модели
5860
+ * (`model: model.$.email`), поэтому построить дерево до модели нечем.
5500
5861
  */
5501
5862
  schema?: (model: FormModel<T>) => FormSchemaNode;
5502
5863
  }
@@ -5508,53 +5869,89 @@ _Source: src/form/create-core-form.ts_
5508
5869
 
5509
5870
  **Kind:** `function`
5510
5871
 
5511
- Создать форму из {@link FormModel} + единой схемы (архитектура M1, рекомендуемый путь).
5872
+ Собрать модель, форму, валидацию и дерево для рендера за один проход.
5512
5873
 
5513
5874
  **Signature:**
5514
5875
  ```typescript
5515
- export function createForm<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>;
5876
+ export function createForm<T extends object>(
5877
+ config: CreateFormConfig<T> & { validation: FormValidation<T> | ValidationSchema<T> }
5878
+ ): ValidatedFormBundle<T>;
5516
5879
  ```
5517
5880
 
5518
5881
  **Parameters:**
5519
- - `args` — - Модель данных, layout-схема (component/componentProps) и (опционально) поведение
5882
+ - `config` — - {@link CreateFormConfig}: (`initial` | `model`) + опц. `schema`, `registry`,
5883
+ `behavior`, `validation`, `seed`, `setup`.
5520
5884
 
5521
- **Returns:** Типизированная форма с Proxy-доступом к полям
5885
+ **Returns:**
5522
5886
 
5523
5887
  **Examples:**
5524
5888
 
5525
- Архитектура M1: `createModel` + layout-схема + `createForm({ model, schema })`
5526
- ```typescript
5527
- import { createModel, createForm } from '@reformer/core';
5528
- import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
5529
- import { required, email, minLength } from '@reformer/core/validators';
5889
+ Рендерер по TS-схеме
5890
+ ```tsx
5891
+ const credit = useFormBundle(() =>
5892
+ createForm<CreditForm>({
5893
+ model: createCreditModel(),
5894
+ schema: creditSchema, // (model) => узел
5895
+ behavior: creditBehavior,
5896
+ validation: { steps: { loan: loanRules }, extras: crossRules },
5897
+ })
5898
+ );
5899
+ return <FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />;
5900
+ ```
5530
5901
 
5531
- interface UserForm {
5532
- email: string;
5533
- password: string;
5534
- }
5902
+ React-руками — та же сборка, разметка в JSX
5903
+ ```tsx
5904
+ const credit = useFormBundle(() => createForm<CreditForm>({ initial, schema: creditSchema }));
5905
+ return <FormField control={credit.form.loanType} />;
5906
+ ```
5535
5907
 
5536
- const model = createModel<UserForm>({ email: '', password: '' });
5908
+ JSON — документ схемы и реестр
5909
+ ```tsx
5910
+ const credit = useFormBundle(() =>
5911
+ createForm<CreditForm>({ model: createCreditModel(), schema: creditJson, registry })
5912
+ );
5913
+ return <FormRenderer form={credit} />;
5914
+ ```
5537
5915
 
5538
- // Layout-схема НЕ несёт validators — только component/componentProps.
5539
- const schema = {
5540
- children: [
5541
- { value: model.$.email, component: Input },
5542
- { value: model.$.password, component: Input },
5543
- ],
5544
- };
5916
+ _Source: src/form/form-bundle.ts_
5545
5917
 
5546
- const form = createForm<UserForm>({ model, schema });
5547
- form.email.setValue('test@mail.com');
5918
+ ### CreateFormConfig
5548
5919
 
5549
- // Валидация — ОТДЕЛЬНАЯ схема, прогон внешним раннером (ошибки сами доезжают до нод):
5550
- const validation = defineValidationSchema<UserForm>(({ model }) => {
5551
- validate(model.$.email, [required(), email()]);
5552
- validate(model.$.password, [required(), minLength(8)]);
5553
- });
5554
- const ok = await validateModel(model, validation);
5920
+ **Kind:** `interface`
5921
+
5922
+ Конфиг {@link createForm}.
5923
+
5924
+ **Signature:**
5925
+ ```typescript
5926
+ export interface CreateFormConfig<T> {
5927
+ /** Начальные значения — из них создаётся модель. Взаимоисключимо с `model`. */
5928
+ initial?: T;
5929
+ /** Готовая модель (если создана отдельной фабрикой). Приоритетнее `initial`. */
5930
+ model?: FormModel<T>;
5931
+ /**
5932
+ * Схема формы: билдер дерева `(model) => узел` либо документ (JSON) — тогда нужен `registry`.
5933
+ * Билдер, а не готовое дерево: узлы держат ручки модели (`model: model.$.email`), и построить
5934
+ * дерево до модели нечем.
5935
+ */
5936
+ schema?: ((model: FormModel<T>) => FormSchemaNode) | object;
5937
+ /** Реестр JSON-варианта: превращает документ схемы в дерево. */
5938
+ registry?: SchemaResolver;
5939
+ /** Поведение формы: значения, состояние нод и правила узлов схемы. */
5940
+ behavior?: FormBehavior<T>;
5941
+ /** Правила валидации: готовая схема либо {@link FormValidation} со стратегией и шагами. */
5942
+ validation?: FormValidation<T> | ValidationSchema<T>;
5943
+ /**
5944
+ * Правка модели ДО сборки формы. Нужна там, где значение обязано существовать к моменту
5945
+ * построения нод — например, массив, наполняемый из вычислений (реактивные `onChange` на
5946
+ * инициализации не срабатывают).
5947
+ */
5948
+ seed?: (model: FormModel<T>) => void;
5949
+ /** Донастройка ПОСЛЕ сборки: получает готовый бандл. */
5950
+ setup?: (bundle: FormBundle<T>) => void;
5951
+ }
5555
5952
  ```
5556
5953
 
5557
- _Source: src/form/create-form.ts_
5954
+ _Source: src/form/form-bundle.ts_
5558
5955
 
5559
5956
  ### CreateFormConfigBase
5560
5957
 
@@ -5581,7 +5978,7 @@ export interface CreateFormConfigBase<T, B> {
5581
5978
  seed?: (model: FormModel<T>) => void;
5582
5979
  /**
5583
5980
  * Донастройка ПОСЛЕ сборки. Единственное место для правок, которые обязаны идти следом за
5584
- * `createForm`: поле, чьё значение уже массив, ноды не получает (см. `buildModelConfig`),
5981
+ * сборкой формы: поле, чьё значение уже массив, ноды не получает (см. `buildModelConfig`),
5585
5982
  * поэтому такой префилл выполняется здесь.
5586
5983
  */
5587
5984
  setup?: (bundle: B) => void;
@@ -5598,12 +5995,14 @@ _Source: src/form/create-core-form.ts_
5598
5995
 
5599
5996
  Значения принадлежат модели (источник истины), ноды формы держат UI/валидационное состояние и
5600
5997
  ссылаются на сигналы модели по идентичности (`node.value === model.$.path`). Обходит структуру
5601
- модели, привязывает конфиг поля (component/componentProps) из схемы, материализует
5602
- top-level массивы как {@link ModelArrayNode}, заполняет реестр сигнал→нода (для `enableWhen`/
5603
- роутинга ошибок) и, при наличии, запускает декларативное поведение (cleanup живёт на форме).
5998
+ модели, привязывает конфиг поля (component/componentProps) из схемы, материализует массивы
5999
+ под-форм как {@link ModelArrayNode} (на любой глубине), заполняет реестр ручка→нода (для
6000
+ `enableWhen`, `apply`/`applyEach` и роутинга ошибок) и, при наличии, запускает декларативное
6001
+ поведение (cleanup живёт на форме).
5604
6002
 
5605
- Обычно вызывается неявно через {@link createForm} с аргументом `{ model, schema }` — прямой вызов
5606
- нужен редко (например, для построения формы элемента массива).
6003
+ Обычно вызывается сборкой одним вызовом (`createForm`): она строит дерево схемы, зовёт эту
6004
+ фабрику и собирает валидацию. Прямой вызов нужен там, где бандл не нужен, — форма элемента
6005
+ массива, тесты, свой слой сборки.
5607
6006
 
5608
6007
  **Signature:**
5609
6008
  ```typescript
@@ -5617,7 +6016,7 @@ export function createFormFromModel<T>(args: CreateFormFromModelArgs<T>): FormPr
5617
6016
 
5618
6017
  **Examples:**
5619
6018
 
5620
- Форма из модели + схемы (эквивалент `createForm({ model, schema })`)
6019
+ Форма из модели + схемы (эквивалент `createFormFromModel({ model, schema })`)
5621
6020
  ```typescript
5622
6021
  import { createModel, createFormFromModel } from '@reformer/core';
5623
6022
 
@@ -5645,7 +6044,7 @@ console.log(model.email); // 'user@mail.com'
5645
6044
  ```
5646
6045
 
5647
6046
  **See also:**
5648
- - {@link createForm} - основная фабрика (диспетчеризует сюда при аргументе `{ model, schema }`)
6047
+ - `createForm` — сборка одним вызовом: модель, форма, валидация и дерево для рендера
5649
6048
 
5650
6049
  _Source: src/form/create-form.ts_
5651
6050
 
@@ -5671,6 +6070,12 @@ export interface CreateFormFromModelArgs<T> {
5671
6070
  * заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
5672
6071
  */
5673
6072
  behavior?: FormBehavior<T>;
6073
+ /**
6074
+ * Схема-контроллер сборки: через него поведение получает `schema` своей области. Его передаёт
6075
+ * сборка одним вызовом, чтобы правила узлов (`hideWhen`, `onComponentEvent`) дошли до рендерера.
6076
+ * Без него поведение пишет правила в собственный контроллер, который никто не читает.
6077
+ */
6078
+ schemaController?: SchemaController;
5674
6079
  }
5675
6080
  ```
5676
6081
 
@@ -5736,6 +6141,37 @@ i18n.fileSize(1536); // '1,5 КБ'
5736
6141
 
5737
6142
  _Source: src/i18n/translator.ts_
5738
6143
 
6144
+ ### createLegacyForm
6145
+
6146
+ **Kind:** `function`
6147
+
6148
+ Создать форму из плоской схемы полей с инлайн-значениями (`value: ''`) — путь ДО архитектуры M1.
6149
+
6150
+ Значения живут в нодах, модели нет: поведение (`defineFormBehavior`) и внешняя валидация
6151
+ (`validateModel`) к такой форме не подключаются. Для нового кода — {@link createFormFromModel}
6152
+ либо сборка одним вызовом.
6153
+
6154
+ **Signature:**
6155
+ ```typescript
6156
+ export function createLegacyForm<T>(schema: FormSchema<T>): FormProxy<T>;
6157
+ ```
6158
+
6159
+ **Parameters:**
6160
+ - `schema` — - Схема полей формы
6161
+
6162
+ **Returns:** Типизированная форма с Proxy-доступом к полям
6163
+
6164
+ **Examples:**
6165
+
6166
+ ```typescript
6167
+ const form = createLegacyForm<UserForm>({
6168
+ email: { value: '', component: Input },
6169
+ password: { value: '', component: Input },
6170
+ });
6171
+ ```
6172
+
6173
+ _Source: src/form/create-form.ts_
6174
+
5739
6175
  ### createLocaleLoader
5740
6176
 
5741
6177
  **Kind:** `function`
@@ -5863,13 +6299,70 @@ model.get(); // { email: 'a@b.c', profile: { name: 'Ada' }, tag
5863
6299
 
5864
6300
  _Source: src/model/create-model.ts_
5865
6301
 
6302
+ ### createSchemaController
6303
+
6304
+ **Kind:** `function`
6305
+
6306
+ Схема-контроллер одной сборки формы.
6307
+
6308
+ **Signature:**
6309
+ ```typescript
6310
+ export function createSchemaController(): SchemaController
6311
+ ```
6312
+
6313
+ **Examples:**
6314
+
6315
+ ```typescript
6316
+ const controller = createSchemaController();
6317
+ const root = controller.scopeOf(model);
6318
+ root.node('mortgage').setHidden(true);
6319
+
6320
+ controller.scopeOf(model.items.at(0)); // область строки — свои селекторы
6321
+ ```
6322
+
6323
+ _Source: src/form/schema-controller.ts_
6324
+
6325
+ ### createSchemaOverrideMaps
6326
+
6327
+ **Kind:** `function`
6328
+
6329
+ Пустое хранилище переопределений области.
6330
+
6331
+ **Signature:**
6332
+ ```typescript
6333
+ export function createSchemaOverrideMaps(): SchemaOverrideMaps
6334
+ ```
6335
+
6336
+ _Source: src/form/schema-controller.ts_
6337
+
6338
+ ### createSchemaScope
6339
+
6340
+ **Kind:** `function`
6341
+
6342
+ Схема области поверх хранилища: `node(selector)` → управление узлом.
6343
+
6344
+ **Signature:**
6345
+ ```typescript
6346
+ export function createSchemaScope(
6347
+ maps: SchemaOverrideMaps = createSchemaOverrideMaps()
6348
+ ): SchemaScope
6349
+ ```
6350
+
6351
+ **Parameters:**
6352
+ - `maps` — - Хранилище области; по умолчанию создаётся новое.
6353
+
6354
+ _Source: src/form/schema-controller.ts_
6355
+
5866
6356
  ### cross
5867
6357
 
5868
6358
  **Kind:** `function`
5869
6359
 
5870
- Cross-field правило: `fn` получает СНАПШОТ модели текущего scope (`model.get()`) и вешает ошибку на `sig`.
5871
- Для элементов массива / под-моделей захватывайте нужный снапшот в замыкание (`const item = im.get()`),
5872
- т.к. `fn` всегда получает модель ТЕКУЩЕГО scope (корень прогона), а не под-модель.
6360
+ Cross-field правило: `fn` получает СНАПШОТ модели текущей области (`model.get()`) и вешает
6361
+ ошибку на `sig`.
6362
+
6363
+ Область — та модель, с которой запущена схема: в корне прогона это вся форма, а в схеме,
6364
+ подключённой через {@link apply} / {@link applyEach}, — под-модель группы или строки массива.
6365
+ Поэтому правила подформы пишутся один раз и не знают, где она стоит.
5873
6366
 
5874
6367
  **Signature:**
5875
6368
  ```typescript
@@ -5922,10 +6415,11 @@ export function defineFormBehavior<T>(setup: (scope: BehaviorScope<T>) => void):
5922
6415
  **Examples:**
5923
6416
 
5924
6417
  ```ts
5925
- export const myBehavior = defineFormBehavior<MyForm>(({ model, form }) => {
6418
+ export const myBehavior = defineFormBehavior<MyForm>(({ model, form, schema }) => {
5926
6419
  compute(model.$.total, () => model.price * model.qty);
5927
6420
  enableWhen([model.$.city], () => Boolean(model.country));
5928
6421
  onChange(model.$.country, async (c) => form.city.updateComponentProps({ options: await load(c) }));
6422
+ hideWhen(schema.node('delivery'), () => model.country === '');
5929
6423
  });
5930
6424
  ```
5931
6425
 
@@ -6019,18 +6513,21 @@ _Source: src/form/behaviors/node.ts_
6019
6513
 
6020
6514
  **Kind:** `function`
6021
6515
 
6022
- Применить под-правила к КАЖДОМУ элементу текущего массива модели.
6023
- `U extends object` — элементы должны быть под-моделями (объектами); массив примитивов — одно
6024
- значение поля, его проверяют целиком: `validate(model.$.<массив>, [...])`.
6516
+ Применить под-правила к КАЖДОМУ элементу массива — прежняя запись {@link applyEach}.
6517
+
6518
+ Колбэк получает под-модель элемента, но области не создаёт: `cross` внутри него видит снапшот
6519
+ корня прогона.
6025
6520
 
6026
6521
  **Signature:**
6027
6522
  ```typescript
6028
6523
  export function each<U extends object>(
6029
- arr: ModelArray<U>,
6524
+ arr: ArrayBinding<U> | ModelArray<U>,
6030
6525
  itemFn: (item: FormModel<U>) => void
6031
6526
  ): void
6032
6527
  ```
6033
6528
 
6529
+ **Deprecated:** Пишите `applyEach(model.$.<массив>, schema)`.
6530
+
6034
6531
  _Source: src/form/validation/operators.ts_
6035
6532
 
6036
6533
  ### eachLeafSignal
@@ -6892,13 +7389,57 @@ _Source: src/i18n/message-format.ts_
6892
7389
  **Signature:**
6893
7390
  ```typescript
6894
7391
  export interface FormBehavior<T> {
6895
- /** @internal Запускается `createForm` после построения нод и заполнения реестра сигнал→нода. */
6896
- __run(model: FormModel<T>, form: FormProxy<T>): BehaviorCleanup;
6897
- }
7392
+ /**
7393
+ * @internal Запускается сборкой формы после построения нод и заполнения реестра сигнал→нода.
7394
+ * `controller` — схема-контроллер сборки; без него поведение получает собственный, который
7395
+ * никто не читает (модель без рендерера).
7396
+ */
7397
+ __run(model: FormModel<T>, form: FormProxy<T>, controller?: SchemaController): BehaviorCleanup;
7398
+ }
6898
7399
  ```
6899
7400
 
6900
7401
  _Source: src/form/behaviors/types.ts_
6901
7402
 
7403
+ ### FormBundle
7404
+
7405
+ **Kind:** `interface`
7406
+
7407
+ Результат {@link createForm}.
7408
+
7409
+ **Signature:**
7410
+ ```typescript
7411
+ export interface FormBundle<T> {
7412
+ model: FormModel<T>;
7413
+ form: FormProxy<T>;
7414
+ /** Собранная валидация; есть, если в конфиге заданы правила. */
7415
+ validation?: FormValidationBundle<T>;
7416
+ render: FormRender;
7417
+ }
7418
+ ```
7419
+
7420
+ _Source: src/form/form-bundle.ts_
7421
+
7422
+ ### FormBundleContext
7423
+
7424
+ **Kind:** `const`
7425
+
7426
+ Контекст сборки. Значение — бандл `createForm`; вне рендерера — `null`.
7427
+
7428
+ **Signature:**
7429
+ ```typescript
7430
+ export const FormBundleContext
7431
+ ```
7432
+
7433
+ **Examples:**
7434
+
7435
+ ```tsx
7436
+ <FormBundleContext.Provider value={bundle}>
7437
+ <MyWizard />
7438
+ </FormBundleContext.Provider>
7439
+ ```
7440
+
7441
+ _Source: src/platforms/react/form-bundle-context.ts_
7442
+
6902
7443
  ### FormBundleLike
6903
7444
 
6904
7445
  **Kind:** `interface`
@@ -7165,7 +7706,7 @@ interface UserForm {
7165
7706
  };
7166
7707
  }
7167
7708
 
7168
- const form = createForm<UserForm>(schema);
7709
+ const form = createLegacyForm<UserForm>(schema);
7169
7710
 
7170
7711
  // Доступ к методам GroupNode
7171
7712
  await form.validate();
@@ -7179,6 +7720,30 @@ form.profile.name.setValue('John');
7179
7720
 
7180
7721
  _Source: src/form/types/form-proxy.ts_
7181
7722
 
7723
+ ### FormRender
7724
+
7725
+ **Kind:** `interface`
7726
+
7727
+ Часть бандла для рендерера: готовое дерево и схема-контроллер сборки.
7728
+
7729
+ **Signature:**
7730
+ ```typescript
7731
+ export interface FormRender {
7732
+ /** Корневой узел дерева схемы; `undefined`, если схема не задана. */
7733
+ readonly tree: FormSchemaNode | undefined;
7734
+ /** Схема-контроллер сборки: области корня, строк массивов и подформ. */
7735
+ readonly controller: SchemaController;
7736
+ /** Управление узлом КОРНЕВОЙ области по его `selector`. */
7737
+ node(selector: string): SchemaNodeControl;
7738
+ /** Обёртка поля из реестра (JSON-вариант). */
7739
+ readonly fieldWrapper?: unknown;
7740
+ /** Граница ошибок из реестра (JSON-вариант). */
7741
+ readonly errorBoundary?: unknown;
7742
+ }
7743
+ ```
7744
+
7745
+ _Source: src/form/form-bundle.ts_
7746
+
7182
7747
  ### FormSchema
7183
7748
 
7184
7749
  **Kind:** `type`
@@ -7192,7 +7757,7 @@ _Source: src/form/types/form-proxy.ts_
7192
7757
  Использует NonNullable для корректной обработки опциональных полей.
7193
7758
 
7194
7759
  ⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
7195
- контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
7760
+ контейнер), передаваемой в `createFormFromModel({ model, schema })`. `FormSchema` — это data-shaped конфиг
7196
7761
  (ключи = поля данных), а не узел дерева.
7197
7762
 
7198
7763
  **Signature:**
@@ -7248,12 +7813,13 @@ _Source: src/form/types/deep-schema.ts_
7248
7813
 
7249
7814
  **Kind:** `interface`
7250
7815
 
7251
- Узел единой схемы M1 — layout-дерево, обходимое `createForm({ model, schema })`
7816
+ Узел единой схемы M1 — layout-дерево, обходимое `createFormFromModel({ model, schema })`
7252
7817
  и рендерерами (schema-валидация живёт отдельно — `@reformer/core/validation`).
7253
7818
 
7254
7819
  Узел совмещает несколько ролей (различаются рантаймом по форме):
7255
- - **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
7256
- - **массив** — `{ array: model.<path>, item(itemModel) }`;
7820
+ - **поле** — `{ model: model.$.x, component, componentProps }`;
7821
+ - **массив под-форм** — `{ model: model.$.items, item: (model) => узел }`;
7822
+ - **подформа** — `{ model: model.$.group, part: (model) => узел }`;
7257
7823
  - **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
7258
7824
  - **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).
7259
7825
 
@@ -7264,9 +7830,20 @@ _Source: src/form/types/deep-schema.ts_
7264
7830
  ```typescript
7265
7831
  export interface FormSchemaNode {
7266
7832
  /**
7267
- * «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма
7268
- * зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до
7269
- * `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
7833
+ * Привязка узла к части модели — ручка из дерева `model.$`. Чем узел является, решают ручка и
7834
+ * соседние ключи:
7835
+ * - лист или массив (`model.$.email`, `model.$.tags`) — **поле**;
7836
+ * - массив вместе с {@link FormSchemaNode.item} — **массив под-форм**;
7837
+ * - группа вместе с {@link FormSchemaNode.part} — **подформа**.
7838
+ *
7839
+ * Узел распознаётся по значению, а не по имени ключа: в записи «имя поля → узел» поле данных
7840
+ * может называться `model`, и тогда под этим ключом лежит обычный вложенный узел.
7841
+ */
7842
+ model?: unknown;
7843
+ /**
7844
+ * Прежняя запись привязки поля — то же, что {@link FormSchemaNode.model}.
7845
+ *
7846
+ * @deprecated Пишите `model: model.$.<path>`.
7270
7847
  */
7271
7848
  value?: unknown;
7272
7849
  /**
@@ -7296,14 +7873,27 @@ export interface FormSchemaNode {
7296
7873
  * выводит на своём месте в порядке следования.
7297
7874
  */
7298
7875
  children?: readonly (FormSchemaNode | string | number | Signal<any>)[];
7299
- /** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */
7876
+ /**
7877
+ * Прежняя запись привязки массива под-форм — фасад `model.<path>` (вместе с `item`).
7878
+ *
7879
+ * @deprecated Пишите `model: model.$.<path>`.
7880
+ */
7300
7881
  array?: SchemaArrayControl;
7301
7882
  /** Схема элемента массива: под-модель элемента → узел поддерева. */
7302
- item?: (itemModel: any) => FormSchemaNode;
7883
+ item?: (model: any) => FormSchemaNode;
7303
7884
  /**
7304
- * Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,
7305
- * либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`
7306
- * схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.
7885
+ * Подформа: под-модель группы → узел поддерева. Часть объявляется один раз и подключается к
7886
+ * любой группе той же формы данных: `{ model: model.$.registrationAddress, part: address }`.
7887
+ * Привязки внутри части идут через `$` полученной под-модели.
7888
+ */
7889
+ part?: (model: any) => FormSchemaNode;
7890
+ /**
7891
+ * Шаблон нового элемента массива для кнопки «Добавить»: либо готовое значение, либо фабрика
7892
+ * `() => value`. Запасной путь — для форм, чья модель создаётся из данных без кода: шаблон,
7893
+ * объявленный в модели (`arrayOf(blank)`), главнее.
7894
+ *
7895
+ * Тип не различает варианты (union `unknown | (() => unknown)` схлопывается в `unknown`) —
7896
+ * рантайм различает по `typeof initialValue === 'function'`.
7307
7897
  */
7308
7898
  initialValue?: unknown;
7309
7899
  /** Свободная вложенность: record-of-fields и произвольные под-узлы. */
@@ -7522,21 +8112,22 @@ _Source: src/platforms/react/field-binding/field-adapter.ts_
7522
8112
 
7523
8113
  **Kind:** `function`
7524
8114
 
7525
- Найти ноду формы по сигналу модели.
8115
+ Найти ноду формы по ручке модели.
7526
8116
 
7527
- Используется state-операциями behavior (`enableWhen`/`disableWhen`) и роутингом ошибок
7528
- валидации в ноды. Возвращает `undefined`, если форма ещё не построена или поле не
7529
- материализовано в этой форме (например, элемент массива, который строится per-item).
8117
+ Используется state-операциями behavior (`enableWhen`/`disableWhen`), подключением под-схем
8118
+ (`apply`/`applyEach`) и роутингом ошибок валидации в ноды. Возвращает `undefined`, если форма
8119
+ ещё не построена или эта часть модели в форме не материализована (например, массив без
8120
+ item-схемы).
7530
8121
 
7531
8122
  **Signature:**
7532
8123
  ```typescript
7533
- export function getNodeForSignal(signal: Signal<any>): FormNode<any> | undefined
8124
+ export function getNodeForSignal(signal: object): FormNode<any> | undefined
7534
8125
  ```
7535
8126
 
7536
8127
  **Parameters:**
7537
- - `signal` — - Сигнал значения из {@link FormModel}
8128
+ - `signal` — - Ручка из {@link FormModel}: `model.$.<поле>`, `model.$.<группа>`, `model.$.<массив>`
7538
8129
 
7539
- **Returns:** Нода формы для этого поля или `undefined`
8130
+ **Returns:** Нода формы — поля, группы или массива — либо `undefined`
7540
8131
 
7541
8132
  **Examples:**
7542
8133
 
@@ -7548,8 +8139,14 @@ const sig = model.$.email;
7548
8139
  getNodeForSignal(sig)?.setErrors([{ code: 'required', message: 'Обязательно' }]);
7549
8140
  ```
7550
8141
 
8142
+ Нода группы и строки массива
8143
+ ```typescript
8144
+ getNodeForSignal(model.$.address)?.disable(); // группа целиком
8145
+ getNodeForSignal(model.$.phones[0])?.markAsTouched(); // форма строки массива
8146
+ ```
8147
+
7551
8148
  **See also:**
7552
- - {@link registerSignalNode} - регистрация связи сигнал→нода
8149
+ - {@link registerSignalNode} - регистрация связи ручка→нода
7553
8150
 
7554
8151
  _Source: src/form/signal-node-registry.ts_
7555
8152
 
@@ -7583,7 +8180,7 @@ _Source: src/form/type-guards.ts_
7583
8180
 
7584
8181
  **Kind:** `function`
7585
8182
 
7586
- Текущий scope ({ model, form }) активной схемы (escape hatch для кросс-операторов).
8183
+ Текущая область ({ model, form, schema }) активной схемы (escape hatch для кросс-операторов).
7587
8184
 
7588
8185
  **Signature:**
7589
8186
  ```typescript
@@ -7599,7 +8196,7 @@ _Source: src/form/behaviors/context.ts_
7599
8196
  GroupNode - узел для группы полей
7600
8197
 
7601
8198
  Создаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`
7602
- (M1: `createForm({ model, schema })`); schema-валидация/behavior живут на слое модели
8199
+ (M1: `createFormFromModel({ model, schema })`); schema-валидация/behavior живут на слое модели
7603
8200
  (`validateModel` из `@reformer/core/validation`, `computeFrom`/`enableWhen`/…), а не на ноде.
7604
8201
 
7605
8202
  **Signature:**
@@ -7646,6 +8243,28 @@ export interface GroupNodeConfig<T> {
7646
8243
 
7647
8244
  _Source: src/form/types/index.ts_
7648
8245
 
8246
+ ### hideWhen
8247
+
8248
+ **Kind:** `function`
8249
+
8250
+ Скрывать узел, пока условие истинно.
8251
+
8252
+ Условие реактивно: пересчитывается при изменении любого сигнала, прочитанного внутри него.
8253
+ Принудительное `schema.node(selector).setHidden(...)` главнее условия.
8254
+
8255
+ **Signature:**
8256
+ ```typescript
8257
+ export function hideWhen(node: SchemaNodeControl, condition: () => boolean): void
8258
+ ```
8259
+
8260
+ **Examples:**
8261
+
8262
+ ```typescript
8263
+ hideWhen(schema.node('mortgage'), () => model.loanType !== 'mortgage');
8264
+ ```
8265
+
8266
+ _Source: src/form/behaviors/schema-operators.ts_
8267
+
7649
8268
  ### I18N_CONTEXT_MARKER
7650
8269
 
7651
8270
  **Kind:** `const`
@@ -9137,10 +9756,13 @@ export interface ModelArray<U> {
9137
9756
  readonly __path: string;
9138
9757
  /** Реактивная длина. */
9139
9758
  readonly length: number;
9140
- /** Добавить элемент в конец (значение элемента целиком). */
9141
- push(item: U): void;
9142
- /** Вставить элемент по индексу. */
9143
- insertAt(index: number, item: U): void;
9759
+ /**
9760
+ * Добавить элемент в конец (значение элемента целиком). Без значения — элемент по шаблону
9761
+ * массива (`arrayOf(blank)` в модели); шаблона нет — ошибка.
9762
+ */
9763
+ push(item?: U): void;
9764
+ /** Вставить элемент по индексу. Без значения — элемент по шаблону, как у {@link push}. */
9765
+ insertAt(index: number, item?: U): void;
9144
9766
  /** Удалить элемент по индексу. */
9145
9767
  removeAt(index: number): void;
9146
9768
  /** Переместить элемент. */
@@ -9177,8 +9799,8 @@ _Source: src/model/types.ts_
9177
9799
  export interface ModelArrayControl<TItem extends object> {
9178
9800
  readonly length: number;
9179
9801
  at(index: number): FormModel<TItem> | undefined;
9180
- push(item: TItem): void;
9181
- insertAt(index: number, item: TItem): void;
9802
+ push(item?: TItem): void;
9803
+ insertAt(index: number, item?: TItem): void;
9182
9804
  removeAt(index: number): void;
9183
9805
  move(from: number, to: number): void;
9184
9806
  swap(a: number, b: number): void;
@@ -9202,7 +9824,7 @@ _Source: src/form/nodes/model-array-node.ts_
9202
9824
  reorder/повторном рендере не пересоздаются (состояние и валидация сохраняются). Реализует тот же
9203
9825
  контракт, что ждут секции массива и `useFormControl` (`length`/`value`/`valid`/`errors`/`at`/`push`/…).
9204
9826
 
9205
- Обычно создаётся не напрямую, а `createForm({ model, schema })`: когда в схеме встречается узел
9827
+ Обычно создаётся не напрямую, а `createFormFromModel({ model, schema })`: когда в схеме встречается узел
9206
9828
  массива `{ array: model.<field>, item: (item) => itemSchema }`, форма материализует его как
9207
9829
  `ModelArrayNode` и кладёт под `form.<field>` (совместим с `FormArraySection`).
9208
9830
 
@@ -9222,7 +9844,7 @@ name: { value: item.$.name, component: Input },
9222
9844
  qty: { value: item.$.qty, component: Input },
9223
9845
  });
9224
9846
 
9225
- const form = createForm({
9847
+ const form = createFormFromModel({
9226
9848
  model,
9227
9849
  schema: {
9228
9850
  children: [{ array: model.rows, item: rowItem }],
@@ -9300,6 +9922,57 @@ export type ModelObject<T> = {
9300
9922
 
9301
9923
  _Source: src/model/types.ts_
9302
9924
 
9925
+ ### modelOf
9926
+
9927
+ **Kind:** `function`
9928
+
9929
+ Value-фасад по ручке дерева `model.$`: группа → под-модель {@link FormModel}, массив →
9930
+ {@link ModelArray}. Обратная операция к `model.$`: `modelOf(model.$.address) === model.address`.
9931
+
9932
+ Ручка находит фасад по ИДЕНТИЧНОСТИ, а не по пути. Путь для этого не годится: он абсолютный
9933
+ (`items.0.phones`) и меняется при перестановке строк, а области поведения и формы строк
9934
+ вложенные — поиск по пути от под-модели строки ничего не находит.
9935
+
9936
+ Лист ручкой контейнера не является — функция бросает. В частности, группа или массив,
9937
+ созданные из начального `null`, в модели — лист: вид узла фиксируется при создании.
9938
+
9939
+ **Signature:**
9940
+ ```typescript
9941
+ export function modelOf<V extends object | null | undefined>(handle: { peek(): V }): ModelOf<V>
9942
+ ```
9943
+
9944
+ **Parameters:**
9945
+ - `handle` — - Узел-группа или узел-массив дерева `model.$`.
9946
+
9947
+ **Returns:** Под-модель группы либо фасад массива.
9948
+
9949
+ **Examples:**
9950
+
9951
+ ```typescript
9952
+ const model = createModel({ address: { city: '' }, phones: [{ number: '' }] });
9953
+
9954
+ modelOf(model.$.address).city = 'Казань'; // под-модель: чтение, запись, get/set/patch
9955
+ modelOf(model.$.phones).push({ number: '+7' }); // фасад массива: мутации и обход
9956
+ modelOf(model.$.phones[0]).number; // строка массива — тоже группа
9957
+ ```
9958
+
9959
+ _Source: src/model/model-value-proxy.ts_
9960
+
9961
+ ### ModelOf
9962
+
9963
+ **Kind:** `type`
9964
+
9965
+ Value-фасад для ручки {@link modelOf}: массив → {@link ModelArray}, объект → под-модель
9966
+ {@link FormModel}.
9967
+
9968
+ **Signature:**
9969
+ ```typescript
9970
+ export type ModelOf<V> =
9971
+ NonNullable<V> extends ReadonlyArray<infer U> ? ModelArray<U> : FormModel<NonNullable<V>>;
9972
+ ```
9973
+
9974
+ _Source: src/model/model-value-proxy.ts_
9975
+
9303
9976
  ### ModelSignals
9304
9977
 
9305
9978
  **Kind:** `type`
@@ -9478,6 +10151,25 @@ const node = factory.createNode({ value: '', component: Input }); // → FieldNo
9478
10151
 
9479
10152
  _Source: src/form/factories/node-factory.ts_
9480
10153
 
10154
+ ### NodeLifecycleHooks
10155
+
10156
+ **Kind:** `interface`
10157
+
10158
+ Хуки жизненного цикла узла схемы. Каждый хук опционален; повторная регистрация перезаписывает
10159
+ предыдущее значение.
10160
+
10161
+ **Signature:**
10162
+ ```typescript
10163
+ export interface NodeLifecycleHooks {
10164
+ /** Срабатывает один раз при mount узла. Может вернуть cleanup-функцию. */
10165
+ onMount?: () => void | (() => void);
10166
+ /** Срабатывает один раз при unmount узла. */
10167
+ onUnmount?: () => void;
10168
+ }
10169
+ ```
10170
+
10171
+ _Source: src/form/schema-controller.ts_
10172
+
9481
10173
  ### nonNegative
9482
10174
 
9483
10175
  **Kind:** `function`
@@ -9572,6 +10264,34 @@ export function onChange<T>(
9572
10264
 
9573
10265
  _Source: src/form/behaviors/operators.ts_
9574
10266
 
10267
+ ### onComponentEvent
10268
+
10269
+ **Kind:** `function`
10270
+
10271
+ Повесить обработчик на проп-событие компонента узла (`onSubmit`, `onChange`, …).
10272
+
10273
+ Обработчик получает те же аргументы, что и исходный проп компонента, и перекрывает одноимённый
10274
+ проп из `componentProps`.
10275
+
10276
+ **Signature:**
10277
+ ```typescript
10278
+ export function onComponentEvent(
10279
+ node: SchemaNodeControl,
10280
+ event: string,
10281
+ handler: (...args: any[]) => any
10282
+ ): void
10283
+ ```
10284
+
10285
+ **Examples:**
10286
+
10287
+ ```typescript
10288
+ onComponentEvent(schema.node('wizard'), 'onSubmit', async () => {
10289
+ await submitApplication(model.get());
10290
+ });
10291
+ ```
10292
+
10293
+ _Source: src/form/behaviors/schema-operators.ts_
10294
+
9575
10295
  ### onDispose
9576
10296
 
9577
10297
  **Kind:** `function`
@@ -9585,6 +10305,70 @@ export function onDispose(cleanup: BehaviorCleanup): void
9585
10305
 
9586
10306
  _Source: src/form/behaviors/context.ts_
9587
10307
 
10308
+ ### onInit
10309
+
10310
+ **Kind:** `function`
10311
+
10312
+ Синхронный хук времени сборки: `fn` вызывается сразу, до первого рендера узла. Единственный
10313
+ хук, способный повлиять на первый рендер, — внутри можно править пропсы через
10314
+ `schema.node(selector).patchProps({ … })`.
10315
+
10316
+ К монтированию узла не привязан; аргумент `node` принимается ради симметрии с
10317
+ {@link onMount} / {@link onUnmount}.
10318
+
10319
+ **Signature:**
10320
+ ```typescript
10321
+ export function onInit(_node: SchemaNodeControl, fn: () => void): void
10322
+ ```
10323
+
10324
+ **Examples:**
10325
+
10326
+ ```typescript
10327
+ onInit(schema.node('summary'), () => {
10328
+ schema.node('summary').patchProps({ currency: detectCurrency() });
10329
+ });
10330
+ ```
10331
+
10332
+ _Source: src/form/behaviors/schema-operators.ts_
10333
+
10334
+ ### onMount
10335
+
10336
+ **Kind:** `function`
10337
+
10338
+ Хук после первого монтирования узла. Может вернуть очистку — она выполнится при размонтировании.
10339
+
10340
+ **Signature:**
10341
+ ```typescript
10342
+ export function onMount(node: SchemaNodeControl, fn: () => void | (() => void)): void
10343
+ ```
10344
+
10345
+ **Examples:**
10346
+
10347
+ ```typescript
10348
+ onMount(schema.node('data-boundary'), () => void loadApplication());
10349
+ ```
10350
+
10351
+ _Source: src/form/behaviors/schema-operators.ts_
10352
+
10353
+ ### onUnmount
10354
+
10355
+ **Kind:** `function`
10356
+
10357
+ Хук перед размонтированием узла.
10358
+
10359
+ **Signature:**
10360
+ ```typescript
10361
+ export function onUnmount(node: SchemaNodeControl, fn: () => void): void
10362
+ ```
10363
+
10364
+ **Examples:**
10365
+
10366
+ ```typescript
10367
+ onUnmount(schema.node('wizard'), () => saveDraft(model.get()));
10368
+ ```
10369
+
10370
+ _Source: src/form/behaviors/schema-operators.ts_
10371
+
9588
10372
  ### parseMessage
9589
10373
 
9590
10374
  **Kind:** `function`
@@ -9780,19 +10564,19 @@ _Source: src/form/validators/phone.ts_
9780
10564
 
9781
10565
  **Kind:** `function`
9782
10566
 
9783
- Связать сигнал модели с его нодой формы.
10567
+ Связать ручку модели с её нодой формы.
9784
10568
 
9785
- Вызывается движком при сборке формы (`createForm`/{@link createFormFromModel}) для каждого
9786
- листового поля. Прикладной код обычно этот реестр не заполняет напрямую.
10569
+ Вызывается движком при сборке формы (`createForm`/{@link createFormFromModel}) для каждого поля,
10570
+ группы и массива под-форм. Прикладной код обычно этот реестр не заполняет напрямую.
9787
10571
 
9788
10572
  **Signature:**
9789
10573
  ```typescript
9790
- export function registerSignalNode(signal: Signal<any>, node: FormNode<any>): void
10574
+ export function registerSignalNode(signal: object, node: FormNode<any>): void
9791
10575
  ```
9792
10576
 
9793
10577
  **Parameters:**
9794
- - `signal` — - Сигнал значения из {@link FormModel} (ручка поля, `model.$.path`)
9795
- - `node` — - Нода формы, отвечающая за это поле
10578
+ - `signal` — - Ручка из {@link FormModel}: сигнал листа, узел-массив или узел-группа дерева `model.$`
10579
+ - `node` — - Нода формы, отвечающая за эту часть модели
9796
10580
 
9797
10581
  **Examples:**
9798
10582
 
@@ -9806,10 +10590,35 @@ if (sig && node) registerSignalNode(sig, node);
9806
10590
  ```
9807
10591
 
9808
10592
  **See also:**
9809
- - {@link getNodeForSignal} - обратный поиск ноды по сигналу
10593
+ - {@link getNodeForSignal} - обратный поиск ноды по ручке
9810
10594
 
9811
10595
  _Source: src/form/signal-node-registry.ts_
9812
10596
 
10597
+ ### renderEffect
10598
+
10599
+ **Kind:** `function`
10600
+
10601
+ Реактивный эффект, живущий, пока схема области смонтирована рендерером.
10602
+
10603
+ В отличие от `effect` поведения, запускается после монтирования — внутри доступны ref узлов
10604
+ (`schema.node(selector).getRef().current`). Может вернуть функцию очистки.
10605
+
10606
+ **Signature:**
10607
+ ```typescript
10608
+ export function renderEffect(schema: SchemaScope, effectFn: () => void | (() => void)): void
10609
+ ```
10610
+
10611
+ **Examples:**
10612
+
10613
+ ```typescript
10614
+ const wizard = schema.node('wizard').getRef<FormWizardHandle>();
10615
+ renderEffect(schema, () => {
10616
+ if (model.loanType === 'mortgage') wizard.current?.goToStep(1);
10617
+ });
10618
+ ```
10619
+
10620
+ _Source: src/form/behaviors/schema-operators.ts_
10621
+
9813
10622
  ### required
9814
10623
 
9815
10624
  **Kind:** `function`
@@ -9874,6 +10683,27 @@ resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue:
9874
10683
 
9875
10684
  _Source: src/model/behaviors-value.ts_
9876
10685
 
10686
+ ### ResolvedSchema
10687
+
10688
+ **Kind:** `interface`
10689
+
10690
+ Дерево, собранное реестром из документа схемы (JSON), и то, что реестр хочет донести до
10691
+ рендерера вместе с ним.
10692
+
10693
+ **Signature:**
10694
+ ```typescript
10695
+ export interface ResolvedSchema {
10696
+ /** Корневой узел дерева. */
10697
+ tree: FormSchemaNode;
10698
+ /** Обёртка поля — компонент, которым рендерер оборачивает каждое поле. */
10699
+ fieldWrapper?: unknown;
10700
+ /** Граница ошибок — компонент, которым рендерер оборачивает дерево. */
10701
+ errorBoundary?: unknown;
10702
+ }
10703
+ ```
10704
+
10705
+ _Source: src/form/form-bundle.ts_
10706
+
9877
10707
  ### resolveLocalized
9878
10708
 
9879
10709
  **Kind:** `function`
@@ -9961,7 +10791,8 @@ _Source: src/model/behaviors-value.ts_
9961
10791
  (`required()`/`min()`/… — они `(value, model, root) => …`) и inline-правила `(value) => …`
9962
10792
  ОБА присваиваются в `Rule<TField>[]` без `any`, при этом сохраняется проверка типа поля
9963
10793
  (`validate(model.$.age, [email()])` подсветится — `email` ждёт `string`, поле `number`).
9964
- На вызове раннер приводит правило к callable и передаёт `(value, model, model)`.
10794
+ На вызове раннер приводит правило к callable и передаёт `(value, scope, root)`: модель текущей
10795
+ области (в корне прогона — вся модель) и корень прогона.
9965
10796
 
9966
10797
  **Signature:**
9967
10798
  ```typescript
@@ -10097,13 +10928,155 @@ export interface SchemaArrayControl {
10097
10928
  /** Реактивная длина. */
10098
10929
  readonly length: number;
10099
10930
  at(index: number): unknown;
10100
- push(item: unknown): void;
10931
+ push(item?: unknown): void;
10101
10932
  removeAt(index: number): void;
10102
10933
  }
10103
10934
  ```
10104
10935
 
10105
10936
  _Source: src/form/types/schema-node.ts_
10106
10937
 
10938
+ ### SchemaController
10939
+
10940
+ **Kind:** `interface`
10941
+
10942
+ Схема-контроллер сборки: области по под-моделям.
10943
+
10944
+ **Signature:**
10945
+ ```typescript
10946
+ export interface SchemaController {
10947
+ /**
10948
+ * Область под-модели: корня сборки, строки массива или группы подформы. Создаётся при первом
10949
+ * обращении; повторное обращение отдаёт ту же область.
10950
+ */
10951
+ scopeOf(subModel: object): SchemaScope;
10952
+ }
10953
+ ```
10954
+
10955
+ _Source: src/form/schema-controller.ts_
10956
+
10957
+ ### SchemaNodeControl
10958
+
10959
+ **Kind:** `interface`
10960
+
10961
+ Управление одним узлом схемы. Получается через `schema.node(selector)`.
10962
+
10963
+ **Signature:**
10964
+ ```typescript
10965
+ export interface SchemaNodeControl {
10966
+ /** Принудительно скрыть или показать узел, минуя условие `hideWhen`. */
10967
+ setHidden(value: boolean): this;
10968
+ /** Убрать принудительное значение — снова действует условие `hideWhen`. */
10969
+ resetHidden(): this;
10970
+ /** Подмешать объект в `componentProps` узла. */
10971
+ patchProps(partial: Record<string, unknown>): this;
10972
+ /** Убрать подмешанные пропсы. */
10973
+ resetProps(): this;
10974
+ /**
10975
+ * Ref на компонент узла. Создаётся один раз на селектор; рендерер вешает его на компонент.
10976
+ * Компонент обязан принимать `ref`.
10977
+ */
10978
+ getRef<H>(): { current: H | null };
10979
+ /** @internal Селектор узла. */
10980
+ __selector: string;
10981
+ /** @internal Хранилище области. */
10982
+ __overrideMaps: SchemaOverrideMaps;
10983
+ }
10984
+ ```
10985
+
10986
+ _Source: src/form/schema-controller.ts_
10987
+
10988
+ ### SchemaOverrideMaps
10989
+
10990
+ **Kind:** `interface`
10991
+
10992
+ Хранилище переопределений одной области схемы. Ключ каждой карты — `selector` узла.
10993
+
10994
+ **Signature:**
10995
+ ```typescript
10996
+ export interface SchemaOverrideMaps {
10997
+ hiddenOverrides: Map<string, boolean | null>;
10998
+ propsOverrides: Map<string, Record<string, unknown> | null>;
10999
+ refRegistry: Map<string, { current: any }>;
11000
+ /** Условия скрытия узлов: selector → реактивное условие. */
11001
+ conditionRegistry: Map<string, () => boolean>;
11002
+ /** Реактивные эффекты, живущие, пока область смонтирована (`renderEffect`). */
11003
+ effectRegistry: Array<() => void | (() => void)>;
11004
+ /** Обработчики событий компонентов: selector → { имя пропа → обработчик }. */
11005
+ callbackRegistry: Map<string, Map<string, (...args: any[]) => any>>;
11006
+ /** Хуки жизненного цикла узла: selector → { onMount, onUnmount }. */
11007
+ lifecycleRegistry: Map<string, NodeLifecycleHooks>;
11008
+ /** Общий счётчик изменений области. Узлы на него не подписываются — см. `versionFor`. */
11009
+ version: Signal<number>;
11010
+ /**
11011
+ * Версия-сигнал одного селектора (создаётся лениво). Узел подписывается только на сигнал СВОЕГО
11012
+ * селектора, поэтому `setHidden` / `patchProps` уведомляют один узел, а не всё дерево.
11013
+ */
11014
+ versionFor(selector: string): Signal<number>;
11015
+ }
11016
+ ```
11017
+
11018
+ _Source: src/form/schema-controller.ts_
11019
+
11020
+ ### SchemaResolver
11021
+
11022
+ **Kind:** `interface`
11023
+
11024
+ Реестр, умеющий превратить документ схемы в дерево. Метод необязателен: у интерфейса реестра
11025
+ есть сторонние реализации, и ядро документ само не разбирает.
11026
+
11027
+ **Signature:**
11028
+ ```typescript
11029
+ export interface SchemaResolver {
11030
+ resolveSchema?(document: unknown, model: FormModel<never>): ResolvedSchema;
11031
+ }
11032
+ ```
11033
+
11034
+ _Source: src/form/form-bundle.ts_
11035
+
11036
+ ### SchemaScope
11037
+
11038
+ **Kind:** `interface`
11039
+
11040
+ Схема одной области: адресует узлы по `selector`.
11041
+
11042
+ **Signature:**
11043
+ ```typescript
11044
+ export interface SchemaScope {
11045
+ /** Управление узлом области по его `selector`. */
11046
+ node(selector: string): SchemaNodeControl;
11047
+ /** @internal Хранилище области — его читает рендерер. */
11048
+ __overrideMaps: SchemaOverrideMaps;
11049
+ }
11050
+ ```
11051
+
11052
+ _Source: src/form/schema-controller.ts_
11053
+
11054
+ ### schemaSubtree
11055
+
11056
+ **Kind:** `function`
11057
+
11058
+ Поддерево схемы для под-модели — результат `builder(subModel)`, один на пару.
11059
+
11060
+ **Signature:**
11061
+ ```typescript
11062
+ export function schemaSubtree<N>(builder: (subModel: any) => N, subModel: object): N
11063
+ ```
11064
+
11065
+ **Parameters:**
11066
+ - `builder` — - Билдер поддерева: `item` узла-массива или `part` узла-подформы.
11067
+ - `subModel` — - Под-модель строки массива или группы.
11068
+
11069
+ **Returns:** Узел поддерева; повторный вызов с той же парой отдаёт тот же объект.
11070
+
11071
+ **Examples:**
11072
+
11073
+ ```typescript
11074
+ const row = model.items.at(0);
11075
+ schemaSubtree(itemSchema, row) === schemaSubtree(itemSchema, row); // true
11076
+ ```
11077
+
11078
+ _Source: src/form/schema-subtree.ts_
11079
+
10107
11080
  ### SetValueOptions
10108
11081
 
10109
11082
  **Kind:** `interface`
@@ -10332,6 +11305,36 @@ export type UnknownRecord = Record<string, unknown>;
10332
11305
 
10333
11306
  _Source: src/form/types/index.ts_
10334
11307
 
11308
+ ### unknownSchemaSelectors
11309
+
11310
+ **Kind:** `function`
11311
+
11312
+ Селекторы, на которые в области записаны правила, но которых нет в её дереве. Такое правило
11313
+ ничего не делает — сборка и рендерер в dev предупреждают о нём.
11314
+
11315
+ Ref в проверку не входит: его разрешено брать по абсолютному пути модели
11316
+ (`phones.0.number`), а такой строки при сборке может ещё не быть.
11317
+
11318
+ **Signature:**
11319
+ ```typescript
11320
+ export function unknownSchemaSelectors(maps: SchemaOverrideMaps, tree: unknown): string[]
11321
+ ```
11322
+
11323
+ **Parameters:**
11324
+ - `maps` — - Хранилище области.
11325
+ - `tree` — - Дерево (поддерево) области.
11326
+
11327
+ **Returns:** Селекторы с правилами, не найденные в дереве.
11328
+
11329
+ **Examples:**
11330
+
11331
+ ```typescript
11332
+ const scope = controller.scopeOf(model);
11333
+ unknownSchemaSelectors(scope.__overrideMaps, tree); // ['mortgage'] — узла с таким selector нет
11334
+ ```
11335
+
11336
+ _Source: src/form/schema-controller.ts_
11337
+
10335
11338
  ### unmarkDerived
10336
11339
 
10337
11340
  **Kind:** `function`
@@ -10519,6 +11522,29 @@ const credit = useFormBundle(() => createCoreForm<CreditForm>({ model: createCre
10519
11522
 
10520
11523
  _Source: src/platforms/react/hooks/use-form-bundle.ts_
10521
11524
 
11525
+ ### useFormBundleContext
11526
+
11527
+ **Kind:** `function`
11528
+
11529
+ Бандл сборки, внутри которой отрисован компонент; `null` — компонент стоит вне рендерера
11530
+ (например, визард в разметке React-руками получает форму и валидацию пропсами).
11531
+
11532
+ **Signature:**
11533
+ ```typescript
11534
+ export function useFormBundleContext<T = unknown>(): FormBundle<T> | null
11535
+ ```
11536
+
11537
+ **Examples:**
11538
+
11539
+ ```tsx
11540
+ function StepFooter() {
11541
+ const bundle = useFormBundleContext<CreditForm>();
11542
+ return <button onClick={() => bundle?.validation?.validateAll()}>Проверить</button>;
11543
+ }
11544
+ ```
11545
+
11546
+ _Source: src/platforms/react/form-bundle-context.ts_
11547
+
10522
11548
  ### useFormControl
10523
11549
 
10524
11550
  **Kind:** `function`