@reformer/core 8.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 +568 -155
- package/package.json +6 -2
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
|
|
@@ -1040,6 +1041,39 @@ const form = createForm<MyForm>({ model, schema });
|
|
|
1040
1041
|
> он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
|
|
1041
1042
|
> См. `30-type-safety-recipes.md`.
|
|
1042
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
|
+
|
|
1043
1077
|
### Array operations — на модели
|
|
1044
1078
|
|
|
1045
1079
|
Мутации массива делаются через `ModelArray` (`model.items`), а не через ноду формы:
|
|
@@ -1060,6 +1094,16 @@ model.items.map((item, i) => item.name); // item — FormModel<Item>
|
|
|
1060
1094
|
> (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
|
|
1061
1095
|
> берутся из `item`-фабрики схемы автоматически.
|
|
1062
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
|
+
|
|
1063
1107
|
### Rendering Arrays
|
|
1064
1108
|
|
|
1065
1109
|
Каждый элемент массива — под-форма (`FormProxy<Item>`). Итерируй через `form.items.map`:
|
|
@@ -1112,7 +1156,12 @@ const uniqueNames: ModelValidator<unknown, unknown, MyForm> = (_value, _scope, r
|
|
|
1112
1156
|
};
|
|
1113
1157
|
```
|
|
1114
1158
|
|
|
1115
|
-
## 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)
|
|
1116
1165
|
|
|
1117
1166
|
Для async-реакции на изменение поля (загрузка зависимых опций, справочников) используй
|
|
1118
1167
|
`onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно
|
|
@@ -1166,7 +1215,7 @@ const stop = watchField(model.$.country, (country) => {
|
|
|
1166
1215
|
// stop() — отписаться
|
|
1167
1216
|
```
|
|
1168
1217
|
|
|
1169
|
-
##
|
|
1218
|
+
## 13. 11. ARRAY CLEANUP PATTERN
|
|
1170
1219
|
|
|
1171
1220
|
Очистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на
|
|
1172
1221
|
модели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.
|
|
@@ -1198,7 +1247,7 @@ function clearWhenOff(flag: ReadonlySignal<boolean>, array: { clear(): void }):
|
|
|
1198
1247
|
clearWhenOff(model.$.hasProperty, model.properties);
|
|
1199
1248
|
```
|
|
1200
1249
|
|
|
1201
|
-
##
|
|
1250
|
+
## 14. 12. MULTI-STEP FORM VALIDATION
|
|
1202
1251
|
|
|
1203
1252
|
Каждый шаг — своя под-схема валидации (дерево узлов `{ children: [...] }`). Переход к
|
|
1204
1253
|
следующему шагу проверяется `validateFormModel(model, stepSchema)`; полный submit — по
|
|
@@ -1275,7 +1324,7 @@ function MultiStepForm() {
|
|
|
1275
1324
|
}
|
|
1276
1325
|
```
|
|
1277
1326
|
|
|
1278
|
-
##
|
|
1327
|
+
## 15. 13. EXTENDED COMMON MISTAKES
|
|
1279
1328
|
|
|
1280
1329
|
### Reuse via apply / applyEach — не дублируй код
|
|
1281
1330
|
|
|
@@ -1357,30 +1406,37 @@ revalidateWhen([model.$.field2], () => validateFormModel(model, schema));
|
|
|
1357
1406
|
> `validateGroup`/`validateTree`/`validateForm`, типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn`
|
|
1358
1407
|
> и `ctx.form.*`/`ctx.setFieldValue` — из старой path-based архитектуры и УДАЛЕНЫ. См. `17-nonexistent-api.md`.
|
|
1359
1408
|
|
|
1360
|
-
##
|
|
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.
|
|
1361
1419
|
|
|
1362
1420
|
```
|
|
1363
1421
|
src/
|
|
1364
|
-
├── components/ui/
|
|
1365
|
-
│ ├── FormField.tsx
|
|
1366
|
-
│ └── FormArraySection.tsx
|
|
1422
|
+
├── components/ui/ # App-wide reusable UI (FormField, FormArraySection, ...)
|
|
1367
1423
|
│
|
|
1368
1424
|
├── forms/
|
|
1369
|
-
│ └── [form-name]/
|
|
1370
|
-
│ ├──
|
|
1371
|
-
│ ├──
|
|
1372
|
-
│ ├──
|
|
1373
|
-
│ ├──
|
|
1374
|
-
│ ├── behavior.ts
|
|
1375
|
-
│ ├──
|
|
1376
|
-
│
|
|
1377
|
-
│
|
|
1378
|
-
│ │ └── loan-info/
|
|
1379
|
-
│ │ └── LoanInfoForm.tsx
|
|
1380
|
-
│ │
|
|
1381
|
-
│ └── 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
|
|
1382
1434
|
```
|
|
1383
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
|
+
|
|
1384
1440
|
### Key Files
|
|
1385
1441
|
|
|
1386
1442
|
```typescript
|
|
@@ -1396,7 +1452,7 @@ import { createModel, type FormModel } from '@reformer/core';
|
|
|
1396
1452
|
export const createCreditApplicationModel = (): FormModel<CreditApplicationForm> =>
|
|
1397
1453
|
createModel<CreditApplicationForm>(createInitialCreditApplication());
|
|
1398
1454
|
|
|
1399
|
-
// forms/credit-application/schema.ts
|
|
1455
|
+
// forms/credit-application/form.schema.ts
|
|
1400
1456
|
import type { FormModel } from '@reformer/core';
|
|
1401
1457
|
export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
|
|
1402
1458
|
loanType: { value: model.$.loanType, component: Select, componentProps: { /* ... */ } },
|
|
@@ -1404,14 +1460,14 @@ export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>)
|
|
|
1404
1460
|
properties: { array: model.properties, item: propertyItem },
|
|
1405
1461
|
});
|
|
1406
1462
|
|
|
1407
|
-
// forms/credit-application/behavior.ts
|
|
1463
|
+
// forms/credit-application/form.behavior.ts
|
|
1408
1464
|
import { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';
|
|
1409
1465
|
export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(({ model }) => {
|
|
1410
1466
|
compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
|
|
1411
1467
|
enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
|
|
1412
1468
|
});
|
|
1413
1469
|
|
|
1414
|
-
// forms/credit-application/
|
|
1470
|
+
// forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline
|
|
1415
1471
|
import { createForm } from '@reformer/core';
|
|
1416
1472
|
export const createCreditApplicationForm = () => {
|
|
1417
1473
|
const model = createCreditApplicationModel();
|
|
@@ -1419,15 +1475,64 @@ export const createCreditApplicationForm = () => {
|
|
|
1419
1475
|
};
|
|
1420
1476
|
```
|
|
1421
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
|
+
|
|
1422
1518
|
### Scaling
|
|
1423
1519
|
|
|
1424
|
-
| Complexity | Structure
|
|
1425
|
-
| ---------- |
|
|
1426
|
-
| Simple
|
|
1427
|
-
|
|
|
1428
|
-
|
|
|
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`).
|
|
1429
1534
|
|
|
1430
|
-
##
|
|
1535
|
+
## 17. 14.5 UI COMPONENT PATTERNS
|
|
1431
1536
|
|
|
1432
1537
|
> **Default rule (read first)**: для UI используй `FormField` из
|
|
1433
1538
|
> [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
|
|
@@ -1612,7 +1717,7 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
|
|
|
1612
1717
|
}
|
|
1613
1718
|
```
|
|
1614
1719
|
|
|
1615
|
-
##
|
|
1720
|
+
## 18. 15. NON-EXISTENT API (DO NOT USE)
|
|
1616
1721
|
|
|
1617
1722
|
**Следующего API НЕТ в @reformer/core** (частично — наследие старой path-based архитектуры,
|
|
1618
1723
|
удалено при переходе на M1):
|
|
@@ -1676,7 +1781,166 @@ enableWhen(path.city, (form) => Boolean(form.country));
|
|
|
1676
1781
|
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
|
|
1677
1782
|
```
|
|
1678
1783
|
|
|
1679
|
-
##
|
|
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)
|
|
1680
1944
|
|
|
1681
1945
|
Под M1 значения живут в модели. Есть три контекста чтения: value-доступ модели, сигналы, и React-хуки.
|
|
1682
1946
|
|
|
@@ -1735,7 +1999,7 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
|
|
|
1735
1999
|
> цикла нет). Для side-эффектов (загрузка опций, обновление componentProps) — `onChange`.
|
|
1736
2000
|
> `computeFrom`/`copyFrom`/`enableWhen` принимают сигналы (`model.$.x`), НЕ строковые пути.
|
|
1737
2001
|
|
|
1738
|
-
##
|
|
2002
|
+
## 26. 17. COMPUTE vs ONCHANGE
|
|
1739
2003
|
|
|
1740
2004
|
Под M1 behaviors работают на сигналах модели. Есть два способа их писать:
|
|
1741
2005
|
|
|
@@ -1776,6 +2040,40 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
|
|
|
1776
2040
|
> отследится и пересчёта не будет. Читай поля по отдельности (`model.field` или `model.$.field.value`).
|
|
1777
2041
|
> `model.get()` — только вне реактивного контекста (в `onChange`, обработчиках событий).
|
|
1778
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
|
+
|
|
1779
2077
|
### computeFrom — явный список источников
|
|
1780
2078
|
|
|
1781
2079
|
Когда нужен явный контроль зависимостей — `computeFrom(sources, target, fn)`. Значения
|
|
@@ -1840,7 +2138,7 @@ const stop = watchField(model.$.country, () => { model.city = ''; });
|
|
|
1840
2138
|
порядке через реактивный граф (цель одного = источник другого). Расходящиеся взаимные
|
|
1841
2139
|
`compute` без стабилизации бросят понятную ошибку (см. `22-cycle-detection.md`).
|
|
1842
2140
|
|
|
1843
|
-
##
|
|
2141
|
+
## 27. 18. ARRAY OPERATIONS
|
|
1844
2142
|
|
|
1845
2143
|
Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);
|
|
1846
2144
|
рендер — через ноду формы (`form.items.map` / `.at`).
|
|
@@ -1948,7 +2246,7 @@ const percentagesSumTo100: ModelValidator<unknown, unknown, MyForm> = (_v, _s, r
|
|
|
1948
2246
|
};
|
|
1949
2247
|
```
|
|
1950
2248
|
|
|
1951
|
-
##
|
|
2249
|
+
## 28. Как устроено под M1
|
|
1952
2250
|
|
|
1953
2251
|
Behaviors на сигналах уже защищены от типичных циклов:
|
|
1954
2252
|
|
|
@@ -1961,7 +2259,7 @@ Behaviors на сигналах уже защищены от типичных ц
|
|
|
1961
2259
|
Тебе НЕ нужно вручную ставить `{ immediate: false }`, guard'ить `disabled.value` перед
|
|
1962
2260
|
`disable()` или сравнивать значения перед записью — это делается внутри операторов.
|
|
1963
2261
|
|
|
1964
|
-
##
|
|
2262
|
+
## 29. Когда всё-таки возникает «Cycle detected»
|
|
1965
2263
|
|
|
1966
2264
|
### 1. Расходящийся взаимный compute
|
|
1967
2265
|
|
|
@@ -2008,7 +2306,7 @@ resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
|
|
|
2008
2306
|
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
2009
2307
|
```
|
|
2010
2308
|
|
|
2011
|
-
##
|
|
2309
|
+
## 30. Prefer built-in operators over manual logic
|
|
2012
2310
|
|
|
2013
2311
|
Вместо ручной сборки reset-on-disable через `onChange` — используй `enableWhen({ resetOnDisable })`:
|
|
2014
2312
|
|
|
@@ -2026,21 +2324,21 @@ enableWhen([model.$.propertyValue, model.$.initialPayment], () => model.loanType
|
|
|
2026
2324
|
});
|
|
2027
2325
|
```
|
|
2028
2326
|
|
|
2029
|
-
##
|
|
2327
|
+
## 31. Key Rules
|
|
2030
2328
|
|
|
2031
2329
|
1. Производные значения — через `compute`/`computeFrom` (peek-guard встроен), не ручным `onChange` + запись.
|
|
2032
2330
|
2. `transformValue`-трансформер должен быть идемпотентным (`f(f(x)) === f(x)`).
|
|
2033
2331
|
3. Условие `resetWhen`/`copyFrom`/`enableWhen` НЕ должно читать собственную цель.
|
|
2034
2332
|
4. Расходящийся взаимный `compute` — разорви однонаправленной зависимостью или `when`.
|
|
2035
2333
|
|
|
2036
|
-
##
|
|
2334
|
+
## 32. Purpose
|
|
2037
2335
|
|
|
2038
2336
|
`copyFrom` декларативно копирует значение одного поля (или группы) в другое при выполнении
|
|
2039
2337
|
условия `when`. Используется для UX-сценариев «совпадает с …»: «адрес проживания = адрес
|
|
2040
2338
|
регистрации», «email для уведомлений = основной email», «billing = shipping». Оператор сам
|
|
2041
2339
|
выходит из reactive-контекста (`runOutsideEffect`), поэтому не порождает «Cycle detected».
|
|
2042
2340
|
|
|
2043
|
-
##
|
|
2341
|
+
## 33. API
|
|
2044
2342
|
|
|
2045
2343
|
Есть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):
|
|
2046
2344
|
|
|
@@ -2065,7 +2363,7 @@ function copyFrom<T>(
|
|
|
2065
2363
|
`when` — реактивное условие (читает сигналы модели через `model.*`). `transform` применяется
|
|
2066
2364
|
к значению перед записью в target. Отдельной опции `fields`/`debounce` нет.
|
|
2067
2365
|
|
|
2068
|
-
##
|
|
2366
|
+
## 34. Examples
|
|
2069
2367
|
|
|
2070
2368
|
### Базовый сценарий — синхронизация двух адресов
|
|
2071
2369
|
|
|
@@ -2133,7 +2431,7 @@ const cleanups = [
|
|
|
2133
2431
|
// teardown: cleanups.forEach((c) => c());
|
|
2134
2432
|
```
|
|
2135
2433
|
|
|
2136
|
-
##
|
|
2434
|
+
## 35. Anti-patterns
|
|
2137
2435
|
|
|
2138
2436
|
```typescript
|
|
2139
2437
|
// ❌ Двусторонняя связь через два copyFrom — конфликт направлений
|
|
@@ -2152,7 +2450,7 @@ copyFrom(model.$.source, model.$.target, { when: () => model.target === '' });
|
|
|
2152
2450
|
copyFrom(model.$.source, model.$.target, { when: () => model.copyEnabled === true });
|
|
2153
2451
|
```
|
|
2154
2452
|
|
|
2155
|
-
##
|
|
2453
|
+
## 36. Troubleshooting
|
|
2156
2454
|
|
|
2157
2455
|
**Q: Скопированное значение не появляется в target.**
|
|
2158
2456
|
A: Проверьте, что (1) behavior передан в `createForm({ behavior })` (или примитив вызван и не
|
|
@@ -2166,14 +2464,14 @@ source и независимых флагов.
|
|
|
2166
2464
|
A: `copyFrom` при `when === false` просто не пишет (не сбрасывает). Для сброса используй параллельно
|
|
2167
2465
|
`resetWhen(model.$.target, () => !model.copyEnabled)` (см. `25-reset-when.md`).
|
|
2168
2466
|
|
|
2169
|
-
##
|
|
2467
|
+
## 37. See also
|
|
2170
2468
|
|
|
2171
2469
|
- [24-sync-fields.md](./24-sync-fields.md) — двусторонняя синхронизация
|
|
2172
2470
|
- [25-reset-when.md](./25-reset-when.md) — сброс target при выключенном условии
|
|
2173
2471
|
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
|
|
2174
2472
|
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `runOutsideEffect` защищает от циклов
|
|
2175
2473
|
|
|
2176
|
-
##
|
|
2474
|
+
## 38. Purpose
|
|
2177
2475
|
|
|
2178
2476
|
`syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них
|
|
2179
2477
|
переписывает второе. Применяется для дублей одного значения в разных частях формы
|
|
@@ -2181,7 +2479,7 @@ A: `copyFrom` при `when === false` просто не пишет (не сбр
|
|
|
2181
2479
|
исключают петли. Для одностороннего копирования — [`copyFrom`](./23-copy-from.md); для
|
|
2182
2480
|
расчётов — [`compute`](./20-compute-vs-watch.md).
|
|
2183
2481
|
|
|
2184
|
-
##
|
|
2482
|
+
## 39. API
|
|
2185
2483
|
|
|
2186
2484
|
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
2187
2485
|
|
|
@@ -2196,7 +2494,7 @@ function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (valu
|
|
|
2196
2494
|
`a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:
|
|
2197
2495
|
применяется только при движении значения `a → b`. Опции `debounce`/`when` нет.
|
|
2198
2496
|
|
|
2199
|
-
##
|
|
2497
|
+
## 40. Examples
|
|
2200
2498
|
|
|
2201
2499
|
### Базовый сценарий — отзеркаливание текста
|
|
2202
2500
|
|
|
@@ -2235,7 +2533,7 @@ const stop = syncFields(model.$.syncField1, model.$.syncField2);
|
|
|
2235
2533
|
// stop() — отписаться
|
|
2236
2534
|
```
|
|
2237
2535
|
|
|
2238
|
-
##
|
|
2536
|
+
## 41. Anti-patterns
|
|
2239
2537
|
|
|
2240
2538
|
```typescript
|
|
2241
2539
|
// ❌ Симметрично через два copyFrom — конфликт направлений
|
|
@@ -2265,7 +2563,7 @@ syncFields(model.$.amountString, model.$.amountNumber); // string ↔ number
|
|
|
2265
2563
|
compute(model.$.amountNumber, () => Number(model.amountString));
|
|
2266
2564
|
```
|
|
2267
2565
|
|
|
2268
|
-
##
|
|
2566
|
+
## 42. Troubleshooting
|
|
2269
2567
|
|
|
2270
2568
|
**Q: Поля «дёргаются», несколько перезаписей.**
|
|
2271
2569
|
A: Чаще всего на одном из полей висит `transformValue`/`compute`. Убедитесь, что transform
|
|
@@ -2279,21 +2577,21 @@ A: Не вешайте дополнительно `onChange`/`watchField`, ко
|
|
|
2279
2577
|
A: У `syncFields` нет `when`. Эмулируй через два `copyFrom(a→b, { when })` / `copyFrom(b→a, { when })`
|
|
2280
2578
|
с флагами, разрешающими только одну активную сторону, либо через `apply` под условием.
|
|
2281
2579
|
|
|
2282
|
-
##
|
|
2580
|
+
## 43. See also
|
|
2283
2581
|
|
|
2284
2582
|
- [23-copy-from.md](./23-copy-from.md) — однонаправленное копирование с `when`
|
|
2285
2583
|
- [26-transform-value.md](./26-transform-value.md) — нормализация значений на месте
|
|
2286
2584
|
- [22-cycle-detection.md](./22-cycle-detection.md) — почему симметричный copy ломается
|
|
2287
2585
|
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
|
|
2288
2586
|
|
|
2289
|
-
##
|
|
2587
|
+
## 44. Purpose
|
|
2290
2588
|
|
|
2291
2589
|
`resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это
|
|
2292
2590
|
альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но
|
|
2293
2591
|
содержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но
|
|
2294
2592
|
поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение.
|
|
2295
2593
|
|
|
2296
|
-
##
|
|
2594
|
+
## 45. API
|
|
2297
2595
|
|
|
2298
2596
|
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
2299
2597
|
|
|
@@ -2309,7 +2607,7 @@ function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { r
|
|
|
2309
2607
|
`resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
|
|
2310
2608
|
поля не трогаются оператором (значения принадлежат модели).
|
|
2311
2609
|
|
|
2312
|
-
##
|
|
2610
|
+
## 46. Examples
|
|
2313
2611
|
|
|
2314
2612
|
### Базовый сценарий — сброс номера карты при смене способа оплаты
|
|
2315
2613
|
|
|
@@ -2345,7 +2643,7 @@ import { resetWhen } from '@reformer/core';
|
|
|
2345
2643
|
const stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
2346
2644
|
```
|
|
2347
2645
|
|
|
2348
|
-
##
|
|
2646
|
+
## 47. Anti-patterns
|
|
2349
2647
|
|
|
2350
2648
|
```typescript
|
|
2351
2649
|
// ❌ resetWhen вместо enableWhen для disable-сценария
|
|
@@ -2380,7 +2678,7 @@ resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
|
|
|
2380
2678
|
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
|
|
2381
2679
|
```
|
|
2382
2680
|
|
|
2383
|
-
##
|
|
2681
|
+
## 48. Troubleshooting
|
|
2384
2682
|
|
|
2385
2683
|
**Q: Сброс не срабатывает, хотя `condition` возвращает true.**
|
|
2386
2684
|
A: Проверьте, что behavior зарегистрирован (`createForm({ behavior })`) или примитив не отписан,
|
|
@@ -2394,13 +2692,13 @@ A: Убедитесь, что `condition` не зависит от значен
|
|
|
2394
2692
|
A: `resetWhen` рассчитан на скалярные сигналы. Для группы используй `enableWhen({ resetOnDisable: true })`
|
|
2395
2693
|
(проходит по поддереву) или `model.reset()` для сброса всей формы к initial-снимку.
|
|
2396
2694
|
|
|
2397
|
-
##
|
|
2695
|
+
## 49. See also
|
|
2398
2696
|
|
|
2399
2697
|
- [04-common-patterns.md](./04-common-patterns.md) — `enableWhen({ resetOnDisable: true })` как альтернатива
|
|
2400
2698
|
- [23-copy-from.md](./23-copy-from.md) — копирование, у которого нет встроенного отката
|
|
2401
2699
|
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `condition` не должен читать целевое поле
|
|
2402
2700
|
|
|
2403
|
-
##
|
|
2701
|
+
## 50. Purpose
|
|
2404
2702
|
|
|
2405
2703
|
`transformValue` подписывается на изменение поля и переписывает его трансформированной
|
|
2406
2704
|
версией: uppercase для кодов, trim+toLowerCase для email, округление для чисел. Применяется
|
|
@@ -2409,7 +2707,7 @@ A: `resetWhen` рассчитан на скалярные сигналы. Для
|
|
|
2409
2707
|
Оператор откладывает запись вне effect-контекста (`runOutsideEffect`) и не пишет, если
|
|
2410
2708
|
`transformer(value) === value` — базовый guard от циклов.
|
|
2411
2709
|
|
|
2412
|
-
##
|
|
2710
|
+
## 51. API
|
|
2413
2711
|
|
|
2414
2712
|
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
2415
2713
|
|
|
@@ -2425,7 +2723,7 @@ function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): voi
|
|
|
2425
2723
|
Дополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора
|
|
2426
2724
|
`transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.
|
|
2427
2725
|
|
|
2428
|
-
##
|
|
2726
|
+
## 52. Examples
|
|
2429
2727
|
|
|
2430
2728
|
### Базовый сценарий — uppercase для кода
|
|
2431
2729
|
|
|
@@ -2487,7 +2785,7 @@ toUpper(model.$.promoCode);
|
|
|
2487
2785
|
trim(model.$.username);
|
|
2488
2786
|
```
|
|
2489
2787
|
|
|
2490
|
-
##
|
|
2788
|
+
## 53. Anti-patterns
|
|
2491
2789
|
|
|
2492
2790
|
```typescript
|
|
2493
2791
|
// ❌ Неидемпотентный transformer — бесконечный цикл
|
|
@@ -2515,7 +2813,7 @@ transformValue(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
|
|
|
2515
2813
|
compute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
|
|
2516
2814
|
```
|
|
2517
2815
|
|
|
2518
|
-
##
|
|
2816
|
+
## 54. Troubleshooting
|
|
2519
2817
|
|
|
2520
2818
|
**Q: «Cycle detected» при transformValue.**
|
|
2521
2819
|
A: 99% — неидемпотентный transformer. Проверь `transformer(transformer(x)) === transformer(x)`.
|
|
@@ -2528,14 +2826,14 @@ A: Проверь, что behavior зарегистрирован (`createForm({
|
|
|
2528
2826
|
A: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)
|
|
2529
2827
|
лучше делать через `<InputMask>` из `@reformer/ui-kit`, а не `transformValue`.
|
|
2530
2828
|
|
|
2531
|
-
##
|
|
2829
|
+
## 55. See also
|
|
2532
2830
|
|
|
2533
2831
|
- [24-sync-fields.md](./24-sync-fields.md) — порядок применения с syncFields
|
|
2534
2832
|
- [23-copy-from.md](./23-copy-from.md) — `transform`-опция при копировании
|
|
2535
2833
|
- [22-cycle-detection.md](./22-cycle-detection.md) — про идемпотентность
|
|
2536
2834
|
- [16-ui-components.md](./16-ui-components.md) — `<InputMask>` для тяжёлого форматирования
|
|
2537
2835
|
|
|
2538
|
-
##
|
|
2836
|
+
## 56. Purpose
|
|
2539
2837
|
|
|
2540
2838
|
`revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
|
|
2541
2839
|
зависимостей. Применяется, когда правило зависит от значений других полей
|
|
@@ -2543,7 +2841,7 @@ A: Симптом частых записей. Форматирование с
|
|
|
2543
2841
|
Под M1 валидация on-demand (`validateFormModel`), поэтому ревалидация выражается явным
|
|
2544
2842
|
колбэком, а не автоматической привязкой к полю.
|
|
2545
2843
|
|
|
2546
|
-
##
|
|
2844
|
+
## 57. API
|
|
2547
2845
|
|
|
2548
2846
|
Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
|
|
2549
2847
|
|
|
@@ -2559,7 +2857,7 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
|
|
|
2559
2857
|
`() => validateFormModel(model, schema)`). Вызывается при изменении любой зависимости
|
|
2560
2858
|
(НЕ на инициализации), вне effect-контекста.
|
|
2561
2859
|
|
|
2562
|
-
##
|
|
2860
|
+
## 58. Examples
|
|
2563
2861
|
|
|
2564
2862
|
### Базовый сценарий — перевалидация amount при смене maxAmount
|
|
2565
2863
|
|
|
@@ -2612,7 +2910,7 @@ import { revalidateWhen, validateFormModel } from '@reformer/core';
|
|
|
2612
2910
|
const stop = revalidateWhen([model.$.maxAmount], () => void validateFormModel(model, schema));
|
|
2613
2911
|
```
|
|
2614
2912
|
|
|
2615
|
-
##
|
|
2913
|
+
## 59. Anti-patterns
|
|
2616
2914
|
|
|
2617
2915
|
```typescript
|
|
2618
2916
|
// ❌ Триггер == поле, которое и так меняется само
|
|
@@ -2635,7 +2933,7 @@ const amountVsMax: ModelValidator<number, unknown, Form> = (v, _s, root) =>
|
|
|
2635
2933
|
revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
|
|
2636
2934
|
```
|
|
2637
2935
|
|
|
2638
|
-
##
|
|
2936
|
+
## 60. Troubleshooting
|
|
2639
2937
|
|
|
2640
2938
|
**Q: Ошибка target не пропадает после изменения триггера.**
|
|
2641
2939
|
A: Проверьте, что (1) правило реально читает значение триггера через `root` (cross-field
|
|
@@ -2645,20 +2943,20 @@ A: Проверьте, что (1) правило реально читает з
|
|
|
2645
2943
|
**Q: Как перевалидировать только одно поле, а не всю схему?**
|
|
2646
2944
|
A: Передай в колбэк под-схему только этого поля: `validateFormModel(model, { children: [fieldNode] })`.
|
|
2647
2945
|
|
|
2648
|
-
##
|
|
2946
|
+
## 61. See also
|
|
2649
2947
|
|
|
2650
2948
|
- [03-api-signatures.md](./03-api-signatures.md) — cross-field `ModelValidator` и `validateFormModel`
|
|
2651
2949
|
- [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
|
|
2652
2950
|
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения
|
|
2653
2951
|
|
|
2654
|
-
##
|
|
2952
|
+
## 62. Purpose
|
|
2655
2953
|
|
|
2656
2954
|
Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateFormModel`) →
|
|
2657
2955
|
проверить `result.valid` → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
|
|
2658
2956
|
Валидация — чистая функция ДАННЫХ (модели), она же роутит ошибки в ноды формы, поэтому UI
|
|
2659
2957
|
подсветит проблемные поля. `model.reset()` возвращает значения к initial-снимку.
|
|
2660
2958
|
|
|
2661
|
-
##
|
|
2959
|
+
## 63. API
|
|
2662
2960
|
|
|
2663
2961
|
```typescript
|
|
2664
2962
|
// Модель (источник истины значений):
|
|
@@ -2680,7 +2978,7 @@ form.<field>.markAsTouched();
|
|
|
2680
2978
|
form.<field>.reset();
|
|
2681
2979
|
```
|
|
2682
2980
|
|
|
2683
|
-
##
|
|
2981
|
+
## 64. Examples
|
|
2684
2982
|
|
|
2685
2983
|
### Базовый submit-handler
|
|
2686
2984
|
|
|
@@ -2793,7 +3091,7 @@ async function processFormPayload(rawData: Partial<MyForm>) {
|
|
|
2793
3091
|
}
|
|
2794
3092
|
```
|
|
2795
3093
|
|
|
2796
|
-
##
|
|
3094
|
+
## 65. Anti-patterns
|
|
2797
3095
|
|
|
2798
3096
|
```typescript
|
|
2799
3097
|
// ❌ Чтение результата валидации без await
|
|
@@ -2833,7 +3131,7 @@ model.password = '';
|
|
|
2833
3131
|
model.reset();
|
|
2834
3132
|
```
|
|
2835
3133
|
|
|
2836
|
-
##
|
|
3134
|
+
## 66. Troubleshooting
|
|
2837
3135
|
|
|
2838
3136
|
**Q: После `reset()` в UI остались старые ошибки.**
|
|
2839
3137
|
A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
|
|
@@ -2848,14 +3146,14 @@ A: `reset()` возвращает к initial-снимку (значения на
|
|
|
2848
3146
|
A: `model.get()` возвращает значения всех полей. Фильтруй вручную после `get()` или используй
|
|
2849
3147
|
`enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial.
|
|
2850
3148
|
|
|
2851
|
-
##
|
|
3149
|
+
## 67. See also
|
|
2852
3150
|
|
|
2853
3151
|
- [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
|
|
2854
3152
|
- [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateFormModel`
|
|
2855
3153
|
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateFormModel`
|
|
2856
3154
|
- [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки
|
|
2857
3155
|
|
|
2858
|
-
##
|
|
3156
|
+
## 68. Purpose
|
|
2859
3157
|
|
|
2860
3158
|
Загрузка формы данными с сервера под M1: (1) **initial values** в `createModel(...)`; (2)
|
|
2861
3159
|
**`model.set` / `model.patch`** для загрузки/обновления значений; (3) **external React-hook**
|
|
@@ -2864,7 +3162,7 @@ race-guard через deps). Динамические `componentProps` (опци
|
|
|
2864
3162
|
`form.field.updateComponentProps({ options })` в `queueMicrotask`, чтобы не пересечься с
|
|
2865
3163
|
реактивными эффектами от `set`/`patch`.
|
|
2866
3164
|
|
|
2867
|
-
##
|
|
3165
|
+
## 69. API
|
|
2868
3166
|
|
|
2869
3167
|
```typescript
|
|
2870
3168
|
// Модель:
|
|
@@ -2880,7 +3178,7 @@ form.field.updateComponentProps(props: Record<string, unknown>): void; // дин
|
|
|
2880
3178
|
Initial values задаются в `createModel(initial)`; `reset()` возвращает к ним. Чтобы загруженные
|
|
2881
3179
|
данные стали новой «точкой отсчёта» для `reset()` — вызови `model.captureInitial()` после load.
|
|
2882
3180
|
|
|
2883
|
-
##
|
|
3181
|
+
## 70. Examples
|
|
2884
3182
|
|
|
2885
3183
|
### Initial values в модели
|
|
2886
3184
|
|
|
@@ -2987,7 +3285,7 @@ export const addressBehavior = defineFormBehavior<AddressForm>(({ model, form })
|
|
|
2987
3285
|
});
|
|
2988
3286
|
```
|
|
2989
3287
|
|
|
2990
|
-
##
|
|
3288
|
+
## 71. Anti-patterns
|
|
2991
3289
|
|
|
2992
3290
|
```typescript
|
|
2993
3291
|
// ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз
|
|
@@ -3027,7 +3325,7 @@ useEffect(() => {
|
|
|
3027
3325
|
}, [id]);
|
|
3028
3326
|
```
|
|
3029
3327
|
|
|
3030
|
-
##
|
|
3328
|
+
## 72. Troubleshooting
|
|
3031
3329
|
|
|
3032
3330
|
**Q: После `set`/`patch` не показываются ошибки.**
|
|
3033
3331
|
A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateFormModel(model, schema)`.
|
|
@@ -3040,14 +3338,14 @@ A: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что
|
|
|
3040
3338
|
A: `reset()` возвращает к initial-снимку из `createModel`, не к последнему `set`. Чтобы «сбросить
|
|
3041
3339
|
к данным с сервера» — после `set` вызови `model.captureInitial()` (новая точка отсчёта).
|
|
3042
3340
|
|
|
3043
|
-
##
|
|
3341
|
+
## 73. See also
|
|
3044
3342
|
|
|
3045
3343
|
- [28-submit-and-reset.md](./28-submit-and-reset.md) — обратная сторона жизненного цикла
|
|
3046
3344
|
- [11-async-watchfield.md](./11-async-watchfield.md) — `onChange` для динамики после preload
|
|
3047
3345
|
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `queueMicrotask` нужен
|
|
3048
3346
|
- [16-ui-components.md](./16-ui-components.md) — `updateComponentProps` для динамических опций
|
|
3049
3347
|
|
|
3050
|
-
##
|
|
3348
|
+
## 74. 30. TYPE-SAFETY RECIPES
|
|
3051
3349
|
|
|
3052
3350
|
Идиоматичные паттерны, которые держат сгенерированный код без `any` и `as`-кастов под M1.
|
|
3053
3351
|
|
|
@@ -3198,7 +3496,7 @@ const schema = {
|
|
|
3198
3496
|
- `as`-касты значений полей внутри `compute` → читай `model.field` напрямую (Recipe 5).
|
|
3199
3497
|
- строковые пути / `(form) => ...` в behaviors → это удалённый API, используй сигналы (`model.$.x`).
|
|
3200
3498
|
|
|
3201
|
-
##
|
|
3499
|
+
## 75. 31. ASYNC VALIDATOR
|
|
3202
3500
|
|
|
3203
3501
|
Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
|
|
3204
3502
|
async-валидатор это `ModelValidator`, возвращающий `Promise<ValidationError | null>`. Он
|
|
@@ -3272,7 +3570,7 @@ return pending ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
|
|
|
3272
3570
|
- [29-async-preload.md](29-async-preload.md) — async preload данных при init формы
|
|
3273
3571
|
- [03-api-signatures.md](03-api-signatures.md) — `ModelValidator` и `validateFormModel`
|
|
3274
3572
|
|
|
3275
|
-
##
|
|
3573
|
+
## 76. 32. ASYNC OPTIONS LOADING
|
|
3276
3574
|
|
|
3277
3575
|
Для динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,
|
|
3278
3576
|
`carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +
|
|
@@ -3381,7 +3679,7 @@ loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: tr
|
|
|
3381
3679
|
- [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
|
|
3382
3680
|
- [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)
|
|
3383
3681
|
|
|
3384
|
-
##
|
|
3682
|
+
## 77. API Reference
|
|
3385
3683
|
|
|
3386
3684
|
_Auto-generated from JSDoc on public exports._
|
|
3387
3685
|
|
|
@@ -3407,7 +3705,7 @@ _Source: src/core/types/index.ts_
|
|
|
3407
3705
|
|
|
3408
3706
|
**Signature:**
|
|
3409
3707
|
```typescript
|
|
3410
|
-
export interface ArrayConfig<T extends
|
|
3708
|
+
export interface ArrayConfig<T extends object> {
|
|
3411
3709
|
itemSchema: FormSchema<T>;
|
|
3412
3710
|
initial?: Partial<T>[];
|
|
3413
3711
|
}
|
|
@@ -3614,16 +3912,16 @@ _Source: src/core/types/index.ts_
|
|
|
3614
3912
|
|
|
3615
3913
|
**Kind:** `type`
|
|
3616
3914
|
|
|
3617
|
-
Чистый асинхронный валидатор поля.
|
|
3618
|
-
|
|
3619
|
-
|
|
3915
|
+
Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
|
|
3916
|
+
{@link Validator}, но возвращает `Promise`. Регистрируется через
|
|
3917
|
+
`validateAsync(path, validator, { debounce })`.
|
|
3620
3918
|
|
|
3621
3919
|
**Signature:**
|
|
3622
3920
|
```typescript
|
|
3623
3921
|
export type AsyncValidator<TForm, TField> = (
|
|
3624
3922
|
value: TField,
|
|
3625
|
-
|
|
3626
|
-
root:
|
|
3923
|
+
scope: unknown,
|
|
3924
|
+
root: FormModel<TForm>
|
|
3627
3925
|
) => Promise<ValidationError | null>;
|
|
3628
3926
|
```
|
|
3629
3927
|
|
|
@@ -3925,10 +4223,10 @@ export interface CreateFormFromModelArgs<T> {
|
|
|
3925
4223
|
/** Реактивная модель данных (источник истины значений). */
|
|
3926
4224
|
model: FormModel<T>;
|
|
3927
4225
|
/**
|
|
3928
|
-
* Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг
|
|
3929
|
-
* по идентичности сигнала (`node.value === model.$.path`). Опциональна.
|
|
4226
|
+
* Единая Schema (дерево узлов {@link FormSchemaNode}). createForm обходит её и привязывает конфиг
|
|
4227
|
+
* поля к ноде по идентичности сигнала (`node.value === model.$.path`). Опциональна.
|
|
3930
4228
|
*/
|
|
3931
|
-
schema?:
|
|
4229
|
+
schema?: FormSchemaNode;
|
|
3932
4230
|
/**
|
|
3933
4231
|
* Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
|
|
3934
4232
|
* заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
|
|
@@ -4081,7 +4379,7 @@ export interface ErrorFilterOptions {
|
|
|
4081
4379
|
message?: string;
|
|
4082
4380
|
|
|
4083
4381
|
/** Фильтр по параметрам ошибки */
|
|
4084
|
-
params?:
|
|
4382
|
+
params?: Record<string, FormValue>;
|
|
4085
4383
|
|
|
4086
4384
|
/** Кастомный предикат для фильтрации */
|
|
4087
4385
|
predicate?: (error: ValidationError) => boolean;
|
|
@@ -4334,8 +4632,7 @@ export interface FieldControlState<T> {
|
|
|
4334
4632
|
* );
|
|
4335
4633
|
* ```
|
|
4336
4634
|
*/
|
|
4337
|
-
|
|
4338
|
-
componentProps: Record<string, any>;
|
|
4635
|
+
componentProps: Record<string, unknown>;
|
|
4339
4636
|
}
|
|
4340
4637
|
```
|
|
4341
4638
|
|
|
@@ -4599,22 +4896,6 @@ try {
|
|
|
4599
4896
|
|
|
4600
4897
|
_Source: src/core/utils/error-handler.ts_
|
|
4601
4898
|
|
|
4602
|
-
### FormFields
|
|
4603
|
-
|
|
4604
|
-
**Kind:** `type`
|
|
4605
|
-
|
|
4606
|
-
Тип для конфига с полями (FormSchema generic constraint)
|
|
4607
|
-
Используется вместо `Record<string, any>` для схем форм
|
|
4608
|
-
|
|
4609
|
-
**Signature:**
|
|
4610
|
-
```typescript
|
|
4611
|
-
export type FormFields = Record<string, FormValue>;
|
|
4612
|
-
```
|
|
4613
|
-
|
|
4614
|
-
**Deprecated:** (no message)
|
|
4615
|
-
|
|
4616
|
-
_Source: src/core/types/index.ts_
|
|
4617
|
-
|
|
4618
4899
|
### FormModel
|
|
4619
4900
|
|
|
4620
4901
|
**Kind:** `type`
|
|
@@ -4711,12 +4992,17 @@ _Source: src/core/types/form-proxy.ts_
|
|
|
4711
4992
|
|
|
4712
4993
|
**Kind:** `type`
|
|
4713
4994
|
|
|
4714
|
-
|
|
4995
|
+
**Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
|
|
4996
|
+
(или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
|
|
4715
4997
|
- `T[] -> [FormSchema<T>]` (массив с одним элементом)
|
|
4716
4998
|
- `object -> FormSchema<T>` (группа)
|
|
4717
4999
|
- `primitive -> FieldConfig<T>` (поле)
|
|
4718
5000
|
|
|
4719
|
-
Использует NonNullable для корректной обработки опциональных
|
|
5001
|
+
Использует NonNullable для корректной обработки опциональных полей.
|
|
5002
|
+
|
|
5003
|
+
⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
|
|
5004
|
+
контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
|
|
5005
|
+
(ключи = поля данных), а не узел дерева.
|
|
4720
5006
|
|
|
4721
5007
|
**Signature:**
|
|
4722
5008
|
```typescript
|
|
@@ -4765,6 +5051,65 @@ const schema: FormSchema<Form> = {
|
|
|
4765
5051
|
|
|
4766
5052
|
_Source: src/core/types/deep-schema.ts_
|
|
4767
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
|
+
|
|
4768
5113
|
### FormStatusMachine
|
|
4769
5114
|
|
|
4770
5115
|
**Kind:** `class`
|
|
@@ -4810,7 +5155,7 @@ FormSubmitter - управляет процессом отправки форм
|
|
|
4810
5155
|
|
|
4811
5156
|
**Signature:**
|
|
4812
5157
|
```typescript
|
|
4813
|
-
export class FormSubmitter<T extends
|
|
5158
|
+
export class FormSubmitter<T extends object> {
|
|
4814
5159
|
/** Внутренний сигнал состояния отправки */ /* … */ }
|
|
4815
5160
|
```
|
|
4816
5161
|
|
|
@@ -5018,7 +5363,7 @@ _Source: src/core/types/index.ts_
|
|
|
5018
5363
|
|
|
5019
5364
|
**Signature:**
|
|
5020
5365
|
```typescript
|
|
5021
|
-
export function integer<TForm = unknown, TField extends number | undefined = number>(
|
|
5366
|
+
export function integer<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5022
5367
|
options?: ValidateOptions
|
|
5023
5368
|
): Validator<TForm, TField>
|
|
5024
5369
|
```
|
|
@@ -5055,7 +5400,7 @@ ArrayNode представляет массив вложенных форм (о
|
|
|
5055
5400
|
|
|
5056
5401
|
**Signature:**
|
|
5057
5402
|
```typescript
|
|
5058
|
-
export function isArrayNode(value: unknown): value is ArrayNode<
|
|
5403
|
+
export function isArrayNode(value: unknown): value is ArrayNode<object>
|
|
5059
5404
|
```
|
|
5060
5405
|
|
|
5061
5406
|
**Parameters:**
|
|
@@ -5217,7 +5562,7 @@ GroupNode представляет объект с вложенными поля
|
|
|
5217
5562
|
|
|
5218
5563
|
**Signature:**
|
|
5219
5564
|
```typescript
|
|
5220
|
-
export function isGroupNode(value: unknown): value is GroupNode<
|
|
5565
|
+
export function isGroupNode(value: unknown): value is GroupNode<object>
|
|
5221
5566
|
```
|
|
5222
5567
|
|
|
5223
5568
|
**Parameters:**
|
|
@@ -5247,7 +5592,7 @@ _Source: src/core/utils/type-guards.ts_
|
|
|
5247
5592
|
|
|
5248
5593
|
**Signature:**
|
|
5249
5594
|
```typescript
|
|
5250
|
-
export function isNumber<TForm = unknown, TField extends number | undefined = number>(
|
|
5595
|
+
export function isNumber<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5251
5596
|
options?: ValidateOptions
|
|
5252
5597
|
): Validator<TForm, TField>
|
|
5253
5598
|
```
|
|
@@ -5315,7 +5660,7 @@ _Source: src/core/utils/derived-registry.ts_
|
|
|
5315
5660
|
|
|
5316
5661
|
**Signature:**
|
|
5317
5662
|
```typescript
|
|
5318
|
-
export function max<TForm = unknown, TField extends number | undefined = number>(
|
|
5663
|
+
export function max<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5319
5664
|
maxValue: number,
|
|
5320
5665
|
options?: ValidateOptions
|
|
5321
5666
|
): Validator<TForm, TField>
|
|
@@ -5477,7 +5822,7 @@ _Source: src/core/validation/validators/max-length.ts_
|
|
|
5477
5822
|
|
|
5478
5823
|
**Signature:**
|
|
5479
5824
|
```typescript
|
|
5480
|
-
export function min<TForm = unknown, TField extends number | undefined = number>(
|
|
5825
|
+
export function min<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5481
5826
|
minValue: number,
|
|
5482
5827
|
options?: ValidateOptions
|
|
5483
5828
|
): Validator<TForm, TField>
|
|
@@ -5645,9 +5990,17 @@ export interface ModelApi<T> {
|
|
|
5645
5990
|
readonly $: ModelSignals<T>;
|
|
5646
5991
|
/** Снимок значений (без подписки) — для submit. */
|
|
5647
5992
|
get(): T;
|
|
5648
|
-
/**
|
|
5993
|
+
/**
|
|
5994
|
+
* @deprecated Алиас {@link ModelApi.patch}: `set` выполняет то же частичное **слияние** —
|
|
5995
|
+
* обновляет только переданные ключи, отсутствующие НЕ трогает (это НЕ полная замена, вопреки
|
|
5996
|
+
* старому названию). Используйте `patch`; для замены с обнулением лишних полей делайте
|
|
5997
|
+
* `patch({ ...defaults, ...dto })` явно. Не меняет initial-снимок.
|
|
5998
|
+
*/
|
|
5649
5999
|
set(value: Partial<T>): void;
|
|
5650
|
-
/**
|
|
6000
|
+
/**
|
|
6001
|
+
* Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
|
|
6002
|
+
* отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
|
|
6003
|
+
*/
|
|
5651
6004
|
patch(value: Partial<T>): void;
|
|
5652
6005
|
/** Отличаются ли текущие значения от initial-снимка (value-diff). */
|
|
5653
6006
|
isDirty(): boolean;
|
|
@@ -5672,6 +6025,11 @@ _Source: src/core/model/types.ts_
|
|
|
5672
6025
|
**Signature:**
|
|
5673
6026
|
```typescript
|
|
5674
6027
|
export interface ModelArray<U> {
|
|
6028
|
+
/**
|
|
6029
|
+
* Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
|
|
6030
|
+
* рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
|
|
6031
|
+
*/
|
|
6032
|
+
readonly __path: string;
|
|
5675
6033
|
/** Реактивная длина. */
|
|
5676
6034
|
readonly length: number;
|
|
5677
6035
|
/** Добавить элемент в конец (значение элемента целиком). */
|
|
@@ -5686,12 +6044,14 @@ export interface ModelArray<U> {
|
|
|
5686
6044
|
swap(a: number, b: number): void;
|
|
5687
6045
|
/** Очистить массив. */
|
|
5688
6046
|
clear(): void;
|
|
5689
|
-
/**
|
|
5690
|
-
at(
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
/**
|
|
5694
|
-
|
|
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;
|
|
5695
6055
|
/** Снимок массива значений (без подписки). */
|
|
5696
6056
|
toArray(): U[];
|
|
5697
6057
|
/** Индексный value-доступ. */
|
|
@@ -5798,13 +6158,7 @@ _Source: src/core/model/types.ts_
|
|
|
5798
6158
|
**Signature:**
|
|
5799
6159
|
```typescript
|
|
5800
6160
|
export type ModelSignals<T> = {
|
|
5801
|
-
[K in keyof T]:
|
|
5802
|
-
? { readonly length: number; readonly [index: number]: ModelSignals<U & object> }
|
|
5803
|
-
: NonNullable<T[K]> extends Opaque
|
|
5804
|
-
? PathAwareSignal<T[K]>
|
|
5805
|
-
: NonNullable<T[K]> extends object
|
|
5806
|
-
? ModelSignals<NonNullable<T[K]>>
|
|
5807
|
-
: PathAwareSignal<T[K]>;
|
|
6161
|
+
[K in keyof T]: ModelSignalNode<T[K]>;
|
|
5808
6162
|
};
|
|
5809
6163
|
```
|
|
5810
6164
|
|
|
@@ -5831,9 +6185,12 @@ _Source: src/core/model/validate-model.ts_
|
|
|
5831
6185
|
|
|
5832
6186
|
**Kind:** `type`
|
|
5833
6187
|
|
|
5834
|
-
Валидатор слоя
|
|
6188
|
+
Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
|
|
5835
6189
|
массива или корень); `root` — корневая модель.
|
|
5836
6190
|
|
|
6191
|
+
Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
|
|
6192
|
+
`FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
|
|
6193
|
+
|
|
5837
6194
|
**Signature:**
|
|
5838
6195
|
```typescript
|
|
5839
6196
|
export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
|
|
@@ -5879,7 +6236,7 @@ _Source: src/core/model/types.ts_
|
|
|
5879
6236
|
|
|
5880
6237
|
**Signature:**
|
|
5881
6238
|
```typescript
|
|
5882
|
-
export function multipleOf<TForm = unknown, TField extends number | undefined = number>(
|
|
6239
|
+
export function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5883
6240
|
divisor: number,
|
|
5884
6241
|
options?: ValidateOptions
|
|
5885
6242
|
): Validator<TForm, TField>
|
|
@@ -5988,7 +6345,7 @@ _Source: src/core/factories/node-factory.ts_
|
|
|
5988
6345
|
|
|
5989
6346
|
**Signature:**
|
|
5990
6347
|
```typescript
|
|
5991
|
-
export function nonNegative<TForm = unknown, TField extends number | undefined = number>(
|
|
6348
|
+
export function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5992
6349
|
options?: ValidateOptions
|
|
5993
6350
|
): Validator<TForm, TField>
|
|
5994
6351
|
```
|
|
@@ -6024,7 +6381,7 @@ _Source: src/core/validation/validators/non-negative.ts_
|
|
|
6024
6381
|
|
|
6025
6382
|
**Signature:**
|
|
6026
6383
|
```typescript
|
|
6027
|
-
export function nonZero<TForm = unknown, TField extends number | undefined = number>(
|
|
6384
|
+
export function nonZero<TForm = unknown, TField extends number | null | undefined = number>(
|
|
6028
6385
|
options?: ValidateOptions
|
|
6029
6386
|
): Validator<TForm, TField>
|
|
6030
6387
|
```
|
|
@@ -6450,6 +6807,48 @@ return effect(() => {
|
|
|
6450
6807
|
|
|
6451
6808
|
_Source: src/core/utils/safe-effect.ts_
|
|
6452
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
|
+
|
|
6453
6852
|
### SetValueOptions
|
|
6454
6853
|
|
|
6455
6854
|
**Kind:** `interface`
|
|
@@ -6534,7 +6933,7 @@ _Source: src/core/utils/form-submitter.ts_
|
|
|
6534
6933
|
|
|
6535
6934
|
**Signature:**
|
|
6536
6935
|
```typescript
|
|
6537
|
-
export interface SubmittableForm<T extends
|
|
6936
|
+
export interface SubmittableForm<T extends object> {
|
|
6538
6937
|
/** Пометить все поля как touched */
|
|
6539
6938
|
markAsTouched(): void;
|
|
6540
6939
|
/** Валидировать форму */
|
|
@@ -6700,7 +7099,7 @@ _Source: src/core/types/index.ts_
|
|
|
6700
7099
|
|
|
6701
7100
|
**Signature:**
|
|
6702
7101
|
```typescript
|
|
6703
|
-
export type UnknownRecord = Record<string,
|
|
7102
|
+
export type UnknownRecord = Record<string, unknown>;
|
|
6704
7103
|
```
|
|
6705
7104
|
|
|
6706
7105
|
_Source: src/core/types/index.ts_
|
|
@@ -6761,7 +7160,7 @@ React-хук для подписки только на длину массива
|
|
|
6761
7160
|
|
|
6762
7161
|
**Signature:**
|
|
6763
7162
|
```typescript
|
|
6764
|
-
export function useArrayLength<T extends
|
|
7163
|
+
export function useArrayLength<T extends object>(control: ArrayNode<T>): number
|
|
6765
7164
|
```
|
|
6766
7165
|
|
|
6767
7166
|
**Parameters:**
|
|
@@ -6815,8 +7214,8 @@ React-хук для подписки на состояние формы (FieldNo
|
|
|
6815
7214
|
**Signature:**
|
|
6816
7215
|
```typescript
|
|
6817
7216
|
export function useFormControl(
|
|
6818
|
-
control: FieldNode<FormValue> | ArrayNode<
|
|
6819
|
-
): FieldControlState<FormValue> | ArrayControlState<
|
|
7217
|
+
control: FieldNode<FormValue> | ArrayNode<object> | undefined
|
|
7218
|
+
): FieldControlState<FormValue> | ArrayControlState<object>
|
|
6820
7219
|
```
|
|
6821
7220
|
|
|
6822
7221
|
**Parameters:**
|
|
@@ -7230,7 +7629,7 @@ In-form валидация: прогоняет {@link validateModel} и роут
|
|
|
7230
7629
|
```typescript
|
|
7231
7630
|
export async function validateFormModel<T>(
|
|
7232
7631
|
model: FormModel<T>,
|
|
7233
|
-
schema:
|
|
7632
|
+
schema: FormSchemaNode
|
|
7234
7633
|
): Promise<ModelValidationResult>
|
|
7235
7634
|
```
|
|
7236
7635
|
|
|
@@ -7262,7 +7661,7 @@ Headless-валидация данных (sync + async). Работает без
|
|
|
7262
7661
|
```typescript
|
|
7263
7662
|
export async function validateModel<T>(
|
|
7264
7663
|
model: FormModel<T>,
|
|
7265
|
-
schema:
|
|
7664
|
+
schema: FormSchemaNode
|
|
7266
7665
|
): Promise<ModelValidationResult>
|
|
7267
7666
|
```
|
|
7268
7667
|
|
|
@@ -7283,7 +7682,10 @@ _Source: src/core/model/validate-model.ts_
|
|
|
7283
7682
|
|
|
7284
7683
|
**Signature:**
|
|
7285
7684
|
```typescript
|
|
7286
|
-
export function validateModelSync<T>(
|
|
7685
|
+
export function validateModelSync<T>(
|
|
7686
|
+
model: FormModel<T>,
|
|
7687
|
+
schema: FormSchemaNode
|
|
7688
|
+
): ModelValidationResult
|
|
7287
7689
|
```
|
|
7288
7690
|
|
|
7289
7691
|
**Parameters:**
|
|
@@ -7315,7 +7717,7 @@ export interface ValidateOptions {
|
|
|
7315
7717
|
/** Сообщение об ошибке */
|
|
7316
7718
|
message?: string;
|
|
7317
7719
|
/** Параметры ошибки */
|
|
7318
|
-
params?:
|
|
7720
|
+
params?: Record<string, FormValue>;
|
|
7319
7721
|
}
|
|
7320
7722
|
```
|
|
7321
7723
|
|
|
@@ -7332,7 +7734,7 @@ _Source: src/core/types/validation-schema.ts_
|
|
|
7332
7734
|
export interface ValidationError {
|
|
7333
7735
|
code: string;
|
|
7334
7736
|
message: string;
|
|
7335
|
-
params?:
|
|
7737
|
+
params?: Record<string, FormValue>;
|
|
7336
7738
|
/** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
|
|
7337
7739
|
severity?: 'error' | 'warning';
|
|
7338
7740
|
}
|
|
@@ -7346,15 +7748,25 @@ _Source: src/core/types/index.ts_
|
|
|
7346
7748
|
|
|
7347
7749
|
Чистый синхронный валидатор поля.
|
|
7348
7750
|
|
|
7349
|
-
|
|
7350
|
-
|
|
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
|
+
дополнительные аргументы игнорируют.
|
|
7351
7763
|
|
|
7352
7764
|
**Signature:**
|
|
7353
7765
|
```typescript
|
|
7354
7766
|
export type Validator<TForm, TField> = (
|
|
7355
7767
|
value: TField,
|
|
7356
|
-
|
|
7357
|
-
root:
|
|
7768
|
+
scope: unknown,
|
|
7769
|
+
root: FormModel<TForm>
|
|
7358
7770
|
) => ValidationError | null;
|
|
7359
7771
|
```
|
|
7360
7772
|
|
|
@@ -7362,8 +7774,9 @@ export type Validator<TForm, TField> = (
|
|
|
7362
7774
|
|
|
7363
7775
|
Кастомный валидатор в массиве `validators` поля схемы
|
|
7364
7776
|
```typescript
|
|
7365
|
-
const isAdult: Validator<MyForm, number> = (value,
|
|
7777
|
+
const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
|
|
7366
7778
|
if (value < 18) return { code: 'tooYoung', message: '18+' };
|
|
7779
|
+
// root — FormModel<MyForm>: root.someOtherField читается как значение
|
|
7367
7780
|
return null;
|
|
7368
7781
|
};
|
|
7369
7782
|
|