@reformer/core 8.0.0 → 9.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -57
- package/dist/behaviors-Br4Im38V.js +263 -0
- package/dist/behaviors.js +146 -146
- package/dist/core/factories/node-factory.d.ts +5 -4
- package/dist/core/model/behaviors.d.ts +15 -0
- 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 +39 -3
- package/dist/core/nodes/model-array-node.d.ts +13 -1
- package/dist/core/types/deep-schema.d.ts +39 -5
- package/dist/core/types/form-proxy.d.ts +27 -2
- 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 +37 -16
- package/dist/core/utils/create-form.d.ts +4 -3
- package/dist/core/utils/derived-registry.d.ts +29 -0
- package/dist/core/utils/error-handler.d.ts +4 -1
- package/dist/core/utils/form-submitter.d.ts +8 -4
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/safe-effect.d.ts +27 -7
- 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/core/validation/validators/required.d.ts +2 -1
- package/dist/{date-utils-xUWFslTj.js → date-utils-dTT_x_Be.js} +12 -10
- package/dist/hooks/types.d.ts +31 -1
- package/dist/hooks/useArrayLength.d.ts +1 -2
- package/dist/hooks/useFormControl.d.ts +2 -2
- package/dist/hooks/useSignalSubscription.d.ts +8 -1
- package/dist/index.js +625 -617
- 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/dist/validators/pattern.js +7 -6
- package/dist/validators/required.js +5 -9
- package/llms.txt +715 -175
- package/package.json +6 -2
- package/dist/behaviors-O9a1Djj9.js +0 -147
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`.
|
|
1429
1529
|
|
|
1430
|
-
|
|
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`).
|
|
1534
|
+
|
|
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
|
}
|
|
@@ -3517,6 +3815,23 @@ export interface ArrayControlState<T> {
|
|
|
3517
3815
|
* ```
|
|
3518
3816
|
*/
|
|
3519
3817
|
dirty: boolean;
|
|
3818
|
+
|
|
3819
|
+
/**
|
|
3820
|
+
* Флаг отключения массива.
|
|
3821
|
+
* `true` когда массив отключён (`ArrayNode.disable()`), в том числе через
|
|
3822
|
+
* распространение disable от родительской группы.
|
|
3823
|
+
*
|
|
3824
|
+
* Используйте для отключения UI-действий добавления/удаления, когда массив
|
|
3825
|
+
* структурно неизменяем.
|
|
3826
|
+
*
|
|
3827
|
+
* @example
|
|
3828
|
+
* ```tsx
|
|
3829
|
+
* const { disabled } = useFormControl(itemsArray);
|
|
3830
|
+
*
|
|
3831
|
+
* return <button disabled={disabled} onClick={() => control.push()}>Add</button>;
|
|
3832
|
+
* ```
|
|
3833
|
+
*/
|
|
3834
|
+
disabled: boolean;
|
|
3520
3835
|
}
|
|
3521
3836
|
```
|
|
3522
3837
|
|
|
@@ -3614,19 +3929,22 @@ _Source: src/core/types/index.ts_
|
|
|
3614
3929
|
|
|
3615
3930
|
**Kind:** `type`
|
|
3616
3931
|
|
|
3617
|
-
Чистый асинхронный валидатор поля.
|
|
3618
|
-
|
|
3619
|
-
Регистрируется через `validateAsync(path, validator, { debounce })`.
|
|
3932
|
+
Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
|
|
3933
|
+
{@link Validator}, но возвращает `Promise`.
|
|
3620
3934
|
|
|
3621
3935
|
**Signature:**
|
|
3622
3936
|
```typescript
|
|
3623
3937
|
export type AsyncValidator<TForm, TField> = (
|
|
3624
3938
|
value: TField,
|
|
3625
|
-
|
|
3626
|
-
root:
|
|
3939
|
+
scope: unknown,
|
|
3940
|
+
root: FormModel<TForm>
|
|
3627
3941
|
) => Promise<ValidationError | null>;
|
|
3628
3942
|
```
|
|
3629
3943
|
|
|
3944
|
+
**Deprecated:** Осиротевший остаток удалённого оператора `validateAsync` (Ф7). Рантаймом не
|
|
3945
|
+
потребляется — живой async-путь узла поля использует `AsyncValidatorFn` `(value, { signal })`.
|
|
3946
|
+
Экспортируется только ради обратной совместимости.
|
|
3947
|
+
|
|
3630
3948
|
_Source: src/core/types/validation-schema.ts_
|
|
3631
3949
|
|
|
3632
3950
|
### AsyncValidatorFn
|
|
@@ -3736,13 +4054,16 @@ _Source: src/core/model/behaviors.ts_
|
|
|
3736
4054
|
|
|
3737
4055
|
**Kind:** `type`
|
|
3738
4056
|
|
|
3739
|
-
|
|
4057
|
+
Функция-предикат `(value) => boolean`.
|
|
3740
4058
|
|
|
3741
4059
|
**Signature:**
|
|
3742
4060
|
```typescript
|
|
3743
4061
|
export type ConditionFn<T> = (value: T) => boolean;
|
|
3744
4062
|
```
|
|
3745
4063
|
|
|
4064
|
+
**Deprecated:** Осиротевший остаток удалённого оператора `applyWhen` (Ф7). Рантаймом не
|
|
4065
|
+
потребляется; экспортируется только ради обратной совместимости.
|
|
4066
|
+
|
|
3746
4067
|
_Source: src/core/types/validation-schema.ts_
|
|
3747
4068
|
|
|
3748
4069
|
### ConfigWithSchema
|
|
@@ -3925,10 +4246,10 @@ export interface CreateFormFromModelArgs<T> {
|
|
|
3925
4246
|
/** Реактивная модель данных (источник истины значений). */
|
|
3926
4247
|
model: FormModel<T>;
|
|
3927
4248
|
/**
|
|
3928
|
-
* Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг
|
|
3929
|
-
* по идентичности сигнала (`node.value === model.$.path`). Опциональна.
|
|
4249
|
+
* Единая Schema (дерево узлов {@link FormSchemaNode}). createForm обходит её и привязывает конфиг
|
|
4250
|
+
* поля к ноде по идентичности сигнала (`node.value === model.$.path`). Опциональна.
|
|
3930
4251
|
*/
|
|
3931
|
-
schema?:
|
|
4252
|
+
schema?: FormSchemaNode;
|
|
3932
4253
|
/**
|
|
3933
4254
|
* Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
|
|
3934
4255
|
* заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
|
|
@@ -4048,6 +4369,15 @@ _Source: src/core/validation/validators/email.ts_
|
|
|
4048
4369
|
⚠️ Поле должно быть материализовано в форме (`createForm`) — иначе ноды в реестре нет (например,
|
|
4049
4370
|
элемент массива, который строится per-item).
|
|
4050
4371
|
|
|
4372
|
+
⚠️ Если то же поле одновременно является целью `compute`/`computeFrom`, обязательно ограничь
|
|
4373
|
+
compute тем же условием (`when`), что и это `enableWhen`. Механизмы не согласованы: `enableWhen`
|
|
4374
|
+
работает со статус-машиной ноды (enable/disable/reset), а `compute` пишет `target.value` напрямую
|
|
4375
|
+
в сигнал модели и проверяет только собственный `options.when` — состояние `disabled` он НЕ смотрит.
|
|
4376
|
+
Поэтому при выключенном поле любое изменение зависимости compute молча заново заполнит его (а с
|
|
4377
|
+
`resetOnDisable` — перезапишет только что сброшенное значение), и так как `getValue()`/`submit`
|
|
4378
|
+
включают disabled-поля, мусор утечёт в payload. Это аналог правила #13 гайда add-behavior,
|
|
4379
|
+
которое сегодня сформулировано только для `copyFrom`; для `compute` то же ограничение обязательно.
|
|
4380
|
+
|
|
4051
4381
|
**Signature:**
|
|
4052
4382
|
```typescript
|
|
4053
4383
|
export function enableWhen(
|
|
@@ -4061,6 +4391,12 @@ export function enableWhen(
|
|
|
4061
4391
|
|
|
4062
4392
|
```typescript
|
|
4063
4393
|
enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', { resetOnDisable: true });
|
|
4394
|
+
|
|
4395
|
+
// Поле-цель compute, которое ТАКЖЕ в resetOnDisable enableWhen: compute обязан нести тот же when,
|
|
4396
|
+
// иначе initialPayment заново заполнится в выключенном состоянии и утечёт в submit.
|
|
4397
|
+
compute(model.$.initialPayment, () => model.propertyValue * 0.2, {
|
|
4398
|
+
when: () => model.loanType === 'mortgage',
|
|
4399
|
+
});
|
|
4064
4400
|
```
|
|
4065
4401
|
|
|
4066
4402
|
_Source: src/core/model/behaviors.ts_
|
|
@@ -4081,7 +4417,7 @@ export interface ErrorFilterOptions {
|
|
|
4081
4417
|
message?: string;
|
|
4082
4418
|
|
|
4083
4419
|
/** Фильтр по параметрам ошибки */
|
|
4084
|
-
params?:
|
|
4420
|
+
params?: Record<string, FormValue>;
|
|
4085
4421
|
|
|
4086
4422
|
/** Кастомный предикат для фильтрации */
|
|
4087
4423
|
predicate?: (error: ValidationError) => boolean;
|
|
@@ -4334,8 +4670,22 @@ export interface FieldControlState<T> {
|
|
|
4334
4670
|
* );
|
|
4335
4671
|
* ```
|
|
4336
4672
|
*/
|
|
4337
|
-
|
|
4338
|
-
|
|
4673
|
+
componentProps: Record<string, unknown>;
|
|
4674
|
+
|
|
4675
|
+
/**
|
|
4676
|
+
* Флаг изменения поля.
|
|
4677
|
+
* `true` когда значение поля отличается от начального.
|
|
4678
|
+
*
|
|
4679
|
+
* Реактивный аналог `control.dirty` — паритет с {@link ArrayControlState.dirty}.
|
|
4680
|
+
*
|
|
4681
|
+
* @example
|
|
4682
|
+
* ```tsx
|
|
4683
|
+
* const { dirty } = useFormControl(field);
|
|
4684
|
+
*
|
|
4685
|
+
* return dirty ? <span className="badge">Изменено</span> : null;
|
|
4686
|
+
* ```
|
|
4687
|
+
*/
|
|
4688
|
+
dirty: boolean;
|
|
4339
4689
|
}
|
|
4340
4690
|
```
|
|
4341
4691
|
|
|
@@ -4599,22 +4949,6 @@ try {
|
|
|
4599
4949
|
|
|
4600
4950
|
_Source: src/core/utils/error-handler.ts_
|
|
4601
4951
|
|
|
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
4952
|
### FormModel
|
|
4619
4953
|
|
|
4620
4954
|
**Kind:** `type`
|
|
@@ -4679,7 +5013,27 @@ _Source: src/core/nodes/form-node.ts_
|
|
|
4679
5013
|
|
|
4680
5014
|
**Signature:**
|
|
4681
5015
|
```typescript
|
|
4682
|
-
export type FormProxy<T> = GroupNode<T> &
|
|
5016
|
+
export type FormProxy<T> = GroupNode<T> &
|
|
5017
|
+
// Исключаем имена, совпадающие с членами GroupNode: для них `form.<name>` возвращает член узла,
|
|
5018
|
+
// а не поле. Оставлять их в полевом пространстве — значит пересекать, напр., ReadonlySignal<FieldStatus>
|
|
5019
|
+
// с FieldNode<string> и молча компилировать невозможный тип. Escape-hatch — `form.$` ниже.
|
|
5020
|
+
Omit<FormControlsProxy<T>, keyof GroupNode<T>> & {
|
|
5021
|
+
/**
|
|
5022
|
+
* Escape-hatch пространство имён «controls»: типобезопасный доступ ко ВСЕМ полям формы
|
|
5023
|
+
* по имени, включая поля, чьи имена совпадают с членами {@link GroupNode}
|
|
5024
|
+
* (`value`/`status`/`id`/`errors`/…), недостижимые через `form.<name>`.
|
|
5025
|
+
*
|
|
5026
|
+
* @example
|
|
5027
|
+
* ```typescript
|
|
5028
|
+
* // модель: { status: string; email: string }
|
|
5029
|
+
* form.status; // ReadonlySignal<FieldStatus> — агрегат GroupNode (не поле!)
|
|
5030
|
+
* form.$.status; // FieldNode<string> — поле пользователя
|
|
5031
|
+
* form.$.status.setValue('active');
|
|
5032
|
+
* form.$.email; // FieldNode<string> — так же доступны и незатенённые поля
|
|
5033
|
+
* ```
|
|
5034
|
+
*/
|
|
5035
|
+
readonly $: FormControlsProxy<T>;
|
|
5036
|
+
};
|
|
4683
5037
|
```
|
|
4684
5038
|
|
|
4685
5039
|
**Examples:**
|
|
@@ -4711,26 +5065,33 @@ _Source: src/core/types/form-proxy.ts_
|
|
|
4711
5065
|
|
|
4712
5066
|
**Kind:** `type`
|
|
4713
5067
|
|
|
4714
|
-
|
|
5068
|
+
**Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
|
|
5069
|
+
(или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
|
|
4715
5070
|
- `T[] -> [FormSchema<T>]` (массив с одним элементом)
|
|
4716
5071
|
- `object -> FormSchema<T>` (группа)
|
|
4717
5072
|
- `primitive -> FieldConfig<T>` (поле)
|
|
4718
5073
|
|
|
4719
|
-
Использует NonNullable для корректной обработки опциональных
|
|
5074
|
+
Использует NonNullable для корректной обработки опциональных полей.
|
|
5075
|
+
|
|
5076
|
+
⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
|
|
5077
|
+
контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
|
|
5078
|
+
(ключи = поля данных), а не узел дерева.
|
|
4720
5079
|
|
|
4721
5080
|
**Signature:**
|
|
4722
5081
|
```typescript
|
|
4723
5082
|
export type FormSchema<T> = {
|
|
5083
|
+
// Листовые позиции используют NonEmptyFieldConfig: пустой `{}` (частая опечатка) отклоняется
|
|
5084
|
+
// на этапе компиляции, но каждое свойство FieldConfig по отдельности остаётся опциональным.
|
|
4724
5085
|
[K in keyof T]: NonNullable<T[K]> extends string | number | boolean
|
|
4725
|
-
?
|
|
5086
|
+
? NonEmptyFieldConfig<T[K]>
|
|
4726
5087
|
: NonNullable<T[K]> extends Array<infer U>
|
|
4727
5088
|
? U extends string | number | boolean
|
|
4728
|
-
?
|
|
5089
|
+
? NonEmptyFieldConfig<T[K]>
|
|
4729
5090
|
: U extends Date | File | Blob | AnyFunction
|
|
4730
|
-
?
|
|
5091
|
+
? NonEmptyFieldConfig<T[K]>
|
|
4731
5092
|
: [FormSchema<U>]
|
|
4732
5093
|
: NonNullable<T[K]> extends Date | File | Blob | AnyFunction
|
|
4733
|
-
?
|
|
5094
|
+
? NonEmptyFieldConfig<T[K]>
|
|
4734
5095
|
: FormSchema<NonNullable<T[K]>>;
|
|
4735
5096
|
};
|
|
4736
5097
|
```
|
|
@@ -4765,6 +5126,65 @@ const schema: FormSchema<Form> = {
|
|
|
4765
5126
|
|
|
4766
5127
|
_Source: src/core/types/deep-schema.ts_
|
|
4767
5128
|
|
|
5129
|
+
### FormSchemaNode
|
|
5130
|
+
|
|
5131
|
+
**Kind:** `interface`
|
|
5132
|
+
|
|
5133
|
+
Узел единой схемы M1 — дерево, обходимое `createForm({ model, schema })`,
|
|
5134
|
+
`validateModel`/`validateFormModel` и рендерерами.
|
|
5135
|
+
|
|
5136
|
+
Узел совмещает несколько ролей (различаются рантаймом по форме):
|
|
5137
|
+
- **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
|
|
5138
|
+
- **массив** — `{ array: model.<path>, item(itemModel) }`;
|
|
5139
|
+
- **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
|
|
5140
|
+
- **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).
|
|
5141
|
+
|
|
5142
|
+
Индексная сигнатура (`[key: string]: unknown`) отражает свободный рекурсивный обход: под-узлы
|
|
5143
|
+
допустимы под любым ключом. Известные поля типизированы (даёт автокомплит и проверку их типов).
|
|
5144
|
+
|
|
5145
|
+
**Signature:**
|
|
5146
|
+
```typescript
|
|
5147
|
+
export interface FormSchemaNode {
|
|
5148
|
+
/**
|
|
5149
|
+
* «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма
|
|
5150
|
+
* зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до
|
|
5151
|
+
* `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
|
|
5152
|
+
*/
|
|
5153
|
+
value?: unknown;
|
|
5154
|
+
/** UI-компонент. Опционален: core-часть работает без UI (значение/валидация). */
|
|
5155
|
+
component?: ComponentType<any>;
|
|
5156
|
+
/** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
|
|
5157
|
+
componentProps?: Record<string, unknown>;
|
|
5158
|
+
validators?: SchemaValidator[];
|
|
5159
|
+
asyncValidators?: SchemaValidator[];
|
|
5160
|
+
updateOn?: 'change' | 'blur' | 'submit';
|
|
5161
|
+
disabled?: boolean;
|
|
5162
|
+
/** Задержка (мс) перед запуском асинхронной валидации. */
|
|
5163
|
+
debounce?: number;
|
|
5164
|
+
/** Идентификатор узла (для wizard/tabs/renderBehavior). */
|
|
5165
|
+
selector?: string;
|
|
5166
|
+
testId?: string;
|
|
5167
|
+
/** Дочерние узлы (даёт контекстную типизацию вложенным литералам — value/validators/when). */
|
|
5168
|
+
children?: readonly FormSchemaNode[];
|
|
5169
|
+
/** Условие включения поддерева (branch-узел `{ when, children }`). */
|
|
5170
|
+
when?: (scope: any, root: any) => boolean;
|
|
5171
|
+
/** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */
|
|
5172
|
+
array?: SchemaArrayControl;
|
|
5173
|
+
/** Схема элемента массива: под-модель элемента → узел поддерева. */
|
|
5174
|
+
item?: (itemModel: any) => FormSchemaNode;
|
|
5175
|
+
/**
|
|
5176
|
+
* Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,
|
|
5177
|
+
* либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`
|
|
5178
|
+
* схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.
|
|
5179
|
+
*/
|
|
5180
|
+
initialValue?: unknown;
|
|
5181
|
+
/** Свободная вложенность: record-of-fields и произвольные под-узлы. */
|
|
5182
|
+
[key: string]: unknown;
|
|
5183
|
+
}
|
|
5184
|
+
```
|
|
5185
|
+
|
|
5186
|
+
_Source: src/core/types/schema-node.ts_
|
|
5187
|
+
|
|
4768
5188
|
### FormStatusMachine
|
|
4769
5189
|
|
|
4770
5190
|
**Kind:** `class`
|
|
@@ -4810,7 +5230,7 @@ FormSubmitter - управляет процессом отправки форм
|
|
|
4810
5230
|
|
|
4811
5231
|
**Signature:**
|
|
4812
5232
|
```typescript
|
|
4813
|
-
export class FormSubmitter<T extends
|
|
5233
|
+
export class FormSubmitter<T extends object> {
|
|
4814
5234
|
/** Внутренний сигнал состояния отправки */ /* … */ }
|
|
4815
5235
|
```
|
|
4816
5236
|
|
|
@@ -5018,7 +5438,7 @@ _Source: src/core/types/index.ts_
|
|
|
5018
5438
|
|
|
5019
5439
|
**Signature:**
|
|
5020
5440
|
```typescript
|
|
5021
|
-
export function integer<TForm = unknown, TField extends number | undefined = number>(
|
|
5441
|
+
export function integer<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5022
5442
|
options?: ValidateOptions
|
|
5023
5443
|
): Validator<TForm, TField>
|
|
5024
5444
|
```
|
|
@@ -5055,7 +5475,7 @@ ArrayNode представляет массив вложенных форм (о
|
|
|
5055
5475
|
|
|
5056
5476
|
**Signature:**
|
|
5057
5477
|
```typescript
|
|
5058
|
-
export function isArrayNode(value: unknown): value is ArrayNode<
|
|
5478
|
+
export function isArrayNode(value: unknown): value is ArrayNode<object>
|
|
5059
5479
|
```
|
|
5060
5480
|
|
|
5061
5481
|
**Parameters:**
|
|
@@ -5217,7 +5637,7 @@ GroupNode представляет объект с вложенными поля
|
|
|
5217
5637
|
|
|
5218
5638
|
**Signature:**
|
|
5219
5639
|
```typescript
|
|
5220
|
-
export function isGroupNode(value: unknown): value is GroupNode<
|
|
5640
|
+
export function isGroupNode(value: unknown): value is GroupNode<object>
|
|
5221
5641
|
```
|
|
5222
5642
|
|
|
5223
5643
|
**Parameters:**
|
|
@@ -5247,7 +5667,7 @@ _Source: src/core/utils/type-guards.ts_
|
|
|
5247
5667
|
|
|
5248
5668
|
**Signature:**
|
|
5249
5669
|
```typescript
|
|
5250
|
-
export function isNumber<TForm = unknown, TField extends number | undefined = number>(
|
|
5670
|
+
export function isNumber<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5251
5671
|
options?: ValidateOptions
|
|
5252
5672
|
): Validator<TForm, TField>
|
|
5253
5673
|
```
|
|
@@ -5283,6 +5703,9 @@ _Source: src/core/validation/validators/is-number.ts_
|
|
|
5283
5703
|
bulk-сеттеры (`model.set`/`model.patch`, `patchValue`/`setValue`) пропускают это поле, чтобы
|
|
5284
5704
|
значение из payload не затирало вычисляемое. Прикладной код напрямую обычно не вызывает.
|
|
5285
5705
|
|
|
5706
|
+
Идемпотентна на уровне флага: повторные вызовы наращивают счётчик ссылок, чтобы каждый оператор,
|
|
5707
|
+
пишущий в этот сигнал, был сбалансирован своим {@link unmarkDerived} при dispose.
|
|
5708
|
+
|
|
5286
5709
|
**Signature:**
|
|
5287
5710
|
```typescript
|
|
5288
5711
|
export function markDerived(signal: Signal<any>): void
|
|
@@ -5302,6 +5725,7 @@ markDerived(model.$.total);
|
|
|
5302
5725
|
|
|
5303
5726
|
**See also:**
|
|
5304
5727
|
- {@link isDerived} - проверка пометки
|
|
5728
|
+
- {@link unmarkDerived} - снять пометку при dispose оператора
|
|
5305
5729
|
|
|
5306
5730
|
_Source: src/core/utils/derived-registry.ts_
|
|
5307
5731
|
|
|
@@ -5315,7 +5739,7 @@ _Source: src/core/utils/derived-registry.ts_
|
|
|
5315
5739
|
|
|
5316
5740
|
**Signature:**
|
|
5317
5741
|
```typescript
|
|
5318
|
-
export function max<TForm = unknown, TField extends number | undefined = number>(
|
|
5742
|
+
export function max<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5319
5743
|
maxValue: number,
|
|
5320
5744
|
options?: ValidateOptions
|
|
5321
5745
|
): Validator<TForm, TField>
|
|
@@ -5477,7 +5901,7 @@ _Source: src/core/validation/validators/max-length.ts_
|
|
|
5477
5901
|
|
|
5478
5902
|
**Signature:**
|
|
5479
5903
|
```typescript
|
|
5480
|
-
export function min<TForm = unknown, TField extends number | undefined = number>(
|
|
5904
|
+
export function min<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5481
5905
|
minValue: number,
|
|
5482
5906
|
options?: ValidateOptions
|
|
5483
5907
|
): Validator<TForm, TField>
|
|
@@ -5645,9 +6069,17 @@ export interface ModelApi<T> {
|
|
|
5645
6069
|
readonly $: ModelSignals<T>;
|
|
5646
6070
|
/** Снимок значений (без подписки) — для submit. */
|
|
5647
6071
|
get(): T;
|
|
5648
|
-
/**
|
|
6072
|
+
/**
|
|
6073
|
+
* @deprecated Алиас {@link ModelApi.patch}: `set` выполняет то же частичное **слияние** —
|
|
6074
|
+
* обновляет только переданные ключи, отсутствующие НЕ трогает (это НЕ полная замена, вопреки
|
|
6075
|
+
* старому названию). Используйте `patch`; для замены с обнулением лишних полей делайте
|
|
6076
|
+
* `patch({ ...defaults, ...dto })` явно. Не меняет initial-снимок.
|
|
6077
|
+
*/
|
|
5649
6078
|
set(value: Partial<T>): void;
|
|
5650
|
-
/**
|
|
6079
|
+
/**
|
|
6080
|
+
* Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
|
|
6081
|
+
* отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
|
|
6082
|
+
*/
|
|
5651
6083
|
patch(value: Partial<T>): void;
|
|
5652
6084
|
/** Отличаются ли текущие значения от initial-снимка (value-diff). */
|
|
5653
6085
|
isDirty(): boolean;
|
|
@@ -5672,6 +6104,11 @@ _Source: src/core/model/types.ts_
|
|
|
5672
6104
|
**Signature:**
|
|
5673
6105
|
```typescript
|
|
5674
6106
|
export interface ModelArray<U> {
|
|
6107
|
+
/**
|
|
6108
|
+
* Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
|
|
6109
|
+
* рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
|
|
6110
|
+
*/
|
|
6111
|
+
readonly __path: string;
|
|
5675
6112
|
/** Реактивная длина. */
|
|
5676
6113
|
readonly length: number;
|
|
5677
6114
|
/** Добавить элемент в конец (значение элемента целиком). */
|
|
@@ -5686,12 +6123,14 @@ export interface ModelArray<U> {
|
|
|
5686
6123
|
swap(a: number, b: number): void;
|
|
5687
6124
|
/** Очистить массив. */
|
|
5688
6125
|
clear(): void;
|
|
5689
|
-
/**
|
|
5690
|
-
at(
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
/**
|
|
5694
|
-
|
|
6126
|
+
/** Элемент по индексу: объект → под-модель, массив → {@link ModelArray}, лист → значение. */
|
|
6127
|
+
at(
|
|
6128
|
+
index: number
|
|
6129
|
+
): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
|
|
6130
|
+
/** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
|
|
6131
|
+
map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
|
|
6132
|
+
/** Итерация по элементам (см. {@link ModelArrayItem}). */
|
|
6133
|
+
forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
|
|
5695
6134
|
/** Снимок массива значений (без подписки). */
|
|
5696
6135
|
toArray(): U[];
|
|
5697
6136
|
/** Индексный value-доступ. */
|
|
@@ -5798,13 +6237,7 @@ _Source: src/core/model/types.ts_
|
|
|
5798
6237
|
**Signature:**
|
|
5799
6238
|
```typescript
|
|
5800
6239
|
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]>;
|
|
6240
|
+
[K in keyof T]: ModelSignalNode<T[K]>;
|
|
5808
6241
|
};
|
|
5809
6242
|
```
|
|
5810
6243
|
|
|
@@ -5831,9 +6264,12 @@ _Source: src/core/model/validate-model.ts_
|
|
|
5831
6264
|
|
|
5832
6265
|
**Kind:** `type`
|
|
5833
6266
|
|
|
5834
|
-
Валидатор слоя
|
|
6267
|
+
Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
|
|
5835
6268
|
массива или корень); `root` — корневая модель.
|
|
5836
6269
|
|
|
6270
|
+
Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
|
|
6271
|
+
`FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
|
|
6272
|
+
|
|
5837
6273
|
**Signature:**
|
|
5838
6274
|
```typescript
|
|
5839
6275
|
export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
|
|
@@ -5879,7 +6315,7 @@ _Source: src/core/model/types.ts_
|
|
|
5879
6315
|
|
|
5880
6316
|
**Signature:**
|
|
5881
6317
|
```typescript
|
|
5882
|
-
export function multipleOf<TForm = unknown, TField extends number | undefined = number>(
|
|
6318
|
+
export function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5883
6319
|
divisor: number,
|
|
5884
6320
|
options?: ValidateOptions
|
|
5885
6321
|
): Validator<TForm, TField>
|
|
@@ -5988,7 +6424,7 @@ _Source: src/core/factories/node-factory.ts_
|
|
|
5988
6424
|
|
|
5989
6425
|
**Signature:**
|
|
5990
6426
|
```typescript
|
|
5991
|
-
export function nonNegative<TForm = unknown, TField extends number | undefined = number>(
|
|
6427
|
+
export function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(
|
|
5992
6428
|
options?: ValidateOptions
|
|
5993
6429
|
): Validator<TForm, TField>
|
|
5994
6430
|
```
|
|
@@ -6024,7 +6460,7 @@ _Source: src/core/validation/validators/non-negative.ts_
|
|
|
6024
6460
|
|
|
6025
6461
|
**Signature:**
|
|
6026
6462
|
```typescript
|
|
6027
|
-
export function nonZero<TForm = unknown, TField extends number | undefined = number>(
|
|
6463
|
+
export function nonZero<TForm = unknown, TField extends number | null | undefined = number>(
|
|
6028
6464
|
options?: ValidateOptions
|
|
6029
6465
|
): Validator<TForm, TField>
|
|
6030
6466
|
```
|
|
@@ -6250,7 +6686,8 @@ _Source: src/core/utils/signal-node-registry.ts_
|
|
|
6250
6686
|
|
|
6251
6687
|
Возвращает чистую функцию-валидатор `(value, control, root)`. Передаётся в `validate()`.
|
|
6252
6688
|
|
|
6253
|
-
Пустыми считаются: `null`, `undefined`, `''` (пустая строка)
|
|
6689
|
+
Пустыми считаются: `null`, `undefined`, `''` (пустая строка), `[]` (пустой массив —
|
|
6690
|
+
обязательный multi-select / FormArray без выбранных элементов).
|
|
6254
6691
|
Для boolean полей требуется значение `true`.
|
|
6255
6692
|
|
|
6256
6693
|
**Signature:**
|
|
@@ -6350,26 +6787,31 @@ _Source: src/core/model/behaviors.ts_
|
|
|
6350
6787
|
|
|
6351
6788
|
**Kind:** `function`
|
|
6352
6789
|
|
|
6353
|
-
Выполняет функцию вне контекста effect
|
|
6790
|
+
Выполняет функцию вне контекста effect (отложенная запись на микротаск).
|
|
6354
6791
|
|
|
6355
|
-
|
|
6792
|
+
Сбои fn (синхронный throw и async-rejection) маршрутизируются через {@link FormErrorHandler}, а не
|
|
6793
|
+
всплывают неперехваченными. Возвращает отменитель: вызов до срабатывания микротаска отменяет запись
|
|
6794
|
+
(liveness-охрана — не писать против снесённого узла при dispose в том же тике).
|
|
6356
6795
|
|
|
6357
6796
|
**Signature:**
|
|
6358
6797
|
```typescript
|
|
6359
|
-
export function runOutsideEffect(fn: () => void | Promise<void>): void
|
|
6798
|
+
export function runOutsideEffect(fn: () => void | Promise<void>): () => void
|
|
6360
6799
|
```
|
|
6361
6800
|
|
|
6362
6801
|
**Parameters:**
|
|
6363
6802
|
- `fn` — - Функция для выполнения
|
|
6364
6803
|
|
|
6804
|
+
**Returns:** Отменитель ещё не выполненной записи (no-op, если запись уже выполнена)
|
|
6805
|
+
|
|
6365
6806
|
**Examples:**
|
|
6366
6807
|
|
|
6367
6808
|
```typescript
|
|
6368
6809
|
effect(() => {
|
|
6369
6810
|
const value = signal.value;
|
|
6370
|
-
runOutsideEffect(() => {
|
|
6811
|
+
const cancel = runOutsideEffect(() => {
|
|
6371
6812
|
otherSignal.value = transform(value);
|
|
6372
6813
|
});
|
|
6814
|
+
onDispose(cancel); // отменить ожидающую запись, если узел снесут в этом же тике
|
|
6373
6815
|
});
|
|
6374
6816
|
```
|
|
6375
6817
|
|
|
@@ -6381,8 +6823,8 @@ _Source: src/core/utils/safe-effect.ts_
|
|
|
6381
6823
|
|
|
6382
6824
|
Создает callback, который выполняется вне контекста effect
|
|
6383
6825
|
|
|
6384
|
-
|
|
6385
|
-
|
|
6826
|
+
Откладывает вызов через {@link runOutsideEffect} — то есть получает маршрутизацию ошибок и
|
|
6827
|
+
обработку async-rejection «бесплатно».
|
|
6386
6828
|
|
|
6387
6829
|
**Signature:**
|
|
6388
6830
|
```typescript
|
|
@@ -6420,7 +6862,9 @@ _Source: src/core/utils/safe-effect.ts_
|
|
|
6420
6862
|
|
|
6421
6863
|
Создает версию callback с поддержкой debounce, безопасную для effect
|
|
6422
6864
|
|
|
6423
|
-
|
|
6865
|
+
`withDebounce` уже откладывает вызов (таймер) — этого достаточно для выхода из effect-контекста,
|
|
6866
|
+
поэтому дополнительного `queueMicrotask` нет (устранён двойной defer). Сбои callback (в т.ч.
|
|
6867
|
+
async-rejection) маршрутизируются через {@link FormErrorHandler}.
|
|
6424
6868
|
|
|
6425
6869
|
**Signature:**
|
|
6426
6870
|
```typescript
|
|
@@ -6450,6 +6894,48 @@ return effect(() => {
|
|
|
6450
6894
|
|
|
6451
6895
|
_Source: src/core/utils/safe-effect.ts_
|
|
6452
6896
|
|
|
6897
|
+
### SchemaArrayControl
|
|
6898
|
+
|
|
6899
|
+
**Kind:** `interface`
|
|
6900
|
+
|
|
6901
|
+
Минимальный контракт реактивного массива модели ({@link FormSchemaNode.array}).
|
|
6902
|
+
Совпадает по форме с рантайм-фасадом `model.<array>` (см. `ModelArray`); рендерерский
|
|
6903
|
+
`RenderModelArrayControl` — его расширение (добавляет `move`).
|
|
6904
|
+
|
|
6905
|
+
**Signature:**
|
|
6906
|
+
```typescript
|
|
6907
|
+
export interface SchemaArrayControl {
|
|
6908
|
+
/** Путь массива в модели (dot-нотация) — нужен для резолва узла массива. */
|
|
6909
|
+
readonly __path: string;
|
|
6910
|
+
/** Реактивная длина. */
|
|
6911
|
+
readonly length: number;
|
|
6912
|
+
at(index: number): unknown;
|
|
6913
|
+
push(item: unknown): void;
|
|
6914
|
+
removeAt(index: number): void;
|
|
6915
|
+
}
|
|
6916
|
+
```
|
|
6917
|
+
|
|
6918
|
+
_Source: src/core/types/schema-node.ts_
|
|
6919
|
+
|
|
6920
|
+
### SchemaValidator
|
|
6921
|
+
|
|
6922
|
+
**Kind:** `type`
|
|
6923
|
+
|
|
6924
|
+
Валидатор поля схемы. Узел хранит validators гетерогенно: движок `validate*` вызывает их как
|
|
6925
|
+
`(value, scope, root)`, но сюда кладут любой контракт — `ValidatorFn` (value),
|
|
6926
|
+
`Validator` (value, control, root) или `ModelValidator` (value, model, root). Поэтому тип
|
|
6927
|
+
намеренно широкий по параметрам.
|
|
6928
|
+
|
|
6929
|
+
**Signature:**
|
|
6930
|
+
```typescript
|
|
6931
|
+
export type SchemaValidator = (
|
|
6932
|
+
value: any,
|
|
6933
|
+
...rest: any[]
|
|
6934
|
+
) => ValidationError | null | Promise<ValidationError | null>;
|
|
6935
|
+
```
|
|
6936
|
+
|
|
6937
|
+
_Source: src/core/types/schema-node.ts_
|
|
6938
|
+
|
|
6453
6939
|
### SetValueOptions
|
|
6454
6940
|
|
|
6455
6941
|
**Kind:** `interface`
|
|
@@ -6534,7 +7020,7 @@ _Source: src/core/utils/form-submitter.ts_
|
|
|
6534
7020
|
|
|
6535
7021
|
**Signature:**
|
|
6536
7022
|
```typescript
|
|
6537
|
-
export interface SubmittableForm<T extends
|
|
7023
|
+
export interface SubmittableForm<T extends object> {
|
|
6538
7024
|
/** Пометить все поля как touched */
|
|
6539
7025
|
markAsTouched(): void;
|
|
6540
7026
|
/** Валидировать форму */
|
|
@@ -6700,11 +7186,47 @@ _Source: src/core/types/index.ts_
|
|
|
6700
7186
|
|
|
6701
7187
|
**Signature:**
|
|
6702
7188
|
```typescript
|
|
6703
|
-
export type UnknownRecord = Record<string,
|
|
7189
|
+
export type UnknownRecord = Record<string, unknown>;
|
|
6704
7190
|
```
|
|
6705
7191
|
|
|
6706
7192
|
_Source: src/core/types/index.ts_
|
|
6707
7193
|
|
|
7194
|
+
### unmarkDerived
|
|
7195
|
+
|
|
7196
|
+
**Kind:** `function`
|
|
7197
|
+
|
|
7198
|
+
Снять пометку производного сигнала (обратная операция к {@link markDerived}).
|
|
7199
|
+
|
|
7200
|
+
Вызывается из очистки (`onDispose`) оператора `compute`/`computeFrom`, когда поведение снимается,
|
|
7201
|
+
но модель/сигнал продолжают жить (динамическая перекоммутация: удалить под-схему → сохранить модель
|
|
7202
|
+
→ bulk-set). Без этого сигнал остаётся помеченным навсегда, и bulk-сеттеры навсегда пропускают
|
|
7203
|
+
поле, которое больше ничем не вычисляется.
|
|
7204
|
+
|
|
7205
|
+
Учитывает счётчик ссылок: пометка снимается только когда снят последний владелец. Вызов на
|
|
7206
|
+
непомеченном сигнале — безопасный no-op.
|
|
7207
|
+
|
|
7208
|
+
**Signature:**
|
|
7209
|
+
```typescript
|
|
7210
|
+
export function unmarkDerived(signal: Signal<any>): void
|
|
7211
|
+
```
|
|
7212
|
+
|
|
7213
|
+
**Parameters:**
|
|
7214
|
+
- `signal` — - Сигнал значения из {@link FormModel}, ранее помеченный {@link markDerived}
|
|
7215
|
+
|
|
7216
|
+
**Examples:**
|
|
7217
|
+
|
|
7218
|
+
```typescript
|
|
7219
|
+
import { markDerived, unmarkDerived } from '@reformer/core';
|
|
7220
|
+
|
|
7221
|
+
markDerived(model.$.total);
|
|
7222
|
+
onDispose(() => unmarkDerived(model.$.total)); // при снятии compute снова разрешаем bulk-set
|
|
7223
|
+
```
|
|
7224
|
+
|
|
7225
|
+
**See also:**
|
|
7226
|
+
- {@link markDerived} - пометить сигнал производным
|
|
7227
|
+
|
|
7228
|
+
_Source: src/core/utils/derived-registry.ts_
|
|
7229
|
+
|
|
6708
7230
|
### url
|
|
6709
7231
|
|
|
6710
7232
|
**Kind:** `function`
|
|
@@ -6761,7 +7283,7 @@ React-хук для подписки только на длину массива
|
|
|
6761
7283
|
|
|
6762
7284
|
**Signature:**
|
|
6763
7285
|
```typescript
|
|
6764
|
-
export function useArrayLength<T extends
|
|
7286
|
+
export function useArrayLength<T extends object>(control: ArrayNode<T>): number
|
|
6765
7287
|
```
|
|
6766
7288
|
|
|
6767
7289
|
**Parameters:**
|
|
@@ -6815,8 +7337,8 @@ React-хук для подписки на состояние формы (FieldNo
|
|
|
6815
7337
|
**Signature:**
|
|
6816
7338
|
```typescript
|
|
6817
7339
|
export function useFormControl(
|
|
6818
|
-
control: FieldNode<FormValue> | ArrayNode<
|
|
6819
|
-
): FieldControlState<FormValue> | ArrayControlState<
|
|
7340
|
+
control: FieldNode<FormValue> | ArrayNode<object> | undefined
|
|
7341
|
+
): FieldControlState<FormValue> | ArrayControlState<object>
|
|
6820
7342
|
```
|
|
6821
7343
|
|
|
6822
7344
|
**Parameters:**
|
|
@@ -7206,16 +7728,18 @@ _Source: src/hooks/useFormControlValue.ts_
|
|
|
7206
7728
|
|
|
7207
7729
|
**Kind:** `interface`
|
|
7208
7730
|
|
|
7209
|
-
Опции для функции validateAsync.
|
|
7210
|
-
|
|
7211
7731
|
**Signature:**
|
|
7212
7732
|
```typescript
|
|
7213
7733
|
export interface ValidateAsyncOptions extends ValidateOptions {
|
|
7214
|
-
/** Задержка перед выполнением валидации (в мс) */
|
|
7734
|
+
/** Задержка перед выполнением валидации (в мс). */
|
|
7215
7735
|
debounce?: number;
|
|
7216
7736
|
}
|
|
7217
7737
|
```
|
|
7218
7738
|
|
|
7739
|
+
**Deprecated:** Осиротевший остаток удалённого оператора `validateAsync` (Ф7): опция `debounce`
|
|
7740
|
+
подключалась тем оператором, которого больше нет. Рантаймом не потребляется; экспортируется
|
|
7741
|
+
только ради обратной совместимости.
|
|
7742
|
+
|
|
7219
7743
|
_Source: src/core/types/validation-schema.ts_
|
|
7220
7744
|
|
|
7221
7745
|
### validateFormModel
|
|
@@ -7230,7 +7754,7 @@ In-form валидация: прогоняет {@link validateModel} и роут
|
|
|
7230
7754
|
```typescript
|
|
7231
7755
|
export async function validateFormModel<T>(
|
|
7232
7756
|
model: FormModel<T>,
|
|
7233
|
-
schema:
|
|
7757
|
+
schema: FormSchemaNode
|
|
7234
7758
|
): Promise<ModelValidationResult>
|
|
7235
7759
|
```
|
|
7236
7760
|
|
|
@@ -7262,7 +7786,7 @@ Headless-валидация данных (sync + async). Работает без
|
|
|
7262
7786
|
```typescript
|
|
7263
7787
|
export async function validateModel<T>(
|
|
7264
7788
|
model: FormModel<T>,
|
|
7265
|
-
schema:
|
|
7789
|
+
schema: FormSchemaNode
|
|
7266
7790
|
): Promise<ModelValidationResult>
|
|
7267
7791
|
```
|
|
7268
7792
|
|
|
@@ -7283,7 +7807,10 @@ _Source: src/core/model/validate-model.ts_
|
|
|
7283
7807
|
|
|
7284
7808
|
**Signature:**
|
|
7285
7809
|
```typescript
|
|
7286
|
-
export function validateModelSync<T>(
|
|
7810
|
+
export function validateModelSync<T>(
|
|
7811
|
+
model: FormModel<T>,
|
|
7812
|
+
schema: FormSchemaNode
|
|
7813
|
+
): ModelValidationResult
|
|
7287
7814
|
```
|
|
7288
7815
|
|
|
7289
7816
|
**Parameters:**
|
|
@@ -7307,15 +7834,17 @@ _Source: src/core/model/validate-model.ts_
|
|
|
7307
7834
|
|
|
7308
7835
|
**Kind:** `interface`
|
|
7309
7836
|
|
|
7310
|
-
Опции
|
|
7837
|
+
Опции валидатора-фабрики (`required()`/`pattern()`/…). Передаются вторым (или последним)
|
|
7838
|
+
аргументом в фабрику и попадают в возвращаемую {@link ValidationError}.
|
|
7311
7839
|
|
|
7312
7840
|
**Signature:**
|
|
7313
7841
|
```typescript
|
|
7314
7842
|
export interface ValidateOptions {
|
|
7315
|
-
/**
|
|
7843
|
+
/** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
|
|
7844
|
+
* резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */
|
|
7316
7845
|
message?: string;
|
|
7317
|
-
/** Параметры ошибки */
|
|
7318
|
-
params?:
|
|
7846
|
+
/** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
|
|
7847
|
+
params?: Record<string, FormValue>;
|
|
7319
7848
|
}
|
|
7320
7849
|
```
|
|
7321
7850
|
|
|
@@ -7332,7 +7861,7 @@ _Source: src/core/types/validation-schema.ts_
|
|
|
7332
7861
|
export interface ValidationError {
|
|
7333
7862
|
code: string;
|
|
7334
7863
|
message: string;
|
|
7335
|
-
params?:
|
|
7864
|
+
params?: Record<string, FormValue>;
|
|
7336
7865
|
/** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
|
|
7337
7866
|
severity?: 'error' | 'warning';
|
|
7338
7867
|
}
|
|
@@ -7346,15 +7875,25 @@ _Source: src/core/types/index.ts_
|
|
|
7346
7875
|
|
|
7347
7876
|
Чистый синхронный валидатор поля.
|
|
7348
7877
|
|
|
7349
|
-
|
|
7350
|
-
|
|
7878
|
+
Сигнатура зеркалит то, что реально вызывает движок M1
|
|
7879
|
+
(`validateModel`/`validateFormModel`/`validateModelSync`): `validator(value, scope, root)`.
|
|
7880
|
+
- `value` — значение поля (`TField`);
|
|
7881
|
+
- `scope` — ближайшая scope-**модель** (под-модель элемента массива или корень). Из `TForm`/`TField`
|
|
7882
|
+
её тип не выводится, поэтому `unknown` — потребитель сужает сам (для типизированного scope
|
|
7883
|
+
используйте {@link ModelValidator}`<TField, TScope, TForm>`);
|
|
7884
|
+
- `root` — корневая модель формы `FormModel<TForm>` (реактивный value-proxy: `root.field` читает
|
|
7885
|
+
значение). Возвращает `ValidationError` либо `null`. Не знает про реестр валидации.
|
|
7886
|
+
|
|
7887
|
+
Совместим с полем `validators` узла схемы (см. `SchemaValidator`) — там же лежат `ModelValidator`
|
|
7888
|
+
и `ValidatorFn`. Встроенные фабрики (`required()`/`email()`/…) возвращают `(value) => …` и
|
|
7889
|
+
дополнительные аргументы игнорируют.
|
|
7351
7890
|
|
|
7352
7891
|
**Signature:**
|
|
7353
7892
|
```typescript
|
|
7354
7893
|
export type Validator<TForm, TField> = (
|
|
7355
7894
|
value: TField,
|
|
7356
|
-
|
|
7357
|
-
root:
|
|
7895
|
+
scope: unknown,
|
|
7896
|
+
root: FormModel<TForm>
|
|
7358
7897
|
) => ValidationError | null;
|
|
7359
7898
|
```
|
|
7360
7899
|
|
|
@@ -7362,8 +7901,9 @@ export type Validator<TForm, TField> = (
|
|
|
7362
7901
|
|
|
7363
7902
|
Кастомный валидатор в массиве `validators` поля схемы
|
|
7364
7903
|
```typescript
|
|
7365
|
-
const isAdult: Validator<MyForm, number> = (value,
|
|
7904
|
+
const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
|
|
7366
7905
|
if (value < 18) return { code: 'tooYoung', message: '18+' };
|
|
7906
|
+
// root — FormModel<MyForm>: root.someOtherField читается как значение
|
|
7367
7907
|
return null;
|
|
7368
7908
|
};
|
|
7369
7909
|
|