@reformer/core 7.0.0 → 9.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/factories/node-factory.d.ts +5 -4
- package/dist/core/model/types.d.ts +41 -12
- package/dist/core/model/validate-model.d.ts +8 -4
- package/dist/core/nodes/field-node.d.ts +2 -2
- package/dist/core/nodes/group-node.d.ts +7 -1
- package/dist/core/nodes/model-array-node.d.ts +5 -0
- package/dist/core/types/deep-schema.d.ts +16 -4
- package/dist/core/types/form-proxy.d.ts +1 -1
- package/dist/core/types/index.d.ts +4 -10
- package/dist/core/types/schema-node.d.ts +79 -0
- package/dist/core/types/validation-schema.d.ts +22 -11
- package/dist/core/utils/create-form.d.ts +4 -3
- package/dist/core/utils/error-handler.d.ts +4 -1
- package/dist/core/utils/form-submitter.d.ts +2 -3
- package/dist/core/utils/type-guards.d.ts +3 -3
- package/dist/core/validation/validators/date-utils.d.ts +5 -1
- package/dist/core/validation/validators/integer.d.ts +1 -1
- package/dist/core/validation/validators/is-number.d.ts +1 -1
- package/dist/core/validation/validators/max.d.ts +1 -1
- package/dist/core/validation/validators/min.d.ts +1 -1
- package/dist/core/validation/validators/multiple-of.d.ts +1 -1
- package/dist/core/validation/validators/non-negative.d.ts +1 -1
- package/dist/core/validation/validators/non-zero.d.ts +1 -1
- package/dist/{date-utils-xUWFslTj.js → date-utils-dTT_x_Be.js} +12 -10
- package/dist/hooks/types.d.ts +1 -1
- package/dist/hooks/useArrayLength.d.ts +1 -2
- package/dist/hooks/useFormControl.d.ts +2 -2
- package/dist/index.js +514 -514
- package/dist/signals.d.ts +13 -0
- package/dist/signals.js +9 -0
- package/dist/validators/future-date.js +1 -1
- package/dist/validators/is-date.js +1 -1
- package/dist/validators/max-age.js +1 -1
- package/dist/validators/max-date.js +1 -1
- package/dist/validators/min-age.js +1 -1
- package/dist/validators/min-date.js +1 -1
- package/dist/validators/multiple-of.js +11 -7
- package/dist/validators/past-date.js +1 -1
- package/llms.txt +586 -164
- package/package.json +7 -3
package/llms.txt
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- 15-project-structure.md — project-structure
|
|
23
23
|
- 16-ui-components.md — ui-components
|
|
24
24
|
- 17-nonexistent-api.md — nonexistent-api
|
|
25
|
+
- 18-conditional-fields.md — Условные поля — видимость, доступность и валидация
|
|
25
26
|
- 19-reading-values.md — reading-values
|
|
26
27
|
- 20-compute-vs-watch.md — compute-vs-watch
|
|
27
28
|
- 21-array-operations.md — array-operations
|
|
@@ -888,6 +889,10 @@ UI-конфиг (`component`/`componentProps`) + валидаторы. Знач
|
|
|
888
889
|
}
|
|
889
890
|
```
|
|
890
891
|
|
|
892
|
+
> `disabled` — **top-level поле узла** (начальное состояние); дальше управляется методами
|
|
893
|
+
> `form.field.disable()`/`enable()` и оператором `enableWhen`. `componentProps.disabled` — это
|
|
894
|
+
> UI-проп и на состояние узла **не влияет** (частая причина «computed-поле остаётся редактируемым»).
|
|
895
|
+
|
|
891
896
|
### Primitive Fields
|
|
892
897
|
|
|
893
898
|
```typescript
|
|
@@ -983,6 +988,11 @@ form.address.city.value.value; // текущее значение (через
|
|
|
983
988
|
model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели
|
|
984
989
|
```
|
|
985
990
|
|
|
991
|
+
> **Тип `schema` в M1 — `unknown`, не `FormSchema<T>`.** Это осознанно: интерпретатор обходит
|
|
992
|
+
> произвольную структуру и собирает листья `{ value: signal, component?, ... }`, поэтому обёртки
|
|
993
|
+
> (`{ children: [...] }`, wizard/steps) компилируются. Строгий `FormSchema<T>` (keyed-map по полям
|
|
994
|
+
> `T`) применяется только на legacy-пути `createForm(schema)` без модели.
|
|
995
|
+
|
|
986
996
|
### createForm Returns a Proxy
|
|
987
997
|
|
|
988
998
|
```typescript
|
|
@@ -1031,6 +1041,39 @@ const form = createForm<MyForm>({ model, schema });
|
|
|
1031
1041
|
> он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
|
|
1032
1042
|
> См. `30-type-safety-recipes.md`.
|
|
1033
1043
|
|
|
1044
|
+
### Один массив — три слоя (три разных движка)
|
|
1045
|
+
|
|
1046
|
+
Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
|
|
1047
|
+
перепутать, но каждая корректна только в своём контексте:
|
|
1048
|
+
|
|
1049
|
+
1. **Layout-схема `createForm`** — единственная форма, которую ест `createForm`:
|
|
1050
|
+
`{ array: model.<path>, item: (itemModel) => subSchema }`. Массив связывается через
|
|
1051
|
+
**value-proxy** `model.properties` (он несёт `__path`), а **не** через сигнальный
|
|
1052
|
+
`model.$.properties`.
|
|
1053
|
+
|
|
1054
|
+
```typescript
|
|
1055
|
+
// узел схемы для createForm({ model, schema })
|
|
1056
|
+
properties: { array: model.properties, item: propertyItem },
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
2. **Validation-схема `validateFormModel`** — секция массива описывается через `componentProps`:
|
|
1060
|
+
`{ componentProps: { control: model.<array>, itemComponent: (item) => subSchema } }`. Движок
|
|
1061
|
+
узнаёт секцию по `componentProps.itemComponent` + `control` и обходит её per-item.
|
|
1062
|
+
|
|
1063
|
+
```typescript
|
|
1064
|
+
// узел схемы для validateFormModel(model, schema)
|
|
1065
|
+
{ componentProps: { control: model.properties, itemComponent: propertyRules } }
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`ModelArrayNode`),
|
|
1069
|
+
а не со схемой: `<FormArray.Root control={form.properties}>` (CDK) или `FormArraySection` из
|
|
1070
|
+
`@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).
|
|
1071
|
+
|
|
1072
|
+
> **Не путай форму по движку.** `createForm` принимает **только** `{ array, item }`;
|
|
1073
|
+
> `componentProps.{ control, itemComponent }` — это форма validation-схемы; `FormArray.Root
|
|
1074
|
+
> control={form.x}` (или `FormArraySection`) — рендер. `control`/`itemComponent` в layout-схеме
|
|
1075
|
+
> `createForm` не подхватятся, а `{ array, item }` схема валидации не обходит.
|
|
1076
|
+
|
|
1034
1077
|
### Array operations — на модели
|
|
1035
1078
|
|
|
1036
1079
|
Мутации массива делаются через `ModelArray` (`model.items`), а не через ноду формы:
|
|
@@ -1051,6 +1094,16 @@ model.items.map((item, i) => item.name); // item — FormModel<Item>
|
|
|
1051
1094
|
> (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
|
|
1052
1095
|
> берутся из `item`-фабрики схемы автоматически.
|
|
1053
1096
|
|
|
1097
|
+
> **Очистка массива в behavior.** Тот же `model.<array>.clear()` (см. список операций выше) —
|
|
1098
|
+
> способ очистить коллекцию **вне React**, из behavior: он мутирует модель напрямую, минуя ноды
|
|
1099
|
+
> формы. Типичный случай — сбросить массив при выключении флага:
|
|
1100
|
+
>
|
|
1101
|
+
> ```typescript
|
|
1102
|
+
> onChange(model.$.hasProperty, (on) => {
|
|
1103
|
+
> if (!on) model.properties.clear();
|
|
1104
|
+
> });
|
|
1105
|
+
> ```
|
|
1106
|
+
|
|
1054
1107
|
### Rendering Arrays
|
|
1055
1108
|
|
|
1056
1109
|
Каждый элемент массива — под-форма (`FormProxy<Item>`). Итерируй через `form.items.map`:
|
|
@@ -1103,7 +1156,12 @@ const uniqueNames: ModelValidator<unknown, unknown, MyForm> = (_value, _scope, r
|
|
|
1103
1156
|
};
|
|
1104
1157
|
```
|
|
1105
1158
|
|
|
1106
|
-
## 11.
|
|
1159
|
+
## 11. See also
|
|
1160
|
+
|
|
1161
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item обход `validateFormModel`
|
|
1162
|
+
- CDK / ui-kit form-array (`FormArray.Root`, `FormArraySection`) — `find_recipe(topic="form-array")`
|
|
1163
|
+
|
|
1164
|
+
## 12. 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)
|
|
1107
1165
|
|
|
1108
1166
|
Для async-реакции на изменение поля (загрузка зависимых опций, справочников) используй
|
|
1109
1167
|
`onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно
|
|
@@ -1157,7 +1215,7 @@ const stop = watchField(model.$.country, (country) => {
|
|
|
1157
1215
|
// stop() — отписаться
|
|
1158
1216
|
```
|
|
1159
1217
|
|
|
1160
|
-
##
|
|
1218
|
+
## 13. 11. ARRAY CLEANUP PATTERN
|
|
1161
1219
|
|
|
1162
1220
|
Очистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на
|
|
1163
1221
|
модели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.
|
|
@@ -1189,7 +1247,7 @@ function clearWhenOff(flag: ReadonlySignal<boolean>, array: { clear(): void }):
|
|
|
1189
1247
|
clearWhenOff(model.$.hasProperty, model.properties);
|
|
1190
1248
|
```
|
|
1191
1249
|
|
|
1192
|
-
##
|
|
1250
|
+
## 14. 12. MULTI-STEP FORM VALIDATION
|
|
1193
1251
|
|
|
1194
1252
|
Каждый шаг — своя под-схема валидации (дерево узлов `{ children: [...] }`). Переход к
|
|
1195
1253
|
следующему шагу проверяется `validateFormModel(model, stepSchema)`; полный submit — по
|
|
@@ -1266,11 +1324,7 @@ function MultiStepForm() {
|
|
|
1266
1324
|
}
|
|
1267
1325
|
```
|
|
1268
1326
|
|
|
1269
|
-
|
|
1270
|
-
> собранные фабрикой `makeCreditValidationConfig(model)` поверх `validateFormModel`. См.
|
|
1271
|
-
> `complex-multy-step-form/schemas/validation.ts`.
|
|
1272
|
-
|
|
1273
|
-
## 14. 13. EXTENDED COMMON MISTAKES
|
|
1327
|
+
## 15. 13. EXTENDED COMMON MISTAKES
|
|
1274
1328
|
|
|
1275
1329
|
### Reuse via apply / applyEach — не дублируй код
|
|
1276
1330
|
|
|
@@ -1352,30 +1406,37 @@ revalidateWhen([model.$.field2], () => validateFormModel(model, schema));
|
|
|
1352
1406
|
> `validateGroup`/`validateTree`/`validateForm`, типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn`
|
|
1353
1407
|
> и `ctx.form.*`/`ctx.setFieldValue` — из старой path-based архитектуры и УДАЛЕНЫ. См. `17-nonexistent-api.md`.
|
|
1354
1408
|
|
|
1355
|
-
##
|
|
1409
|
+
## 16. 14. PROJECT STRUCTURE (COLOCATION)
|
|
1410
|
+
|
|
1411
|
+
One form is one module folder. **Default layout — flat minimalist:** every concern is a single
|
|
1412
|
+
file at the module root, and the whole form (entry + all wizard steps inline) lives in one
|
|
1413
|
+
`index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
|
|
1414
|
+
carried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a
|
|
1415
|
+
render-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add
|
|
1416
|
+
`renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`
|
|
1417
|
+
nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
|
|
1418
|
+
and rendered with `FormArraySection` — no per-step component files.
|
|
1356
1419
|
|
|
1357
1420
|
```
|
|
1358
1421
|
src/
|
|
1359
|
-
├── components/ui/
|
|
1360
|
-
│ ├── FormField.tsx
|
|
1361
|
-
│ └── FormArraySection.tsx
|
|
1422
|
+
├── components/ui/ # App-wide reusable UI (FormField, FormArraySection, ...)
|
|
1362
1423
|
│
|
|
1363
1424
|
├── forms/
|
|
1364
|
-
│ └── [form-name]/
|
|
1365
|
-
│ ├──
|
|
1366
|
-
│ ├──
|
|
1367
|
-
│ ├──
|
|
1368
|
-
│ ├──
|
|
1369
|
-
│ ├── behavior.ts
|
|
1370
|
-
│ ├──
|
|
1371
|
-
│
|
|
1372
|
-
│
|
|
1373
|
-
│ │ └── loan-info/
|
|
1374
|
-
│ │ └── LoanInfoForm.tsx
|
|
1375
|
-
│ │
|
|
1376
|
-
│ └── nested-forms/ # Reusable sub-forms (Address, PersonalData, ...)
|
|
1425
|
+
│ └── [form-name]/ # Form module — flat, one file per concern
|
|
1426
|
+
│ ├── index.tsx # entry + whole form: createModel→createForm→<FormWizard> with all steps inline; arrays via FormArraySection
|
|
1427
|
+
│ ├── types.ts # form type + enums + { value, label } option type + constant dictionaries
|
|
1428
|
+
│ ├── model.ts # createModel + initial values + empty-array-element factories
|
|
1429
|
+
│ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
|
|
1430
|
+
│ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
|
|
1431
|
+
│ ├── validation.ts # ALL validation → { validateStep, validateAll }
|
|
1432
|
+
│ ├── data-sources.ts # options + async loaders (dataSources)
|
|
1433
|
+
│ └── api.ts # submit + prefill/load
|
|
1377
1434
|
```
|
|
1378
1435
|
|
|
1436
|
+
Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
|
|
1437
|
+
`index.tsx`; validation is a single `validation.ts`.** This is the working default for
|
|
1438
|
+
almost every form — reach for folders only when a file stops fitting on a screen.
|
|
1439
|
+
|
|
1379
1440
|
### Key Files
|
|
1380
1441
|
|
|
1381
1442
|
```typescript
|
|
@@ -1391,7 +1452,7 @@ import { createModel, type FormModel } from '@reformer/core';
|
|
|
1391
1452
|
export const createCreditApplicationModel = (): FormModel<CreditApplicationForm> =>
|
|
1392
1453
|
createModel<CreditApplicationForm>(createInitialCreditApplication());
|
|
1393
1454
|
|
|
1394
|
-
// forms/credit-application/schema.ts
|
|
1455
|
+
// forms/credit-application/form.schema.ts
|
|
1395
1456
|
import type { FormModel } from '@reformer/core';
|
|
1396
1457
|
export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
|
|
1397
1458
|
loanType: { value: model.$.loanType, component: Select, componentProps: { /* ... */ } },
|
|
@@ -1399,14 +1460,14 @@ export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>)
|
|
|
1399
1460
|
properties: { array: model.properties, item: propertyItem },
|
|
1400
1461
|
});
|
|
1401
1462
|
|
|
1402
|
-
// forms/credit-application/behavior.ts
|
|
1463
|
+
// forms/credit-application/form.behavior.ts
|
|
1403
1464
|
import { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';
|
|
1404
1465
|
export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(({ model }) => {
|
|
1405
1466
|
compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
|
|
1406
1467
|
enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
|
|
1407
1468
|
});
|
|
1408
1469
|
|
|
1409
|
-
// forms/credit-application/
|
|
1470
|
+
// forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline
|
|
1410
1471
|
import { createForm } from '@reformer/core';
|
|
1411
1472
|
export const createCreditApplicationForm = () => {
|
|
1412
1473
|
const model = createCreditApplicationModel();
|
|
@@ -1414,15 +1475,64 @@ export const createCreditApplicationForm = () => {
|
|
|
1414
1475
|
};
|
|
1415
1476
|
```
|
|
1416
1477
|
|
|
1478
|
+
### Scaling up: folders (large forms)
|
|
1479
|
+
|
|
1480
|
+
When the flat module gets unwieldy — steps you want in their own files, sub-forms reused across
|
|
1481
|
+
steps, calc/validators that outgrow one file — promote it to a three-folder module: `lib/`
|
|
1482
|
+
(domain raw material), `schema/` (the form definition), `components/` (React layout), plus the
|
|
1483
|
+
entry component and `index.ts`. Keep the module root to just the entry + `index.ts`; everything
|
|
1484
|
+
else lives in a folder.
|
|
1485
|
+
|
|
1486
|
+
```
|
|
1487
|
+
forms/
|
|
1488
|
+
└── [form-name]/ # Form module — folders (scale-up)
|
|
1489
|
+
├── [FormName]Form.tsx # Entry: builds model+form, renders FormWizard + step components
|
|
1490
|
+
├── index.ts # Public re-exports of the module
|
|
1491
|
+
│
|
|
1492
|
+
├── lib/ # Domain helpers (target-agnostic)
|
|
1493
|
+
│ ├── types.ts # Form interface + field enums + { value, label } option type
|
|
1494
|
+
│ ├── constants.ts # Option dictionaries (LOAN_TYPES, GENDERS, ...)
|
|
1495
|
+
│ ├── calc.ts # Pure fns for derived fields (age, monthlyPayment, ...)
|
|
1496
|
+
│ ├── custom-validators.ts # Reusable validator factories
|
|
1497
|
+
│ └── api.ts # Data sources + submit
|
|
1498
|
+
│
|
|
1499
|
+
├── schema/ # The form definition
|
|
1500
|
+
│ ├── model.ts # createModel factory + initial values + array-element factories
|
|
1501
|
+
│ ├── schema.ts # createForm schema tree ({ value: model.$.x, component })
|
|
1502
|
+
│ ├── validation.ts # validation schema + validateFormModel config
|
|
1503
|
+
│ ├── behavior.ts # defineFormBehavior(...)
|
|
1504
|
+
│ └── create-form.ts # Assembly: createForm({ model, schema, behavior })
|
|
1505
|
+
│
|
|
1506
|
+
└── components/
|
|
1507
|
+
├── steps/ # One component per wizard step
|
|
1508
|
+
├── nested-forms/ # Reusable sub-forms (Address, PersonalData, ...)
|
|
1509
|
+
└── ui/ # Form-specific helper blocks (summary, warnings, sections)
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
|
|
1513
|
+
`schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
|
|
1514
|
+
may co-locate each step's `schema.ts` / `validation.ts` / `behavior.ts` inside its
|
|
1515
|
+
`steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
|
|
1516
|
+
the guide below.
|
|
1517
|
+
|
|
1417
1518
|
### Scaling
|
|
1418
1519
|
|
|
1419
|
-
| Complexity | Structure
|
|
1420
|
-
| ---------- |
|
|
1421
|
-
| Simple
|
|
1422
|
-
|
|
|
1423
|
-
|
|
|
1520
|
+
| Complexity | Structure |
|
|
1521
|
+
| ---------- | --------------------------------------------------------------------------------------- |
|
|
1522
|
+
| Simple | Single file: `index.tsx` (model + schema + behavior + component) |
|
|
1523
|
+
| **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |
|
|
1524
|
+
| Folders (`lib/` + `schema/` + `components/`) | Large forms: split concerns into folders, one component per step, reusable `nested-forms/` |
|
|
1525
|
+
|
|
1526
|
+
> The leading layout is configurable: set `REFORMER_FORM_LAYOUT` (`minimalist` | `folders`)
|
|
1527
|
+
> when registering the MCP server to choose which structure the generators lead with. Default
|
|
1528
|
+
> is `minimalist`.
|
|
1529
|
+
|
|
1530
|
+
> Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
|
|
1531
|
+
> renderer-json `renderer.schema.json` + `renderer.behavior.ts` + `registry.ts`), the
|
|
1532
|
+
> centralized-vs-co-located choice, and the full reuse map live in the
|
|
1533
|
+
> **form-directory-layout** guide (`@reformer/mcp`).
|
|
1424
1534
|
|
|
1425
|
-
##
|
|
1535
|
+
## 17. 14.5 UI COMPONENT PATTERNS
|
|
1426
1536
|
|
|
1427
1537
|
> **Default rule (read first)**: для UI используй `FormField` из
|
|
1428
1538
|
> [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
|
|
@@ -1607,7 +1717,7 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
|
|
|
1607
1717
|
}
|
|
1608
1718
|
```
|
|
1609
1719
|
|
|
1610
|
-
##
|
|
1720
|
+
## 18. 15. NON-EXISTENT API (DO NOT USE)
|
|
1611
1721
|
|
|
1612
1722
|
**Следующего API НЕТ в @reformer/core** (частично — наследие старой path-based архитектуры,
|
|
1613
1723
|
удалено при переходе на M1):
|
|
@@ -1671,7 +1781,166 @@ enableWhen(path.city, (form) => Boolean(form.country));
|
|
|
1671
1781
|
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
|
|
1672
1782
|
```
|
|
1673
1783
|
|
|
1674
|
-
##
|
|
1784
|
+
## 19. Purpose
|
|
1785
|
+
|
|
1786
|
+
«Показать поле X только если Y» под M1 решается тремя независимыми механизмами — выбирай по
|
|
1787
|
+
тому, что именно должно быть условным:
|
|
1788
|
+
|
|
1789
|
+
1. **Видимость / доступность** (поле остаётся в модели, но выключается/включается и не валидируется)
|
|
1790
|
+
— behaviors: `enableWhen`, а для условной записи значения — `compute` / `copyFrom` с опцией `{ when }`.
|
|
1791
|
+
2. **Условная валидация** (правила применяются только в истинной ветке) — нативный branch-узел
|
|
1792
|
+
схемы `{ when, children }`, исполняется `validateFormModel`.
|
|
1793
|
+
3. **Скрытие из разметки** (узел вообще не рендерится) — `useFormControlValue` + условный рендер в JSX.
|
|
1794
|
+
|
|
1795
|
+
> Ни `applyWhen`, ни `ValidationSchemaFn` **не экспортируются** из `@reformer/core` — см. раздел
|
|
1796
|
+
> [Не экспорт (частая ошибка)](#не-экспорт-частая-ошибка) и `17-nonexistent-api.md`.
|
|
1797
|
+
|
|
1798
|
+
## 20. Видимость и доступность
|
|
1799
|
+
|
|
1800
|
+
`enableWhen(target, () => condition, { resetOnDisable })` из `@reformer/core/behaviors`
|
|
1801
|
+
включает/выключает поле реактивно. При `disabled` поле не участвует в валидации; с
|
|
1802
|
+
`resetOnDisable: true` его значение сбрасывается при выключении (иначе — сохраняется).
|
|
1803
|
+
|
|
1804
|
+
```typescript
|
|
1805
|
+
import { defineFormBehavior, enableWhen, compute, copyFrom } from '@reformer/core/behaviors';
|
|
1806
|
+
|
|
1807
|
+
type CreditForm = {
|
|
1808
|
+
loanType: 'mortgage' | 'car' | 'consumer';
|
|
1809
|
+
propertyValue: number | null;
|
|
1810
|
+
initialPayment: number | null;
|
|
1811
|
+
sameEmail: boolean;
|
|
1812
|
+
email: string;
|
|
1813
|
+
emailAdditional: string;
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
|
|
1817
|
+
// один или несколько таргетов; условие читает model.* реактивно
|
|
1818
|
+
enableWhen(
|
|
1819
|
+
[model.$.propertyValue, model.$.initialPayment],
|
|
1820
|
+
() => model.loanType === 'mortgage',
|
|
1821
|
+
{ resetOnDisable: true },
|
|
1822
|
+
);
|
|
1823
|
+
|
|
1824
|
+
// условная ЗАПИСЬ производного значения — compute с { when }
|
|
1825
|
+
compute(model.$.initialPayment, () => Math.round((model.propertyValue ?? 0) * 0.2), {
|
|
1826
|
+
when: () => model.loanType === 'mortgage',
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
// условное КОПИРОВАНИЕ из другого поля — copyFrom с { when }
|
|
1830
|
+
copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true });
|
|
1831
|
+
});
|
|
1832
|
+
```
|
|
1833
|
+
|
|
1834
|
+
Требования:
|
|
1835
|
+
|
|
1836
|
+
- **Все поля должны быть материализованы в модели** (`createModel`), даже те, что показываются
|
|
1837
|
+
только под условием. `enableWhen`/`compute`/`copyFrom` принимают сигналы `model.$.field`, а не
|
|
1838
|
+
строковые пути; сигнала для несуществующего поля нет.
|
|
1839
|
+
- Поведение подключается к форме через `createForm({ model, schema, behavior })` — форма владеет
|
|
1840
|
+
жизненным циклом, DSL-операторы регистрируют свой cleanup сами (ручной массив cleanup'ов не нужен).
|
|
1841
|
+
- Таргетом может быть и **группа**: `enableWhen(model.$.residenceAddress, () => model.sameAsRegistration === false)`
|
|
1842
|
+
проходит по поддереву; без `resetOnDisable` значение группы сохраняется (удобно, когда оно копируется
|
|
1843
|
+
из другого источника).
|
|
1844
|
+
|
|
1845
|
+
> **Групповой таргет работает только у DSL-`enableWhen` из `@reformer/core/behaviors`**, не у
|
|
1846
|
+
> одноимённого корневого `enableWhen` из `@reformer/core` (тот резолвит только leaf-сигналы через
|
|
1847
|
+
> реестр → на group-сигнале это тихий no-op). `get_symbol_docs("enableWhen")` возвращает именно
|
|
1848
|
+
> корневой, не group-capable вариант.
|
|
1849
|
+
|
|
1850
|
+
## 21. Условная валидация
|
|
1851
|
+
|
|
1852
|
+
Правила, действующие только при выполнении условия, задаются **нативным branch-узлом**
|
|
1853
|
+
`{ when: (scope, root) => boolean, children: [...] }` прямо в дереве схемы. `validateFormModel`
|
|
1854
|
+
обходит дерево: при истинном `when` валидирует `children`, при ложном — **поддерево пропускается,
|
|
1855
|
+
а ошибки его полей очищаются** (движок вызывает `setErrors([])`). `scope` — ближайшая под-модель
|
|
1856
|
+
(элемент массива или корень), `root` — корневая модель формы.
|
|
1857
|
+
|
|
1858
|
+
```typescript
|
|
1859
|
+
import { validateFormModel } from '@reformer/core';
|
|
1860
|
+
import { required, min } from '@reformer/core/validators';
|
|
1861
|
+
|
|
1862
|
+
const schema = {
|
|
1863
|
+
children: [
|
|
1864
|
+
{ value: model.$.loanType, validators: [required()] },
|
|
1865
|
+
{
|
|
1866
|
+
// ветка применяется только для ипотеки
|
|
1867
|
+
when: (_scope, root) => root.loanType === 'mortgage',
|
|
1868
|
+
children: [
|
|
1869
|
+
{ value: model.$.propertyValue, validators: [required(), min(1_000_000)] },
|
|
1870
|
+
{ value: model.$.initialPayment, validators: [required()] },
|
|
1871
|
+
],
|
|
1872
|
+
},
|
|
1873
|
+
],
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
const { valid, errors } = await validateFormModel(model, schema);
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
Переключение `loanType` с `mortgage` на другой тип автоматически снимает ошибки `required` с
|
|
1880
|
+
`propertyValue`/`initialPayment` — их держать enabled и не валидировать помогает `enableWhen` выше.
|
|
1881
|
+
|
|
1882
|
+
## 22. Скрытие в JSX
|
|
1883
|
+
|
|
1884
|
+
Чтобы условный блок вообще не рендерился, читай значение-триггер хуком `useFormControlValue(form.field)`
|
|
1885
|
+
и делай ранний `return null`. Хук возвращает **только значение** (без деструктуризации `{ value }`).
|
|
1886
|
+
|
|
1887
|
+
```tsx
|
|
1888
|
+
import { useFormControlValue } from '@reformer/core';
|
|
1889
|
+
|
|
1890
|
+
function MortgageFields({ form }: { form: CreditForm }) {
|
|
1891
|
+
const loanType = useFormControlValue(form.loanType);
|
|
1892
|
+
if (loanType !== 'mortgage') return null;
|
|
1893
|
+
|
|
1894
|
+
return (
|
|
1895
|
+
<>
|
|
1896
|
+
<PropertyValueField form={form} />
|
|
1897
|
+
<InitialPaymentField form={form} />
|
|
1898
|
+
</>
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
```
|
|
1902
|
+
|
|
1903
|
+
Скрытие в JSX — чисто визуальное: если поле должно ещё и **выпадать из валидации/состояния**,
|
|
1904
|
+
сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или branch-узлом (валидация).
|
|
1905
|
+
|
|
1906
|
+
## 23. Не экспорт (частая ошибка)
|
|
1907
|
+
|
|
1908
|
+
- **`applyWhen` — это ЛОКАЛЬНЫЙ typed-хелпер примера, а не экспорт `@reformer/core`.** Он лишь
|
|
1909
|
+
эмитит нативный branch-узел `{ when, children }`. Каноничное определение (см.
|
|
1910
|
+
`complex-multy-step-form/schemas/validation.ts`):
|
|
1911
|
+
|
|
1912
|
+
```typescript
|
|
1913
|
+
// локальный сахар в самом примере — НЕ импорт из библиотеки
|
|
1914
|
+
const applyWhen = (cond: (form: Root) => boolean, children: SchemaNode[]): SchemaNode => ({
|
|
1915
|
+
when: (_scope: unknown, root: unknown) => cond(root as Root),
|
|
1916
|
+
children,
|
|
1917
|
+
});
|
|
1918
|
+
```
|
|
1919
|
+
|
|
1920
|
+
Публичный API — сам `validateFormModel(model, schema)` и форма узла `{ when, children }`.
|
|
1921
|
+
Не пиши `import { applyWhen } from '@reformer/core'` — такого символа нет.
|
|
1922
|
+
|
|
1923
|
+
- **`ValidationSchemaFn` не существует** (тип старой path-схемы удалён при переходе на M1). Тип
|
|
1924
|
+
кастомного/cross-field валидатора — **`ModelValidator<TValue, TModel, TRoot>`**:
|
|
1925
|
+
|
|
1926
|
+
```typescript
|
|
1927
|
+
import type { ModelValidator } from '@reformer/core';
|
|
1928
|
+
|
|
1929
|
+
// (value, scope, root) — cross-field читает соседей через root
|
|
1930
|
+
const passwordsMatch: ModelValidator<string, unknown, { password: string }> = (value, _scope, root) =>
|
|
1931
|
+
value && root.password && value !== root.password
|
|
1932
|
+
? { code: 'mismatch', message: 'Пароли не совпадают' }
|
|
1933
|
+
: null;
|
|
1934
|
+
```
|
|
1935
|
+
|
|
1936
|
+
## 24. See also
|
|
1937
|
+
|
|
1938
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom`, branch-узел, `ModelValidator`
|
|
1939
|
+
- [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, …)
|
|
1940
|
+
- [25-reset-when.md](./25-reset-when.md) — `resetWhen` как альтернатива, когда поле остаётся enabled
|
|
1941
|
+
- [19-reading-values.md](./19-reading-values.md) — `useFormControlValue` и чтение значений в React
|
|
1942
|
+
|
|
1943
|
+
## 25. 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
|
|
1675
1944
|
|
|
1676
1945
|
Под M1 значения живут в модели. Есть три контекста чтения: value-доступ модели, сигналы, и React-хуки.
|
|
1677
1946
|
|
|
@@ -1730,7 +1999,7 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
|
|
|
1730
1999
|
> цикла нет). Для side-эффектов (загрузка опций, обновление componentProps) — `onChange`.
|
|
1731
2000
|
> `computeFrom`/`copyFrom`/`enableWhen` принимают сигналы (`model.$.x`), НЕ строковые пути.
|
|
1732
2001
|
|
|
1733
|
-
##
|
|
2002
|
+
## 26. 17. COMPUTE vs ONCHANGE
|
|
1734
2003
|
|
|
1735
2004
|
Под M1 behaviors работают на сигналах модели. Есть два способа их писать:
|
|
1736
2005
|
|
|
@@ -1767,6 +2036,44 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
|
|
|
1767
2036
|
});
|
|
1768
2037
|
```
|
|
1769
2038
|
|
|
2039
|
+
> **Не читай `model.get()` внутри `compute`/`when`** — это нереактивный снимок, зависимость не
|
|
2040
|
+
> отследится и пересчёта не будет. Читай поля по отдельности (`model.field` или `model.$.field.value`).
|
|
2041
|
+
> `model.get()` — только вне реактивного контекста (в `onChange`, обработчиках событий).
|
|
2042
|
+
|
|
2043
|
+
### Computed sum over a FormArray
|
|
2044
|
+
|
|
2045
|
+
Агрегат по массиву (сумма доходов созаёмщиков и т.п.) считается **реактивно** через value-proxy
|
|
2046
|
+
массива — `model.<array>.map(...)`:
|
|
2047
|
+
|
|
2048
|
+
```typescript
|
|
2049
|
+
compute(model.$.coBorrowersIncome, () =>
|
|
2050
|
+
model.coBorrowers.map((cb) => cb.monthlyIncome ?? 0).reduce((s, v) => s + v, 0)
|
|
2051
|
+
);
|
|
2052
|
+
```
|
|
2053
|
+
|
|
2054
|
+
Почему реактивно: `.map` читает сигнал самого массива (трекает `push`/`removeAt`/reorder) **и**
|
|
2055
|
+
внутри колбэка читает каждый `cb.<field>` (трекает правки элементов) → пересчёт срабатывает на оба
|
|
2056
|
+
вида изменений.
|
|
2057
|
+
|
|
2058
|
+
Ключевой нюанс — **на каком proxy** живёт `.map`/`.reduce`(-via-`.map`):
|
|
2059
|
+
|
|
2060
|
+
- **value-proxy `model.coBorrowers`** — есть `.map`/`.forEach`/`.at`/индексы (обходит элементы как
|
|
2061
|
+
значения). Именно его читай в `compute`.
|
|
2062
|
+
- **signals-proxy `model.$.coBorrowers`** — только индексный доступ и `.length`, без `.map`. Для
|
|
2063
|
+
агрегата не подходит.
|
|
2064
|
+
|
|
2065
|
+
Если нужен только **счётчик** (пересчёт лишь на изменение длины, без чтения полей элементов) —
|
|
2066
|
+
`model.<array>.map(() => null)` (длина трекается, значения — нет):
|
|
2067
|
+
|
|
2068
|
+
```typescript
|
|
2069
|
+
// зависит только от КОЛИЧЕСТВА элементов, не от их полей
|
|
2070
|
+
compute(model.$.interestRate, () => computeRate(model.properties.map(() => null).length));
|
|
2071
|
+
```
|
|
2072
|
+
|
|
2073
|
+
> **Не агрегируй через `model.get()` / `model.<array>.peek()`** в `compute` — это нереактивный
|
|
2074
|
+
> снапшот массива, зависимость не отследится и сумма не пересчитается. Читай массив только через
|
|
2075
|
+
> value-proxy `model.<array>.map(...)`.
|
|
2076
|
+
|
|
1770
2077
|
### computeFrom — явный список источников
|
|
1771
2078
|
|
|
1772
2079
|
Когда нужен явный контроль зависимостей — `computeFrom(sources, target, fn)`. Значения
|
|
@@ -1831,7 +2138,7 @@ const stop = watchField(model.$.country, () => { model.city = ''; });
|
|
|
1831
2138
|
порядке через реактивный граф (цель одного = источник другого). Расходящиеся взаимные
|
|
1832
2139
|
`compute` без стабилизации бросят понятную ошибку (см. `22-cycle-detection.md`).
|
|
1833
2140
|
|
|
1834
|
-
##
|
|
2141
|
+
## 27. 18. ARRAY OPERATIONS
|
|
1835
2142
|
|
|
1836
2143
|
Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);
|
|
1837
2144
|
рендер — через ноду формы (`form.items.map` / `.at`).
|
|
@@ -1939,7 +2246,7 @@ const percentagesSumTo100: ModelValidator<unknown, unknown, MyForm> = (_v, _s, r
|
|
|
1939
2246
|
};
|
|
1940
2247
|
```
|
|
1941
2248
|
|
|
1942
|
-
##
|
|
2249
|
+
## 28. Как устроено под M1
|
|
1943
2250
|
|
|
1944
2251
|
Behaviors на сигналах уже защищены от типичных циклов:
|
|
1945
2252
|
|
|
@@ -1952,7 +2259,7 @@ Behaviors на сигналах уже защищены от типичных ц
|
|
|
1952
2259
|
Тебе НЕ нужно вручную ставить `{ immediate: false }`, guard'ить `disabled.value` перед
|
|
1953
2260
|
`disable()` или сравнивать значения перед записью — это делается внутри операторов.
|
|
1954
2261
|
|
|
1955
|
-
##
|
|
2262
|
+
## 29. Когда всё-таки возникает «Cycle detected»
|
|
1956
2263
|
|
|
1957
2264
|
### 1. Расходящийся взаимный compute
|
|
1958
2265
|
|
|
@@ -1999,7 +2306,7 @@ resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
|
|
|
1999
2306
|
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
2000
2307
|
```
|
|
2001
2308
|
|
|
2002
|
-
##
|
|
2309
|
+
## 30. Prefer built-in operators over manual logic
|
|
2003
2310
|
|
|
2004
2311
|
Вместо ручной сборки reset-on-disable через `onChange` — используй `enableWhen({ resetOnDisable })`:
|
|
2005
2312
|
|
|
@@ -2017,21 +2324,21 @@ enableWhen([model.$.propertyValue, model.$.initialPayment], () => model.loanType
|
|
|
2017
2324
|
});
|
|
2018
2325
|
```
|
|
2019
2326
|
|
|
2020
|
-
##
|
|
2327
|
+
## 31. Key Rules
|
|
2021
2328
|
|
|
2022
2329
|
1. Производные значения — через `compute`/`computeFrom` (peek-guard встроен), не ручным `onChange` + запись.
|
|
2023
2330
|
2. `transformValue`-трансформер должен быть идемпотентным (`f(f(x)) === f(x)`).
|
|
2024
2331
|
3. Условие `resetWhen`/`copyFrom`/`enableWhen` НЕ должно читать собственную цель.
|
|
2025
2332
|
4. Расходящийся взаимный `compute` — разорви однонаправленной зависимостью или `when`.
|
|
2026
2333
|
|
|
2027
|
-
##
|
|
2334
|
+
## 32. Purpose
|
|
2028
2335
|
|
|
2029
2336
|
`copyFrom` декларативно копирует значение одного поля (или группы) в другое при выполнении
|
|
2030
2337
|
условия `when`. Используется для UX-сценариев «совпадает с …»: «адрес проживания = адрес
|
|
2031
2338
|
регистрации», «email для уведомлений = основной email», «billing = shipping». Оператор сам
|
|
2032
2339
|
выходит из reactive-контекста (`runOutsideEffect`), поэтому не порождает «Cycle detected».
|
|
2033
2340
|
|
|
2034
|
-
##
|
|
2341
|
+
## 33. API
|
|
2035
2342
|
|
|
2036
2343
|
Есть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):
|
|
2037
2344
|
|
|
@@ -2056,7 +2363,7 @@ function copyFrom<T>(
|
|
|
2056
2363
|
`when` — реактивное условие (читает сигналы модели через `model.*`). `transform` применяется
|
|
2057
2364
|
к значению перед записью в target. Отдельной опции `fields`/`debounce` нет.
|
|
2058
2365
|
|
|
2059
|
-
##
|
|
2366
|
+
## 34. Examples
|
|
2060
2367
|
|
|
2061
2368
|
### Базовый сценарий — синхронизация двух адресов
|
|
2062
2369
|
|
|
@@ -2076,7 +2383,7 @@ export const orderBehavior = defineFormBehavior<OrderForm>(({ model }) => {
|
|
|
2076
2383
|
});
|
|
2077
2384
|
```
|
|
2078
2385
|
|
|
2079
|
-
|
|
2386
|
+
Пример: `copyFrom(model.$.shippingAddress, model.$.billingAddress, { when: () => model.useShippingAsBilling })`.
|
|
2080
2387
|
|
|
2081
2388
|
### Копирование группы целиком
|
|
2082
2389
|
|
|
@@ -2124,7 +2431,7 @@ const cleanups = [
|
|
|
2124
2431
|
// teardown: cleanups.forEach((c) => c());
|
|
2125
2432
|
```
|
|
2126
2433
|
|
|
2127
|
-
##
|
|
2434
|
+
## 35. Anti-patterns
|
|
2128
2435
|
|
|
2129
2436
|
```typescript
|
|
2130
2437
|
// ❌ Двусторонняя связь через два copyFrom — конфликт направлений
|
|
@@ -2143,7 +2450,7 @@ copyFrom(model.$.source, model.$.target, { when: () => model.target === '' });
|
|
|
2143
2450
|
copyFrom(model.$.source, model.$.target, { when: () => model.copyEnabled === true });
|
|
2144
2451
|
```
|
|
2145
2452
|
|
|
2146
|
-
##
|
|
2453
|
+
## 36. Troubleshooting
|
|
2147
2454
|
|
|
2148
2455
|
**Q: Скопированное значение не появляется в target.**
|
|
2149
2456
|
A: Проверьте, что (1) behavior передан в `createForm({ behavior })` (или примитив вызван и не
|
|
@@ -2157,14 +2464,14 @@ source и независимых флагов.
|
|
|
2157
2464
|
A: `copyFrom` при `when === false` просто не пишет (не сбрасывает). Для сброса используй параллельно
|
|
2158
2465
|
`resetWhen(model.$.target, () => !model.copyEnabled)` (см. `25-reset-when.md`).
|
|
2159
2466
|
|
|
2160
|
-
##
|
|
2467
|
+
## 37. See also
|
|
2161
2468
|
|
|
2162
2469
|
- [24-sync-fields.md](./24-sync-fields.md) — двусторонняя синхронизация
|
|
2163
2470
|
- [25-reset-when.md](./25-reset-when.md) — сброс target при выключенном условии
|
|
2164
2471
|
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
|
|
2165
2472
|
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `runOutsideEffect` защищает от циклов
|
|
2166
2473
|
|
|
2167
|
-
##
|
|
2474
|
+
## 38. Purpose
|
|
2168
2475
|
|
|
2169
2476
|
`syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них
|
|
2170
2477
|
переписывает второе. Применяется для дублей одного значения в разных частях формы
|
|
@@ -2172,7 +2479,7 @@ A: `copyFrom` при `when === false` просто не пишет (не сбр
|
|
|
2172
2479
|
исключают петли. Для одностороннего копирования — [`copyFrom`](./23-copy-from.md); для
|
|
2173
2480
|
расчётов — [`compute`](./20-compute-vs-watch.md).
|
|
2174
2481
|
|
|
2175
|
-
##
|
|
2482
|
+
## 39. API
|
|
2176
2483
|
|
|
2177
2484
|
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
2178
2485
|
|
|
@@ -2187,7 +2494,7 @@ function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (valu
|
|
|
2187
2494
|
`a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:
|
|
2188
2495
|
применяется только при движении значения `a → b`. Опции `debounce`/`when` нет.
|
|
2189
2496
|
|
|
2190
|
-
##
|
|
2497
|
+
## 40. Examples
|
|
2191
2498
|
|
|
2192
2499
|
### Базовый сценарий — отзеркаливание текста
|
|
2193
2500
|
|
|
@@ -2201,7 +2508,7 @@ export const mirrorBehavior = defineFormBehavior<MirrorForm>(({ model }) => {
|
|
|
2201
2508
|
});
|
|
2202
2509
|
```
|
|
2203
2510
|
|
|
2204
|
-
|
|
2511
|
+
Пример: `syncFields(model.$.syncField1, model.$.syncField2)`.
|
|
2205
2512
|
|
|
2206
2513
|
### С трансформацией — нормализация при прямой записи
|
|
2207
2514
|
|
|
@@ -2226,7 +2533,7 @@ const stop = syncFields(model.$.syncField1, model.$.syncField2);
|
|
|
2226
2533
|
// stop() — отписаться
|
|
2227
2534
|
```
|
|
2228
2535
|
|
|
2229
|
-
##
|
|
2536
|
+
## 41. Anti-patterns
|
|
2230
2537
|
|
|
2231
2538
|
```typescript
|
|
2232
2539
|
// ❌ Симметрично через два copyFrom — конфликт направлений
|
|
@@ -2256,7 +2563,7 @@ syncFields(model.$.amountString, model.$.amountNumber); // string ↔ number
|
|
|
2256
2563
|
compute(model.$.amountNumber, () => Number(model.amountString));
|
|
2257
2564
|
```
|
|
2258
2565
|
|
|
2259
|
-
##
|
|
2566
|
+
## 42. Troubleshooting
|
|
2260
2567
|
|
|
2261
2568
|
**Q: Поля «дёргаются», несколько перезаписей.**
|
|
2262
2569
|
A: Чаще всего на одном из полей висит `transformValue`/`compute`. Убедитесь, что transform
|
|
@@ -2270,21 +2577,21 @@ A: Не вешайте дополнительно `onChange`/`watchField`, ко
|
|
|
2270
2577
|
A: У `syncFields` нет `when`. Эмулируй через два `copyFrom(a→b, { when })` / `copyFrom(b→a, { when })`
|
|
2271
2578
|
с флагами, разрешающими только одну активную сторону, либо через `apply` под условием.
|
|
2272
2579
|
|
|
2273
|
-
##
|
|
2580
|
+
## 43. See also
|
|
2274
2581
|
|
|
2275
2582
|
- [23-copy-from.md](./23-copy-from.md) — однонаправленное копирование с `when`
|
|
2276
2583
|
- [26-transform-value.md](./26-transform-value.md) — нормализация значений на месте
|
|
2277
2584
|
- [22-cycle-detection.md](./22-cycle-detection.md) — почему симметричный copy ломается
|
|
2278
2585
|
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
|
|
2279
2586
|
|
|
2280
|
-
##
|
|
2587
|
+
## 44. Purpose
|
|
2281
2588
|
|
|
2282
2589
|
`resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это
|
|
2283
2590
|
альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но
|
|
2284
2591
|
содержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но
|
|
2285
2592
|
поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение.
|
|
2286
2593
|
|
|
2287
|
-
##
|
|
2594
|
+
## 45. API
|
|
2288
2595
|
|
|
2289
2596
|
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
2290
2597
|
|
|
@@ -2300,7 +2607,7 @@ function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { r
|
|
|
2300
2607
|
`resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
|
|
2301
2608
|
поля не трогаются оператором (значения принадлежат модели).
|
|
2302
2609
|
|
|
2303
|
-
##
|
|
2610
|
+
## 46. Examples
|
|
2304
2611
|
|
|
2305
2612
|
### Базовый сценарий — сброс номера карты при смене способа оплаты
|
|
2306
2613
|
|
|
@@ -2314,7 +2621,7 @@ export const checkoutBehavior = defineFormBehavior<CheckoutForm>(({ model }) =>
|
|
|
2314
2621
|
});
|
|
2315
2622
|
```
|
|
2316
2623
|
|
|
2317
|
-
|
|
2624
|
+
Пример: `resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' })`.
|
|
2318
2625
|
|
|
2319
2626
|
### resetValue для числовых полей
|
|
2320
2627
|
|
|
@@ -2336,7 +2643,7 @@ import { resetWhen } from '@reformer/core';
|
|
|
2336
2643
|
const stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
2337
2644
|
```
|
|
2338
2645
|
|
|
2339
|
-
##
|
|
2646
|
+
## 47. Anti-patterns
|
|
2340
2647
|
|
|
2341
2648
|
```typescript
|
|
2342
2649
|
// ❌ resetWhen вместо enableWhen для disable-сценария
|
|
@@ -2371,7 +2678,7 @@ resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
|
|
|
2371
2678
|
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
2372
2679
|
```
|
|
2373
2680
|
|
|
2374
|
-
##
|
|
2681
|
+
## 48. Troubleshooting
|
|
2375
2682
|
|
|
2376
2683
|
**Q: Сброс не срабатывает, хотя `condition` возвращает true.**
|
|
2377
2684
|
A: Проверьте, что behavior зарегистрирован (`createForm({ behavior })`) или примитив не отписан,
|
|
@@ -2385,13 +2692,13 @@ A: Убедитесь, что `condition` не зависит от значен
|
|
|
2385
2692
|
A: `resetWhen` рассчитан на скалярные сигналы. Для группы используй `enableWhen({ resetOnDisable: true })`
|
|
2386
2693
|
(проходит по поддереву) или `model.reset()` для сброса всей формы к initial-снимку.
|
|
2387
2694
|
|
|
2388
|
-
##
|
|
2695
|
+
## 49. See also
|
|
2389
2696
|
|
|
2390
2697
|
- [04-common-patterns.md](./04-common-patterns.md) — `enableWhen({ resetOnDisable: true })` как альтернатива
|
|
2391
2698
|
- [23-copy-from.md](./23-copy-from.md) — копирование, у которого нет встроенного отката
|
|
2392
2699
|
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `condition` не должен читать целевое поле
|
|
2393
2700
|
|
|
2394
|
-
##
|
|
2701
|
+
## 50. Purpose
|
|
2395
2702
|
|
|
2396
2703
|
`transformValue` подписывается на изменение поля и переписывает его трансформированной
|
|
2397
2704
|
версией: uppercase для кодов, trim+toLowerCase для email, округление для чисел. Применяется
|
|
@@ -2400,7 +2707,7 @@ A: `resetWhen` рассчитан на скалярные сигналы. Для
|
|
|
2400
2707
|
Оператор откладывает запись вне effect-контекста (`runOutsideEffect`) и не пишет, если
|
|
2401
2708
|
`transformer(value) === value` — базовый guard от циклов.
|
|
2402
2709
|
|
|
2403
|
-
##
|
|
2710
|
+
## 51. API
|
|
2404
2711
|
|
|
2405
2712
|
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
2406
2713
|
|
|
@@ -2416,7 +2723,7 @@ function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): voi
|
|
|
2416
2723
|
Дополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора
|
|
2417
2724
|
`transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.
|
|
2418
2725
|
|
|
2419
|
-
##
|
|
2726
|
+
## 52. Examples
|
|
2420
2727
|
|
|
2421
2728
|
### Базовый сценарий — uppercase для кода
|
|
2422
2729
|
|
|
@@ -2430,7 +2737,7 @@ export const promoBehavior = defineFormBehavior<PromoForm>(({ model }) => {
|
|
|
2430
2737
|
});
|
|
2431
2738
|
```
|
|
2432
2739
|
|
|
2433
|
-
|
|
2740
|
+
Пример: `transformValue(model.$.uppercaseField, (v) => (v ?? '').toUpperCase())`.
|
|
2434
2741
|
|
|
2435
2742
|
### Несколько трансформаций
|
|
2436
2743
|
|
|
@@ -2478,7 +2785,7 @@ toUpper(model.$.promoCode);
|
|
|
2478
2785
|
trim(model.$.username);
|
|
2479
2786
|
```
|
|
2480
2787
|
|
|
2481
|
-
##
|
|
2788
|
+
## 53. Anti-patterns
|
|
2482
2789
|
|
|
2483
2790
|
```typescript
|
|
2484
2791
|
// ❌ Неидемпотентный transformer — бесконечный цикл
|
|
@@ -2506,7 +2813,7 @@ transformValue(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
|
|
|
2506
2813
|
compute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
|
|
2507
2814
|
```
|
|
2508
2815
|
|
|
2509
|
-
##
|
|
2816
|
+
## 54. Troubleshooting
|
|
2510
2817
|
|
|
2511
2818
|
**Q: «Cycle detected» при transformValue.**
|
|
2512
2819
|
A: 99% — неидемпотентный transformer. Проверь `transformer(transformer(x)) === transformer(x)`.
|
|
@@ -2519,14 +2826,14 @@ A: Проверь, что behavior зарегистрирован (`createForm({
|
|
|
2519
2826
|
A: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)
|
|
2520
2827
|
лучше делать через `<InputMask>` из `@reformer/ui-kit`, а не `transformValue`.
|
|
2521
2828
|
|
|
2522
|
-
##
|
|
2829
|
+
## 55. See also
|
|
2523
2830
|
|
|
2524
2831
|
- [24-sync-fields.md](./24-sync-fields.md) — порядок применения с syncFields
|
|
2525
2832
|
- [23-copy-from.md](./23-copy-from.md) — `transform`-опция при копировании
|
|
2526
2833
|
- [22-cycle-detection.md](./22-cycle-detection.md) — про идемпотентность
|
|
2527
2834
|
- [16-ui-components.md](./16-ui-components.md) — `<InputMask>` для тяжёлого форматирования
|
|
2528
2835
|
|
|
2529
|
-
##
|
|
2836
|
+
## 56. Purpose
|
|
2530
2837
|
|
|
2531
2838
|
`revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
|
|
2532
2839
|
зависимостей. Применяется, когда правило зависит от значений других полей
|
|
@@ -2534,7 +2841,7 @@ A: Симптом частых записей. Форматирование с
|
|
|
2534
2841
|
Под M1 валидация on-demand (`validateFormModel`), поэтому ревалидация выражается явным
|
|
2535
2842
|
колбэком, а не автоматической привязкой к полю.
|
|
2536
2843
|
|
|
2537
|
-
##
|
|
2844
|
+
## 57. API
|
|
2538
2845
|
|
|
2539
2846
|
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
2540
2847
|
|
|
@@ -2550,7 +2857,7 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
|
|
|
2550
2857
|
`() => validateFormModel(model, schema)`). Вызывается при изменении любой зависимости
|
|
2551
2858
|
(НЕ на инициализации), вне effect-контекста.
|
|
2552
2859
|
|
|
2553
|
-
##
|
|
2860
|
+
## 58. Examples
|
|
2554
2861
|
|
|
2555
2862
|
### Базовый сценарий — перевалидация amount при смене maxAmount
|
|
2556
2863
|
|
|
@@ -2565,7 +2872,7 @@ export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
|
|
|
2565
2872
|
});
|
|
2566
2873
|
```
|
|
2567
2874
|
|
|
2568
|
-
|
|
2875
|
+
Пример: `revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema))`.
|
|
2569
2876
|
|
|
2570
2877
|
### Несколько триггеров
|
|
2571
2878
|
|
|
@@ -2603,7 +2910,7 @@ import { revalidateWhen, validateFormModel } from '@reformer/core';
|
|
|
2603
2910
|
const stop = revalidateWhen([model.$.maxAmount], () => void validateFormModel(model, schema));
|
|
2604
2911
|
```
|
|
2605
2912
|
|
|
2606
|
-
##
|
|
2913
|
+
## 59. Anti-patterns
|
|
2607
2914
|
|
|
2608
2915
|
```typescript
|
|
2609
2916
|
// ❌ Триггер == поле, которое и так меняется само
|
|
@@ -2626,7 +2933,7 @@ const amountVsMax: ModelValidator<number, unknown, Form> = (v, _s, root) =>
|
|
|
2626
2933
|
revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
|
|
2627
2934
|
```
|
|
2628
2935
|
|
|
2629
|
-
##
|
|
2936
|
+
## 60. Troubleshooting
|
|
2630
2937
|
|
|
2631
2938
|
**Q: Ошибка target не пропадает после изменения триггера.**
|
|
2632
2939
|
A: Проверьте, что (1) правило реально читает значение триггера через `root` (cross-field
|
|
@@ -2636,20 +2943,20 @@ A: Проверьте, что (1) правило реально читает з
|
|
|
2636
2943
|
**Q: Как перевалидировать только одно поле, а не всю схему?**
|
|
2637
2944
|
A: Передай в колбэк под-схему только этого поля: `validateFormModel(model, { children: [fieldNode] })`.
|
|
2638
2945
|
|
|
2639
|
-
##
|
|
2946
|
+
## 61. See also
|
|
2640
2947
|
|
|
2641
2948
|
- [03-api-signatures.md](./03-api-signatures.md) — cross-field `ModelValidator` и `validateFormModel`
|
|
2642
2949
|
- [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
|
|
2643
2950
|
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения
|
|
2644
2951
|
|
|
2645
|
-
##
|
|
2952
|
+
## 62. Purpose
|
|
2646
2953
|
|
|
2647
2954
|
Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateFormModel`) →
|
|
2648
2955
|
проверить `result.valid` → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
|
|
2649
2956
|
Валидация — чистая функция ДАННЫХ (модели), она же роутит ошибки в ноды формы, поэтому UI
|
|
2650
2957
|
подсветит проблемные поля. `model.reset()` возвращает значения к initial-снимку.
|
|
2651
2958
|
|
|
2652
|
-
##
|
|
2959
|
+
## 63. API
|
|
2653
2960
|
|
|
2654
2961
|
```typescript
|
|
2655
2962
|
// Модель (источник истины значений):
|
|
@@ -2671,7 +2978,7 @@ form.<field>.markAsTouched();
|
|
|
2671
2978
|
form.<field>.reset();
|
|
2672
2979
|
```
|
|
2673
2980
|
|
|
2674
|
-
##
|
|
2981
|
+
## 64. Examples
|
|
2675
2982
|
|
|
2676
2983
|
### Базовый submit-handler
|
|
2677
2984
|
|
|
@@ -2784,7 +3091,7 @@ async function processFormPayload(rawData: Partial<MyForm>) {
|
|
|
2784
3091
|
}
|
|
2785
3092
|
```
|
|
2786
3093
|
|
|
2787
|
-
##
|
|
3094
|
+
## 65. Anti-patterns
|
|
2788
3095
|
|
|
2789
3096
|
```typescript
|
|
2790
3097
|
// ❌ Чтение результата валидации без await
|
|
@@ -2824,7 +3131,7 @@ model.password = '';
|
|
|
2824
3131
|
model.reset();
|
|
2825
3132
|
```
|
|
2826
3133
|
|
|
2827
|
-
##
|
|
3134
|
+
## 66. Troubleshooting
|
|
2828
3135
|
|
|
2829
3136
|
**Q: После `reset()` в UI остались старые ошибки.**
|
|
2830
3137
|
A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
|
|
@@ -2839,14 +3146,14 @@ A: `reset()` возвращает к initial-снимку (значения на
|
|
|
2839
3146
|
A: `model.get()` возвращает значения всех полей. Фильтруй вручную после `get()` или используй
|
|
2840
3147
|
`enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial.
|
|
2841
3148
|
|
|
2842
|
-
##
|
|
3149
|
+
## 67. See also
|
|
2843
3150
|
|
|
2844
3151
|
- [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
|
|
2845
3152
|
- [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateFormModel`
|
|
2846
3153
|
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateFormModel`
|
|
2847
3154
|
- [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки
|
|
2848
3155
|
|
|
2849
|
-
##
|
|
3156
|
+
## 68. Purpose
|
|
2850
3157
|
|
|
2851
3158
|
Загрузка формы данными с сервера под M1: (1) **initial values** в `createModel(...)`; (2)
|
|
2852
3159
|
**`model.set` / `model.patch`** для загрузки/обновления значений; (3) **external React-hook**
|
|
@@ -2855,7 +3162,7 @@ race-guard через deps). Динамические `componentProps` (опци
|
|
|
2855
3162
|
`form.field.updateComponentProps({ options })` в `queueMicrotask`, чтобы не пересечься с
|
|
2856
3163
|
реактивными эффектами от `set`/`patch`.
|
|
2857
3164
|
|
|
2858
|
-
##
|
|
3165
|
+
## 69. API
|
|
2859
3166
|
|
|
2860
3167
|
```typescript
|
|
2861
3168
|
// Модель:
|
|
@@ -2871,7 +3178,7 @@ form.field.updateComponentProps(props: Record<string, unknown>): void; // дин
|
|
|
2871
3178
|
Initial values задаются в `createModel(initial)`; `reset()` возвращает к ним. Чтобы загруженные
|
|
2872
3179
|
данные стали новой «точкой отсчёта» для `reset()` — вызови `model.captureInitial()` после load.
|
|
2873
3180
|
|
|
2874
|
-
##
|
|
3181
|
+
## 70. Examples
|
|
2875
3182
|
|
|
2876
3183
|
### Initial values в модели
|
|
2877
3184
|
|
|
@@ -2978,7 +3285,7 @@ export const addressBehavior = defineFormBehavior<AddressForm>(({ model, form })
|
|
|
2978
3285
|
});
|
|
2979
3286
|
```
|
|
2980
3287
|
|
|
2981
|
-
##
|
|
3288
|
+
## 71. Anti-patterns
|
|
2982
3289
|
|
|
2983
3290
|
```typescript
|
|
2984
3291
|
// ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз
|
|
@@ -3018,7 +3325,7 @@ useEffect(() => {
|
|
|
3018
3325
|
}, [id]);
|
|
3019
3326
|
```
|
|
3020
3327
|
|
|
3021
|
-
##
|
|
3328
|
+
## 72. Troubleshooting
|
|
3022
3329
|
|
|
3023
3330
|
**Q: После `set`/`patch` не показываются ошибки.**
|
|
3024
3331
|
A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateFormModel(model, schema)`.
|
|
@@ -3031,14 +3338,14 @@ A: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что
|
|
|
3031
3338
|
A: `reset()` возвращает к initial-снимку из `createModel`, не к последнему `set`. Чтобы «сбросить
|
|
3032
3339
|
к данным с сервера» — после `set` вызови `model.captureInitial()` (новая точка отсчёта).
|
|
3033
3340
|
|
|
3034
|
-
##
|
|
3341
|
+
## 73. See also
|
|
3035
3342
|
|
|
3036
3343
|
- [28-submit-and-reset.md](./28-submit-and-reset.md) — обратная сторона жизненного цикла
|
|
3037
3344
|
- [11-async-watchfield.md](./11-async-watchfield.md) — `onChange` для динамики после preload
|
|
3038
3345
|
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `queueMicrotask` нужен
|
|
3039
3346
|
- [16-ui-components.md](./16-ui-components.md) — `updateComponentProps` для динамических опций
|
|
3040
3347
|
|
|
3041
|
-
##
|
|
3348
|
+
## 74. 30. TYPE-SAFETY RECIPES
|
|
3042
3349
|
|
|
3043
3350
|
Идиоматичные паттерны, которые держат сгенерированный код без `any` и `as`-кастов под M1.
|
|
3044
3351
|
|
|
@@ -3189,7 +3496,7 @@ const schema = {
|
|
|
3189
3496
|
- `as`-касты значений полей внутри `compute` → читай `model.field` напрямую (Recipe 5).
|
|
3190
3497
|
- строковые пути / `(form) => ...` в behaviors → это удалённый API, используй сигналы (`model.$.x`).
|
|
3191
3498
|
|
|
3192
|
-
##
|
|
3499
|
+
## 75. 31. ASYNC VALIDATOR
|
|
3193
3500
|
|
|
3194
3501
|
Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
|
|
3195
3502
|
async-валидатор это `ModelValidator`, возвращающий `Promise<ValidationError | null>`. Он
|
|
@@ -3263,7 +3570,7 @@ return pending ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
|
|
|
3263
3570
|
- [29-async-preload.md](29-async-preload.md) — async preload данных при init формы
|
|
3264
3571
|
- [03-api-signatures.md](03-api-signatures.md) — `ModelValidator` и `validateFormModel`
|
|
3265
3572
|
|
|
3266
|
-
##
|
|
3573
|
+
## 76. 32. ASYNC OPTIONS LOADING
|
|
3267
3574
|
|
|
3268
3575
|
Для динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,
|
|
3269
3576
|
`carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +
|
|
@@ -3372,7 +3679,7 @@ loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: tr
|
|
|
3372
3679
|
- [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
|
|
3373
3680
|
- [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)
|
|
3374
3681
|
|
|
3375
|
-
##
|
|
3682
|
+
## 77. API Reference
|
|
3376
3683
|
|
|
3377
3684
|
_Auto-generated from JSDoc on public exports._
|
|
3378
3685
|
|
|
@@ -3398,7 +3705,7 @@ _Source: src/core/types/index.ts_
|
|
|
3398
3705
|
|
|
3399
3706
|
**Signature:**
|
|
3400
3707
|
```typescript
|
|
3401
|
-
export interface ArrayConfig<T extends
|
|
3708
|
+
export interface ArrayConfig<T extends object> {
|
|
3402
3709
|
itemSchema: FormSchema<T>;
|
|
3403
3710
|
initial?: Partial<T>[];
|
|
3404
3711
|
}
|
|
@@ -3605,16 +3912,16 @@ _Source: src/core/types/index.ts_
|
|
|
3605
3912
|
|
|
3606
3913
|
**Kind:** `type`
|
|
3607
3914
|
|
|
3608
|
-
Чистый асинхронный валидатор поля.
|
|
3609
|
-
|
|
3610
|
-
|
|
3915
|
+
Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
|
|
3916
|
+
{@link Validator}, но возвращает `Promise`. Регистрируется через
|
|
3917
|
+
`validateAsync(path, validator, { debounce })`.
|
|
3611
3918
|
|
|
3612
3919
|
**Signature:**
|
|
3613
3920
|
```typescript
|
|
3614
3921
|
export type AsyncValidator<TForm, TField> = (
|
|
3615
3922
|
value: TField,
|
|
3616
|
-
|
|
3617
|
-
root:
|
|
3923
|
+
scope: unknown,
|
|
3924
|
+
root: FormModel<TForm>
|
|
3618
3925
|
) => Promise<ValidationError | null>;
|
|
3619
3926
|
```
|
|
3620
3927
|
|
|
@@ -3916,10 +4223,10 @@ export interface CreateFormFromModelArgs<T> {
|
|
|
3916
4223
|
/** Реактивная модель данных (источник истины значений). */
|
|
3917
4224
|
model: FormModel<T>;
|
|
3918
4225
|
/**
|
|
3919
|
-
* Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг
|
|
3920
|
-
* по идентичности сигнала (`node.value === model.$.path`). Опциональна.
|
|
4226
|
+
* Единая Schema (дерево узлов {@link FormSchemaNode}). createForm обходит её и привязывает конфиг
|
|
4227
|
+
* поля к ноде по идентичности сигнала (`node.value === model.$.path`). Опциональна.
|
|
3921
4228
|
*/
|
|
3922
|
-
schema?:
|
|
4229
|
+
schema?: FormSchemaNode;
|
|
3923
4230
|
/**
|
|
3924
4231
|
* Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
|
|
3925
4232
|
* заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
|
|
@@ -4072,7 +4379,7 @@ export interface ErrorFilterOptions {
|
|
|
4072
4379
|
message?: string;
|
|
4073
4380
|
|
|
4074
4381
|
/** Фильтр по параметрам ошибки */
|
|
4075
|
-
params?:
|
|
4382
|
+
params?: Record<string, FormValue>;
|
|
4076
4383
|
|
|
4077
4384
|
/** Кастомный предикат для фильтрации */
|
|
4078
4385
|
predicate?: (error: ValidationError) => boolean;
|
|
@@ -4325,8 +4632,7 @@ export interface FieldControlState<T> {
|
|
|
4325
4632
|
* );
|
|
4326
4633
|
* ```
|
|
4327
4634
|
*/
|
|
4328
|
-
|
|
4329
|
-
componentProps: Record<string, any>;
|
|
4635
|
+
componentProps: Record<string, unknown>;
|
|
4330
4636
|
}
|
|
4331
4637
|
```
|
|
4332
4638
|
|
|
@@ -4590,22 +4896,6 @@ try {
|
|
|
4590
4896
|
|
|
4591
4897
|
_Source: src/core/utils/error-handler.ts_
|
|
4592
4898
|
|
|
4593
|
-
### FormFields
|
|
4594
|
-
|
|
4595
|
-
**Kind:** `type`
|
|
4596
|
-
|
|
4597
|
-
Тип для конфига с полями (FormSchema generic constraint)
|
|
4598
|
-
Используется вместо `Record<string, any>` для схем форм
|
|
4599
|
-
|
|
4600
|
-
**Signature:**
|
|
4601
|
-
```typescript
|
|
4602
|
-
export type FormFields = Record<string, FormValue>;
|
|
4603
|
-
```
|
|
4604
|
-
|
|
4605
|
-
**Deprecated:** (no message)
|
|
4606
|
-
|
|
4607
|
-
_Source: src/core/types/index.ts_
|
|
4608
|
-
|
|
4609
4899
|
### FormModel
|
|
4610
4900
|
|
|
4611
4901
|
**Kind:** `type`
|
|
@@ -4702,12 +4992,17 @@ _Source: src/core/types/form-proxy.ts_
|
|
|
4702
4992
|
|
|
4703
4993
|
**Kind:** `type`
|
|
4704
4994
|
|
|
4705
|
-
|
|
4995
|
+
**Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
|
|
4996
|
+
(или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
|
|
4706
4997
|
- `T[] -> [FormSchema<T>]` (массив с одним элементом)
|
|
4707
4998
|
- `object -> FormSchema<T>` (группа)
|
|
4708
4999
|
- `primitive -> FieldConfig<T>` (поле)
|
|
4709
5000
|
|
|
4710
|
-
Использует NonNullable для корректной обработки опциональных
|
|
5001
|
+
Использует NonNullable для корректной обработки опциональных полей.
|
|
5002
|
+
|
|
5003
|
+
⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
|
|
5004
|
+
контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
|
|
5005
|
+
(ключи = поля данных), а не узел дерева.
|
|
4711
5006
|
|
|
4712
5007
|
**Signature:**
|
|
4713
5008
|
```typescript
|
|
@@ -4756,6 +5051,65 @@ const schema: FormSchema<Form> = {
|
|
|
4756
5051
|
|
|
4757
5052
|
_Source: src/core/types/deep-schema.ts_
|
|
4758
5053
|
|
|
5054
|
+
### FormSchemaNode
|
|
5055
|
+
|
|
5056
|
+
**Kind:** `interface`
|
|
5057
|
+
|
|
5058
|
+
Узел единой схемы M1 — дерево, обходимое `createForm({ model, schema })`,
|
|
5059
|
+
`validateModel`/`validateFormModel` и рендерерами.
|
|
5060
|
+
|
|
5061
|
+
Узел совмещает несколько ролей (различаются рантаймом по форме):
|
|
5062
|
+
- **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
|
|
5063
|
+
- **массив** — `{ array: model.<path>, item(itemModel) }`;
|
|
5064
|
+
- **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
|
|
5065
|
+
- **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).
|
|
5066
|
+
|
|
5067
|
+
Индексная сигнатура (`[key: string]: unknown`) отражает свободный рекурсивный обход: под-узлы
|
|
5068
|
+
допустимы под любым ключом. Известные поля типизированы (даёт автокомплит и проверку их типов).
|
|
5069
|
+
|
|
5070
|
+
**Signature:**
|
|
5071
|
+
```typescript
|
|
5072
|
+
export interface FormSchemaNode {
|
|
5073
|
+
/**
|
|
5074
|
+
* «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма
|
|
5075
|
+
* зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до
|
|
5076
|
+
* `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
|
|
5077
|
+
*/
|
|
5078
|
+
value?: unknown;
|
|
5079
|
+
/** UI-компонент. Опционален: core-часть работает без UI (значение/валидация). */
|
|
5080
|
+
component?: ComponentType<any>;
|
|
5081
|
+
/** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
|
|
5082
|
+
componentProps?: Record<string, unknown>;
|
|
5083
|
+
validators?: SchemaValidator[];
|
|
5084
|
+
asyncValidators?: SchemaValidator[];
|
|
5085
|
+
updateOn?: 'change' | 'blur' | 'submit';
|
|
5086
|
+
disabled?: boolean;
|
|
5087
|
+
/** Задержка (мс) перед запуском асинхронной валидации. */
|
|
5088
|
+
debounce?: number;
|
|
5089
|
+
/** Идентификатор узла (для wizard/tabs/renderBehavior). */
|
|
5090
|
+
selector?: string;
|
|
5091
|
+
testId?: string;
|
|
5092
|
+
/** Дочерние узлы (даёт контекстную типизацию вложенным литералам — value/validators/when). */
|
|
5093
|
+
children?: readonly FormSchemaNode[];
|
|
5094
|
+
/** Условие включения поддерева (branch-узел `{ when, children }`). */
|
|
5095
|
+
when?: (scope: any, root: any) => boolean;
|
|
5096
|
+
/** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */
|
|
5097
|
+
array?: SchemaArrayControl;
|
|
5098
|
+
/** Схема элемента массива: под-модель элемента → узел поддерева. */
|
|
5099
|
+
item?: (itemModel: any) => FormSchemaNode;
|
|
5100
|
+
/**
|
|
5101
|
+
* Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,
|
|
5102
|
+
* либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`
|
|
5103
|
+
* схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.
|
|
5104
|
+
*/
|
|
5105
|
+
initialValue?: unknown;
|
|
5106
|
+
/** Свободная вложенность: record-of-fields и произвольные под-узлы. */
|
|
5107
|
+
[key: string]: unknown;
|
|
5108
|
+
}
|
|
5109
|
+
```
|
|
5110
|
+
|
|
5111
|
+
_Source: src/core/types/schema-node.ts_
|
|
5112
|
+
|
|
4759
5113
|
### FormStatusMachine
|
|
4760
5114
|
|
|
4761
5115
|
**Kind:** `class`
|
|
@@ -4801,7 +5155,7 @@ FormSubmitter - управляет процессом отправки форм
|
|
|
4801
5155
|
|
|
4802
5156
|
**Signature:**
|
|
4803
5157
|
```typescript
|
|
4804
|
-
export class FormSubmitter<T extends
|
|
5158
|
+
export class FormSubmitter<T extends object> {
|
|
4805
5159
|
/** Внутренний сигнал состояния отправки */ /* … */ }
|
|
4806
5160
|
```
|
|
4807
5161
|
|
|
@@ -5009,7 +5363,7 @@ _Source: src/core/types/index.ts_
|
|
|
5009
5363
|
|
|
5010
5364
|
**Signature:**
|
|
5011
5365
|
```typescript
|
|
5012
|
-
export function integer<TForm = unknown, TField extends number | undefined = number>(
|
|
5366
|
+
export function integer<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5013
5367
|
options?: ValidateOptions
|
|
5014
5368
|
): Validator<TForm, TField>
|
|
5015
5369
|
```
|
|
@@ -5046,7 +5400,7 @@ ArrayNode представляет массив вложенных форм (о
|
|
|
5046
5400
|
|
|
5047
5401
|
**Signature:**
|
|
5048
5402
|
```typescript
|
|
5049
|
-
export function isArrayNode(value: unknown): value is ArrayNode<
|
|
5403
|
+
export function isArrayNode(value: unknown): value is ArrayNode<object>
|
|
5050
5404
|
```
|
|
5051
5405
|
|
|
5052
5406
|
**Parameters:**
|
|
@@ -5208,7 +5562,7 @@ GroupNode представляет объект с вложенными поля
|
|
|
5208
5562
|
|
|
5209
5563
|
**Signature:**
|
|
5210
5564
|
```typescript
|
|
5211
|
-
export function isGroupNode(value: unknown): value is GroupNode<
|
|
5565
|
+
export function isGroupNode(value: unknown): value is GroupNode<object>
|
|
5212
5566
|
```
|
|
5213
5567
|
|
|
5214
5568
|
**Parameters:**
|
|
@@ -5238,7 +5592,7 @@ _Source: src/core/utils/type-guards.ts_
|
|
|
5238
5592
|
|
|
5239
5593
|
**Signature:**
|
|
5240
5594
|
```typescript
|
|
5241
|
-
export function isNumber<TForm = unknown, TField extends number | undefined = number>(
|
|
5595
|
+
export function isNumber<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5242
5596
|
options?: ValidateOptions
|
|
5243
5597
|
): Validator<TForm, TField>
|
|
5244
5598
|
```
|
|
@@ -5306,7 +5660,7 @@ _Source: src/core/utils/derived-registry.ts_
|
|
|
5306
5660
|
|
|
5307
5661
|
**Signature:**
|
|
5308
5662
|
```typescript
|
|
5309
|
-
export function max<TForm = unknown, TField extends number | undefined = number>(
|
|
5663
|
+
export function max<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5310
5664
|
maxValue: number,
|
|
5311
5665
|
options?: ValidateOptions
|
|
5312
5666
|
): Validator<TForm, TField>
|
|
@@ -5468,7 +5822,7 @@ _Source: src/core/validation/validators/max-length.ts_
|
|
|
5468
5822
|
|
|
5469
5823
|
**Signature:**
|
|
5470
5824
|
```typescript
|
|
5471
|
-
export function min<TForm = unknown, TField extends number | undefined = number>(
|
|
5825
|
+
export function min<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5472
5826
|
minValue: number,
|
|
5473
5827
|
options?: ValidateOptions
|
|
5474
5828
|
): Validator<TForm, TField>
|
|
@@ -5636,9 +5990,17 @@ export interface ModelApi<T> {
|
|
|
5636
5990
|
readonly $: ModelSignals<T>;
|
|
5637
5991
|
/** Снимок значений (без подписки) — для submit. */
|
|
5638
5992
|
get(): T;
|
|
5639
|
-
/**
|
|
5993
|
+
/**
|
|
5994
|
+
* @deprecated Алиас {@link ModelApi.patch}: `set` выполняет то же частичное **слияние** —
|
|
5995
|
+
* обновляет только переданные ключи, отсутствующие НЕ трогает (это НЕ полная замена, вопреки
|
|
5996
|
+
* старому названию). Используйте `patch`; для замены с обнулением лишних полей делайте
|
|
5997
|
+
* `patch({ ...defaults, ...dto })` явно. Не меняет initial-снимок.
|
|
5998
|
+
*/
|
|
5640
5999
|
set(value: Partial<T>): void;
|
|
5641
|
-
/**
|
|
6000
|
+
/**
|
|
6001
|
+
* Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
|
|
6002
|
+
* отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
|
|
6003
|
+
*/
|
|
5642
6004
|
patch(value: Partial<T>): void;
|
|
5643
6005
|
/** Отличаются ли текущие значения от initial-снимка (value-diff). */
|
|
5644
6006
|
isDirty(): boolean;
|
|
@@ -5663,6 +6025,11 @@ _Source: src/core/model/types.ts_
|
|
|
5663
6025
|
**Signature:**
|
|
5664
6026
|
```typescript
|
|
5665
6027
|
export interface ModelArray<U> {
|
|
6028
|
+
/**
|
|
6029
|
+
* Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
|
|
6030
|
+
* рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
|
|
6031
|
+
*/
|
|
6032
|
+
readonly __path: string;
|
|
5666
6033
|
/** Реактивная длина. */
|
|
5667
6034
|
readonly length: number;
|
|
5668
6035
|
/** Добавить элемент в конец (значение элемента целиком). */
|
|
@@ -5677,12 +6044,14 @@ export interface ModelArray<U> {
|
|
|
5677
6044
|
swap(a: number, b: number): void;
|
|
5678
6045
|
/** Очистить массив. */
|
|
5679
6046
|
clear(): void;
|
|
5680
|
-
/**
|
|
5681
|
-
at(
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
/**
|
|
5685
|
-
|
|
6047
|
+
/** Элемент по индексу: объект → под-модель, массив → {@link ModelArray}, лист → значение. */
|
|
6048
|
+
at(
|
|
6049
|
+
index: number
|
|
6050
|
+
): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
|
|
6051
|
+
/** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
|
|
6052
|
+
map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
|
|
6053
|
+
/** Итерация по элементам (см. {@link ModelArrayItem}). */
|
|
6054
|
+
forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
|
|
5686
6055
|
/** Снимок массива значений (без подписки). */
|
|
5687
6056
|
toArray(): U[];
|
|
5688
6057
|
/** Индексный value-доступ. */
|
|
@@ -5789,13 +6158,7 @@ _Source: src/core/model/types.ts_
|
|
|
5789
6158
|
**Signature:**
|
|
5790
6159
|
```typescript
|
|
5791
6160
|
export type ModelSignals<T> = {
|
|
5792
|
-
[K in keyof T]:
|
|
5793
|
-
? { readonly length: number; readonly [index: number]: ModelSignals<U & object> }
|
|
5794
|
-
: NonNullable<T[K]> extends Opaque
|
|
5795
|
-
? PathAwareSignal<T[K]>
|
|
5796
|
-
: NonNullable<T[K]> extends object
|
|
5797
|
-
? ModelSignals<NonNullable<T[K]>>
|
|
5798
|
-
: PathAwareSignal<T[K]>;
|
|
6161
|
+
[K in keyof T]: ModelSignalNode<T[K]>;
|
|
5799
6162
|
};
|
|
5800
6163
|
```
|
|
5801
6164
|
|
|
@@ -5822,9 +6185,12 @@ _Source: src/core/model/validate-model.ts_
|
|
|
5822
6185
|
|
|
5823
6186
|
**Kind:** `type`
|
|
5824
6187
|
|
|
5825
|
-
Валидатор слоя
|
|
6188
|
+
Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
|
|
5826
6189
|
массива или корень); `root` — корневая модель.
|
|
5827
6190
|
|
|
6191
|
+
Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
|
|
6192
|
+
`FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
|
|
6193
|
+
|
|
5828
6194
|
**Signature:**
|
|
5829
6195
|
```typescript
|
|
5830
6196
|
export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
|
|
@@ -5870,7 +6236,7 @@ _Source: src/core/model/types.ts_
|
|
|
5870
6236
|
|
|
5871
6237
|
**Signature:**
|
|
5872
6238
|
```typescript
|
|
5873
|
-
export function multipleOf<TForm = unknown, TField extends number | undefined = number>(
|
|
6239
|
+
export function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5874
6240
|
divisor: number,
|
|
5875
6241
|
options?: ValidateOptions
|
|
5876
6242
|
): Validator<TForm, TField>
|
|
@@ -5979,7 +6345,7 @@ _Source: src/core/factories/node-factory.ts_
|
|
|
5979
6345
|
|
|
5980
6346
|
**Signature:**
|
|
5981
6347
|
```typescript
|
|
5982
|
-
export function nonNegative<TForm = unknown, TField extends number | undefined = number>(
|
|
6348
|
+
export function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5983
6349
|
options?: ValidateOptions
|
|
5984
6350
|
): Validator<TForm, TField>
|
|
5985
6351
|
```
|
|
@@ -6015,7 +6381,7 @@ _Source: src/core/validation/validators/non-negative.ts_
|
|
|
6015
6381
|
|
|
6016
6382
|
**Signature:**
|
|
6017
6383
|
```typescript
|
|
6018
|
-
export function nonZero<TForm = unknown, TField extends number | undefined = number>(
|
|
6384
|
+
export function nonZero<TForm = unknown, TField extends number | null | undefined = number>(
|
|
6019
6385
|
options?: ValidateOptions
|
|
6020
6386
|
): Validator<TForm, TField>
|
|
6021
6387
|
```
|
|
@@ -6441,6 +6807,48 @@ return effect(() => {
|
|
|
6441
6807
|
|
|
6442
6808
|
_Source: src/core/utils/safe-effect.ts_
|
|
6443
6809
|
|
|
6810
|
+
### SchemaArrayControl
|
|
6811
|
+
|
|
6812
|
+
**Kind:** `interface`
|
|
6813
|
+
|
|
6814
|
+
Минимальный контракт реактивного массива модели ({@link FormSchemaNode.array}).
|
|
6815
|
+
Совпадает по форме с рантайм-фасадом `model.<array>` (см. `ModelArray`); рендерерский
|
|
6816
|
+
`RenderModelArrayControl` — его расширение (добавляет `move`).
|
|
6817
|
+
|
|
6818
|
+
**Signature:**
|
|
6819
|
+
```typescript
|
|
6820
|
+
export interface SchemaArrayControl {
|
|
6821
|
+
/** Путь массива в модели (dot-нотация) — нужен для резолва узла массива. */
|
|
6822
|
+
readonly __path: string;
|
|
6823
|
+
/** Реактивная длина. */
|
|
6824
|
+
readonly length: number;
|
|
6825
|
+
at(index: number): unknown;
|
|
6826
|
+
push(item: unknown): void;
|
|
6827
|
+
removeAt(index: number): void;
|
|
6828
|
+
}
|
|
6829
|
+
```
|
|
6830
|
+
|
|
6831
|
+
_Source: src/core/types/schema-node.ts_
|
|
6832
|
+
|
|
6833
|
+
### SchemaValidator
|
|
6834
|
+
|
|
6835
|
+
**Kind:** `type`
|
|
6836
|
+
|
|
6837
|
+
Валидатор поля схемы. Узел хранит validators гетерогенно: движок `validate*` вызывает их как
|
|
6838
|
+
`(value, scope, root)`, но сюда кладут любой контракт — `ValidatorFn` (value),
|
|
6839
|
+
`Validator` (value, control, root) или `ModelValidator` (value, model, root). Поэтому тип
|
|
6840
|
+
намеренно широкий по параметрам.
|
|
6841
|
+
|
|
6842
|
+
**Signature:**
|
|
6843
|
+
```typescript
|
|
6844
|
+
export type SchemaValidator = (
|
|
6845
|
+
value: any,
|
|
6846
|
+
...rest: any[]
|
|
6847
|
+
) => ValidationError | null | Promise<ValidationError | null>;
|
|
6848
|
+
```
|
|
6849
|
+
|
|
6850
|
+
_Source: src/core/types/schema-node.ts_
|
|
6851
|
+
|
|
6444
6852
|
### SetValueOptions
|
|
6445
6853
|
|
|
6446
6854
|
**Kind:** `interface`
|
|
@@ -6525,7 +6933,7 @@ _Source: src/core/utils/form-submitter.ts_
|
|
|
6525
6933
|
|
|
6526
6934
|
**Signature:**
|
|
6527
6935
|
```typescript
|
|
6528
|
-
export interface SubmittableForm<T extends
|
|
6936
|
+
export interface SubmittableForm<T extends object> {
|
|
6529
6937
|
/** Пометить все поля как touched */
|
|
6530
6938
|
markAsTouched(): void;
|
|
6531
6939
|
/** Валидировать форму */
|
|
@@ -6691,7 +7099,7 @@ _Source: src/core/types/index.ts_
|
|
|
6691
7099
|
|
|
6692
7100
|
**Signature:**
|
|
6693
7101
|
```typescript
|
|
6694
|
-
export type UnknownRecord = Record<string,
|
|
7102
|
+
export type UnknownRecord = Record<string, unknown>;
|
|
6695
7103
|
```
|
|
6696
7104
|
|
|
6697
7105
|
_Source: src/core/types/index.ts_
|
|
@@ -6752,7 +7160,7 @@ React-хук для подписки только на длину массива
|
|
|
6752
7160
|
|
|
6753
7161
|
**Signature:**
|
|
6754
7162
|
```typescript
|
|
6755
|
-
export function useArrayLength<T extends
|
|
7163
|
+
export function useArrayLength<T extends object>(control: ArrayNode<T>): number
|
|
6756
7164
|
```
|
|
6757
7165
|
|
|
6758
7166
|
**Parameters:**
|
|
@@ -6806,8 +7214,8 @@ React-хук для подписки на состояние формы (FieldNo
|
|
|
6806
7214
|
**Signature:**
|
|
6807
7215
|
```typescript
|
|
6808
7216
|
export function useFormControl(
|
|
6809
|
-
control: FieldNode<FormValue> | ArrayNode<
|
|
6810
|
-
): FieldControlState<FormValue> | ArrayControlState<
|
|
7217
|
+
control: FieldNode<FormValue> | ArrayNode<object> | undefined
|
|
7218
|
+
): FieldControlState<FormValue> | ArrayControlState<object>
|
|
6811
7219
|
```
|
|
6812
7220
|
|
|
6813
7221
|
**Parameters:**
|
|
@@ -7221,7 +7629,7 @@ In-form валидация: прогоняет {@link validateModel} и роут
|
|
|
7221
7629
|
```typescript
|
|
7222
7630
|
export async function validateFormModel<T>(
|
|
7223
7631
|
model: FormModel<T>,
|
|
7224
|
-
schema:
|
|
7632
|
+
schema: FormSchemaNode
|
|
7225
7633
|
): Promise<ModelValidationResult>
|
|
7226
7634
|
```
|
|
7227
7635
|
|
|
@@ -7253,7 +7661,7 @@ Headless-валидация данных (sync + async). Работает без
|
|
|
7253
7661
|
```typescript
|
|
7254
7662
|
export async function validateModel<T>(
|
|
7255
7663
|
model: FormModel<T>,
|
|
7256
|
-
schema:
|
|
7664
|
+
schema: FormSchemaNode
|
|
7257
7665
|
): Promise<ModelValidationResult>
|
|
7258
7666
|
```
|
|
7259
7667
|
|
|
@@ -7274,7 +7682,10 @@ _Source: src/core/model/validate-model.ts_
|
|
|
7274
7682
|
|
|
7275
7683
|
**Signature:**
|
|
7276
7684
|
```typescript
|
|
7277
|
-
export function validateModelSync<T>(
|
|
7685
|
+
export function validateModelSync<T>(
|
|
7686
|
+
model: FormModel<T>,
|
|
7687
|
+
schema: FormSchemaNode
|
|
7688
|
+
): ModelValidationResult
|
|
7278
7689
|
```
|
|
7279
7690
|
|
|
7280
7691
|
**Parameters:**
|
|
@@ -7306,7 +7717,7 @@ export interface ValidateOptions {
|
|
|
7306
7717
|
/** Сообщение об ошибке */
|
|
7307
7718
|
message?: string;
|
|
7308
7719
|
/** Параметры ошибки */
|
|
7309
|
-
params?:
|
|
7720
|
+
params?: Record<string, FormValue>;
|
|
7310
7721
|
}
|
|
7311
7722
|
```
|
|
7312
7723
|
|
|
@@ -7323,7 +7734,7 @@ _Source: src/core/types/validation-schema.ts_
|
|
|
7323
7734
|
export interface ValidationError {
|
|
7324
7735
|
code: string;
|
|
7325
7736
|
message: string;
|
|
7326
|
-
params?:
|
|
7737
|
+
params?: Record<string, FormValue>;
|
|
7327
7738
|
/** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
|
|
7328
7739
|
severity?: 'error' | 'warning';
|
|
7329
7740
|
}
|
|
@@ -7337,15 +7748,25 @@ _Source: src/core/types/index.ts_
|
|
|
7337
7748
|
|
|
7338
7749
|
Чистый синхронный валидатор поля.
|
|
7339
7750
|
|
|
7340
|
-
|
|
7341
|
-
|
|
7751
|
+
Сигнатура зеркалит то, что реально вызывает движок M1
|
|
7752
|
+
(`validateModel`/`validateFormModel`/`validateModelSync`): `validator(value, scope, root)`.
|
|
7753
|
+
- `value` — значение поля (`TField`);
|
|
7754
|
+
- `scope` — ближайшая scope-**модель** (под-модель элемента массива или корень). Из `TForm`/`TField`
|
|
7755
|
+
её тип не выводится, поэтому `unknown` — потребитель сужает сам (для типизированного scope
|
|
7756
|
+
используйте {@link ModelValidator}`<TField, TScope, TForm>`);
|
|
7757
|
+
- `root` — корневая модель формы `FormModel<TForm>` (реактивный value-proxy: `root.field` читает
|
|
7758
|
+
значение). Возвращает `ValidationError` либо `null`. Не знает про реестр валидации.
|
|
7759
|
+
|
|
7760
|
+
Совместим с полем `validators` узла схемы (см. `SchemaValidator`) — там же лежат `ModelValidator`
|
|
7761
|
+
и `ValidatorFn`. Встроенные фабрики (`required()`/`email()`/…) возвращают `(value) => …` и
|
|
7762
|
+
дополнительные аргументы игнорируют.
|
|
7342
7763
|
|
|
7343
7764
|
**Signature:**
|
|
7344
7765
|
```typescript
|
|
7345
7766
|
export type Validator<TForm, TField> = (
|
|
7346
7767
|
value: TField,
|
|
7347
|
-
|
|
7348
|
-
root:
|
|
7768
|
+
scope: unknown,
|
|
7769
|
+
root: FormModel<TForm>
|
|
7349
7770
|
) => ValidationError | null;
|
|
7350
7771
|
```
|
|
7351
7772
|
|
|
@@ -7353,8 +7774,9 @@ export type Validator<TForm, TField> = (
|
|
|
7353
7774
|
|
|
7354
7775
|
Кастомный валидатор в массиве `validators` поля схемы
|
|
7355
7776
|
```typescript
|
|
7356
|
-
const isAdult: Validator<MyForm, number> = (value,
|
|
7777
|
+
const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
|
|
7357
7778
|
if (value < 18) return { code: 'tooYoung', message: '18+' };
|
|
7779
|
+
// root — FormModel<MyForm>: root.someOtherField читается как значение
|
|
7358
7780
|
return null;
|
|
7359
7781
|
};
|
|
7360
7782
|
|