@reformer/core 10.1.0 → 11.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/{behaviors-node-DUrqAqp1.js → behaviors-node-DGEFMTQu.js} +1 -1
- package/dist/{behaviors-value-lCpqicfa.js → behaviors-value-C7dSEhCc.js} +43 -57
- package/dist/behaviors.js +108 -107
- package/dist/derived-registry-CluZCQ7U.js +16 -0
- package/dist/form/form-submitter.d.ts +0 -4
- package/dist/form/hooks/types.d.ts +3 -3
- package/dist/form/hooks/use-form-validation.d.ts +48 -0
- package/dist/form/index.d.ts +2 -3
- package/dist/form/nodes/array-node.d.ts +0 -84
- package/dist/form/nodes/form-node.d.ts +0 -14
- package/dist/form/nodes/group-node.d.ts +0 -4
- package/dist/form/status-machine.d.ts +0 -38
- package/dist/form/types/contracts.d.ts +0 -6
- package/dist/form/types/deep-schema.d.ts +0 -9
- package/dist/form/types/index.d.ts +4 -38
- package/dist/form/types/schema-node.d.ts +13 -16
- package/dist/form/types/validation-schema.d.ts +0 -25
- package/dist/form/unique-id.d.ts +3 -11
- package/dist/form/validation-schema.d.ts +16 -3
- package/dist/form/validation-strategy.d.ts +55 -0
- package/dist/form/validators.d.ts +8 -3
- package/dist/form-model-DYCzGGKF.js +286 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +477 -648
- package/dist/runtime-token.d.ts +23 -0
- package/dist/state/form-model.d.ts +12 -1
- package/dist/state.js +17 -15
- package/dist/subscription-manager-705bls97.js +171 -0
- package/dist/validation.js +133 -81
- package/dist/validators.d.ts +0 -4
- package/dist/validators.js +88 -122
- package/docs/llms/01-api-reference.md +63 -0
- package/docs/llms/02-quick-start.md +143 -0
- package/docs/llms/03-api-signatures.md +273 -0
- package/docs/llms/04-common-patterns.md +202 -0
- package/docs/llms/05-common-mistakes.md +167 -0
- package/docs/llms/06-troubleshooting.md +11 -0
- package/docs/llms/07-complete-import.md +99 -0
- package/docs/llms/08-form-types.md +47 -0
- package/docs/llms/09-formschema.md +143 -0
- package/docs/llms/10-arrays.md +161 -0
- package/docs/llms/11-async-watchfield.md +53 -0
- package/docs/llms/12-array-cleanup.md +31 -0
- package/docs/llms/13-multi-step.md +91 -0
- package/docs/llms/14-extended-mistakes.md +98 -0
- package/docs/llms/15-project-structure.md +125 -0
- package/docs/llms/16-ui-components.md +189 -0
- package/docs/llms/17-nonexistent-api.md +93 -0
- package/docs/llms/18-conditional-fields.md +183 -0
- package/docs/llms/19-reading-values.md +59 -0
- package/docs/llms/20-compute-vs-watch.md +138 -0
- package/docs/llms/21-array-operations.md +113 -0
- package/docs/llms/22-cycle-detection.md +86 -0
- package/docs/llms/23-copy-from.md +141 -0
- package/docs/llms/24-sync-fields.md +114 -0
- package/docs/llms/25-reset-when.md +115 -0
- package/docs/llms/26-transform-value.md +136 -0
- package/docs/llms/27-revalidate-when.md +132 -0
- package/docs/llms/28-submit-and-reset.md +259 -0
- package/docs/llms/29-async-preload.md +193 -0
- package/docs/llms/30-type-safety-recipes.md +159 -0
- package/docs/llms/31-async-validator-debounce.md +135 -0
- package/docs/llms/32-async-options-loading.md +108 -0
- package/docs/llms/33-validation-strategy.md +193 -0
- package/llms.txt +435 -1580
- package/package.json +33 -10
- package/dist/form-model-C8Fi83k4.js +0 -439
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
## 13. EXTENDED COMMON MISTAKES
|
|
2
|
+
|
|
3
|
+
### Reuse via apply / applyEach — не дублируй код
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { defineFormBehavior, apply, applyEach, compute } from '@reformer/core/behaviors';
|
|
7
|
+
|
|
8
|
+
// ✅ apply — одна под-схема на несколько групп
|
|
9
|
+
const addressBehavior = defineFormBehavior<Address>(({ model }) => {
|
|
10
|
+
compute(model.$.full, () => `${model.city}, ${model.street}`);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const behavior = defineFormBehavior<Form>(({ model }) => {
|
|
14
|
+
apply([model.$.homeAddress, model.$.workAddress], addressBehavior);
|
|
15
|
+
|
|
16
|
+
// ✅ applyEach — под-схема на КАЖДЫЙ элемент массива (реагирует на add/remove)
|
|
17
|
+
applyEach(
|
|
18
|
+
model.$.items,
|
|
19
|
+
defineFormBehavior<Item>(({ model: row }) => {
|
|
20
|
+
compute(row.$.lineTotal, () => row.qty * row.price);
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Идемпотентность transformValue
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// ❌ неидемпотентный transformer — бесконечный цикл setValue → callback → setValue
|
|
30
|
+
transformValue(model.$.field, (v) => `prefix-${v}`); // f(f(x)) ≠ f(x)
|
|
31
|
+
|
|
32
|
+
// ✅ guard «уже преобразовано»
|
|
33
|
+
transformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### compute для производных полей вместо ручной синхронизации
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// ❌ ручной onChange + запись — легко зациклить/забыть кейс
|
|
40
|
+
onChange(model.$.firstName, () => { model.fullName = `${model.firstName} ${model.lastName}`; });
|
|
41
|
+
|
|
42
|
+
// ✅ compute: цель не входит в источники → цикла нет, запись идемпотентна (peek-guard)
|
|
43
|
+
compute(model.$.fullName, () => [model.firstName, model.lastName].filter(Boolean).join(' '));
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Расходящийся цикл compute (Cycle detected)
|
|
47
|
+
|
|
48
|
+
Взаимные `compute`/`computeFrom` без стабилизации preact обрывает как «Cycle detected».
|
|
49
|
+
DSL перехватывает это и бросает понятную ошибку с именем поля. Решение — разорвать цикл
|
|
50
|
+
условием `when` или читать одну сторону через `peek()`:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// ❌ взаимный пересчёт без стабилизации
|
|
54
|
+
compute(model.$.a, () => model.b + 1);
|
|
55
|
+
compute(model.$.b, () => model.a + 1); // расходится → Cycle detected
|
|
56
|
+
|
|
57
|
+
// ✅ добавь стабилизирующее условие или однонаправленную зависимость
|
|
58
|
+
compute(model.$.total, () => model.price * model.qty); // одно направление
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Cross-field валидация — через `cross(sig, fn)`
|
|
62
|
+
|
|
63
|
+
Cross-field живёт в схеме валидации (`@reformer/core/validation`), а не в layout: правило вешается на
|
|
64
|
+
ПОЛЕ-НОСИТЕЛЬ ошибки оператором `cross(sig, fn)`, а `fn` читает снапшот текущего scope (`model.get()`).
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { defineValidationSchema, validate, cross, validateModel } from '@reformer/core/validation';
|
|
68
|
+
import { required } from '@reformer/core/validators';
|
|
69
|
+
import { revalidateWhen, type ValidationError } from '@reformer/core';
|
|
70
|
+
|
|
71
|
+
// ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
|
|
72
|
+
const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
|
|
73
|
+
root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;
|
|
74
|
+
const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
|
|
75
|
+
validateFormModel(model, legacySchema);
|
|
76
|
+
|
|
77
|
+
// ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root
|
|
78
|
+
const field1LessThanField2 = (f: Form): ValidationError | null =>
|
|
79
|
+
f.field1 > f.field2 ? { code: 'error', message: 'Invalid' } : null;
|
|
80
|
+
|
|
81
|
+
const schema = defineValidationSchema<Form>(({ model }) => {
|
|
82
|
+
validate(model.$.field1, [required()]);
|
|
83
|
+
cross(model.$.field1, field1LessThanField2); // правило на поле-носителе ошибки
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Прогон — ТОЛЬКО внешним раннером; ошибки сами доезжают до нод формы.
|
|
87
|
+
// ⚠️ form.validate()/submit() схему валидации больше НЕ прогоняют.
|
|
88
|
+
await validateModel(model, schema);
|
|
89
|
+
|
|
90
|
+
// Перезапуск правила при изменении соседнего поля — мост «поведение → валидация»:
|
|
91
|
+
revalidateWhen([model.$.field2], () => void validateModel(model, schema));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> **Актуальный vs удалённый API.** Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`
|
|
95
|
+
> ТЕПЕРЬ существуют — в `@reformer/core/validation` с новыми сигнатурами (ambient, активны только внутри прогона
|
|
96
|
+
> `validateModel`). УДАЛЕНЫ: `applyWhen`, `validateGroup`/`validateTree`/`validateForm`/`validateFormModel`,
|
|
97
|
+
> типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn` и `ctx.form.*`/`ctx.setFieldValue` (наследие
|
|
98
|
+
> path-based архитектуры). См. `17-nonexistent-api.md`.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
## 14. PROJECT STRUCTURE (COLOCATION)
|
|
2
|
+
|
|
3
|
+
One form is one module folder. **Default layout — flat minimalist:** every concern is a single
|
|
4
|
+
file at the module root, and the whole form (entry + all wizard steps inline) lives in one
|
|
5
|
+
`index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
|
|
6
|
+
carried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a
|
|
7
|
+
render-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add
|
|
8
|
+
`renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`
|
|
9
|
+
nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
|
|
10
|
+
and rendered with `FormArraySection` — no per-step component files.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
src/
|
|
14
|
+
├── components/ui/ # App-wide reusable UI (FormField, FormArraySection, ...)
|
|
15
|
+
│
|
|
16
|
+
├── forms/
|
|
17
|
+
│ └── [form-name]/ # Form module — flat, one file per concern
|
|
18
|
+
│ ├── index.tsx # entry + whole form: createModel→createForm→<FormWizard> with all steps inline; arrays via FormArraySection
|
|
19
|
+
│ ├── types.ts # form type + enums + { value, label } option type + constant dictionaries
|
|
20
|
+
│ ├── model.ts # createModel + initial values + empty-array-element factories
|
|
21
|
+
│ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
|
|
22
|
+
│ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
|
|
23
|
+
│ ├── validation.ts # ALL validation → { validateStep, validateAll }
|
|
24
|
+
│ ├── data-sources.ts # options + async loaders (dataSources)
|
|
25
|
+
│ └── api.ts # submit + prefill/load
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
|
|
29
|
+
`index.tsx`; validation is a single `validation.ts`.** This is the working default for
|
|
30
|
+
almost every form — reach for folders only when a file stops fitting on a screen.
|
|
31
|
+
|
|
32
|
+
### Key Files
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// forms/credit-application/types.ts
|
|
36
|
+
export type CreditApplicationForm = {
|
|
37
|
+
loanType: LoanType;
|
|
38
|
+
loanAmount: number | null;
|
|
39
|
+
// ...
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// forms/credit-application/model.ts
|
|
43
|
+
import { createModel, type FormModel } from '@reformer/core';
|
|
44
|
+
export const createCreditApplicationModel = (): FormModel<CreditApplicationForm> =>
|
|
45
|
+
createModel<CreditApplicationForm>(createInitialCreditApplication());
|
|
46
|
+
|
|
47
|
+
// forms/credit-application/form.schema.ts
|
|
48
|
+
import type { FormModel } from '@reformer/core';
|
|
49
|
+
export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
|
|
50
|
+
loanType: { value: model.$.loanType, component: Select, componentProps: { /* ... */ } },
|
|
51
|
+
personalData: personalDataNodes(model.$.personalData),
|
|
52
|
+
properties: { array: model.properties, item: propertyItem },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// forms/credit-application/form.behavior.ts
|
|
56
|
+
import { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';
|
|
57
|
+
export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(({ model }) => {
|
|
58
|
+
compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
|
|
59
|
+
enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline
|
|
63
|
+
import { createForm } from '@reformer/core';
|
|
64
|
+
export const createCreditApplicationForm = () => {
|
|
65
|
+
const model = createCreditApplicationModel();
|
|
66
|
+
return createForm({ model, schema: creditApplicationSchema(model), behavior: creditApplicationBehavior });
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Scaling up: folders (large forms)
|
|
71
|
+
|
|
72
|
+
When the flat module gets unwieldy — steps you want in their own files, sub-forms reused across
|
|
73
|
+
steps, calc/validators that outgrow one file — promote it to a three-folder module: `lib/`
|
|
74
|
+
(domain raw material), `schema/` (the form definition), `components/` (React layout), plus the
|
|
75
|
+
entry component and `index.ts`. Keep the module root to just the entry + `index.ts`; everything
|
|
76
|
+
else lives in a folder.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
forms/
|
|
80
|
+
└── [form-name]/ # Form module — folders (scale-up)
|
|
81
|
+
├── [FormName]Form.tsx # Entry: builds model+form, renders FormWizard + step components
|
|
82
|
+
├── index.ts # Public re-exports of the module
|
|
83
|
+
│
|
|
84
|
+
├── lib/ # Domain helpers (target-agnostic)
|
|
85
|
+
│ ├── types.ts # Form interface + field enums + { value, label } option type
|
|
86
|
+
│ ├── constants.ts # Option dictionaries (LOAN_TYPES, GENDERS, ...)
|
|
87
|
+
│ ├── calc.ts # Pure fns for derived fields (age, monthlyPayment, ...)
|
|
88
|
+
│ ├── custom-validators.ts # Reusable validator factories
|
|
89
|
+
│ └── api.ts # Data sources + submit
|
|
90
|
+
│
|
|
91
|
+
├── schema/ # The form definition
|
|
92
|
+
│ ├── model.ts # createModel factory + initial values + array-element factories
|
|
93
|
+
│ ├── schema.ts # createForm schema tree ({ value: model.$.x, component })
|
|
94
|
+
│ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
|
|
95
|
+
│ ├── behavior.ts # defineFormBehavior(...)
|
|
96
|
+
│ └── create-form.ts # Assembly: createForm({ model, schema, behavior })
|
|
97
|
+
│
|
|
98
|
+
└── components/
|
|
99
|
+
├── steps/ # One component per wizard step
|
|
100
|
+
├── nested-forms/ # Reusable sub-forms (Address, PersonalData, ...)
|
|
101
|
+
└── ui/ # Form-specific helper blocks (summary, warnings, sections)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
|
|
105
|
+
`schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
|
|
106
|
+
may co-locate each step's `schema.ts` / `validation.ts` / `behavior.ts` inside its
|
|
107
|
+
`steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
|
|
108
|
+
the guide below.
|
|
109
|
+
|
|
110
|
+
### Scaling
|
|
111
|
+
|
|
112
|
+
| Complexity | Structure |
|
|
113
|
+
| ---------- | --------------------------------------------------------------------------------------- |
|
|
114
|
+
| Simple | Single file: `index.tsx` (model + schema + behavior + component) |
|
|
115
|
+
| **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 |
|
|
116
|
+
| Folders (`lib/` + `schema/` + `components/`) | Large forms: split concerns into folders, one component per step, reusable `nested-forms/` |
|
|
117
|
+
|
|
118
|
+
> The leading layout is configurable: set `REFORMER_FORM_LAYOUT` (`minimalist` | `folders`)
|
|
119
|
+
> when registering the MCP server to choose which structure the generators lead with. Default
|
|
120
|
+
> is `minimalist`.
|
|
121
|
+
|
|
122
|
+
> Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
|
|
123
|
+
> renderer-json `renderer.schema.json` + `renderer.behavior.ts` + `registry.ts`), the
|
|
124
|
+
> centralized-vs-co-located choice, and the full reuse map live in the
|
|
125
|
+
> **form-directory-layout** guide (`@reformer/mcp`).
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
## 14.5 UI COMPONENT PATTERNS
|
|
2
|
+
|
|
3
|
+
> **Default rule (read first)**: для UI используй `FormField` из
|
|
4
|
+
> [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
|
|
5
|
+
> строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если
|
|
6
|
+
> ui-kit не подходит (другая design system, особый low-level input).
|
|
7
|
+
>
|
|
8
|
+
> Канонический schema-driven подход:
|
|
9
|
+
>
|
|
10
|
+
> - **компонент** объявляется в схеме как `component: Input` (или `Select`, `Checkbox`, etc.)
|
|
11
|
+
> - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
|
|
12
|
+
> - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
|
|
13
|
+
>
|
|
14
|
+
> См. `find_recipe(package="@reformer/ui-kit", topic="form-field-integration")`
|
|
15
|
+
> для полного руководства.
|
|
16
|
+
|
|
17
|
+
### Default — FormField из ui-kit (canonical)
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { useMemo } from 'react';
|
|
21
|
+
import { createModel, createForm } from '@reformer/core';
|
|
22
|
+
import { FormField, Input, Select, Checkbox, Button } from '@reformer/ui-kit';
|
|
23
|
+
|
|
24
|
+
type RegistrationForm = {
|
|
25
|
+
email: string;
|
|
26
|
+
country: string;
|
|
27
|
+
agree: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function RegistrationPage() {
|
|
31
|
+
const form = useMemo(() => {
|
|
32
|
+
const model = createModel<RegistrationForm>({ email: '', country: 'ru', agree: false });
|
|
33
|
+
const schema = {
|
|
34
|
+
email: {
|
|
35
|
+
value: model.$.email,
|
|
36
|
+
component: Input,
|
|
37
|
+
componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
|
|
38
|
+
},
|
|
39
|
+
country: {
|
|
40
|
+
value: model.$.country,
|
|
41
|
+
component: Select,
|
|
42
|
+
componentProps: {
|
|
43
|
+
label: 'Country',
|
|
44
|
+
options: [
|
|
45
|
+
{ value: 'ru', label: 'Россия' },
|
|
46
|
+
{ value: 'by', label: 'Беларусь' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
agree: {
|
|
51
|
+
value: model.$.agree,
|
|
52
|
+
component: Checkbox,
|
|
53
|
+
componentProps: { label: 'I agree to terms' },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
return createForm<RegistrationForm>({ model, schema });
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<form>
|
|
61
|
+
<FormField control={form.email} testId="email" />
|
|
62
|
+
<FormField control={form.country} testId="country" />
|
|
63
|
+
<FormField control={form.agree} testId="agree" />
|
|
64
|
+
<Button type="submit">Register</Button>
|
|
65
|
+
</form>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`FormField` сам читает `componentProps.label`, `componentProps.placeholder`,
|
|
71
|
+
`componentProps.options` через `useFormControl(...).componentProps` и применяет
|
|
72
|
+
их к нужному `<input>`/`<select>`/etc. Error rendering, `pending` для async-валидаций,
|
|
73
|
+
`data-testid` для e2e — всё из коробки.
|
|
74
|
+
|
|
75
|
+
### Anti-patterns (не делай так)
|
|
76
|
+
|
|
77
|
+
❌ **Свои field-компоненты с label-prop'ами в JSX**:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// WRONG — дублирует логику FormField, ломает schema-driven архитектуру
|
|
81
|
+
<Input control={form.email} label="Email" placeholder="..." />
|
|
82
|
+
<Select control={form.country} options={[...]} />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
❌ **Передача компонент-пропсов через JSX вместо схемы**:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// WRONG — нарушает single source of truth (схема)
|
|
89
|
+
<FormField control={form.email} label="Email" />
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
✅ Всё это в схеме:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
{ email: { component: Input, componentProps: { label: 'Email' } } }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<FormField control={form.email} />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Advanced — кастомный input через `children` slot
|
|
103
|
+
|
|
104
|
+
Когда нужен низкоуровневый input, которого нет в ui-kit (маска, особый combobox):
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { FormField } from '@reformer/ui-kit';
|
|
108
|
+
import { InputMask } from 'react-input-mask';
|
|
109
|
+
|
|
110
|
+
<FormField control={form.phone} testId="phone">
|
|
111
|
+
<InputMask mask="+7 (999) 999-99-99" />
|
|
112
|
+
</FormField>;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`children` оборачивается в `CdkFormField.Control asChild` и получает все нужные
|
|
116
|
+
props (`value`, `onChange`, `onBlur`, `aria-invalid`).
|
|
117
|
+
|
|
118
|
+
### Advanced — write your own from scratch (rare)
|
|
119
|
+
|
|
120
|
+
Если ты не хочешь подключать `@reformer/ui-kit`, пиши свои компоненты на основе
|
|
121
|
+
`useFormControl` — но **сохраняй schema-driven подход**: читай label/placeholder
|
|
122
|
+
из `componentProps`, не из JSX-props.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import type { FieldNode } from '@reformer/core';
|
|
126
|
+
import { useFormControl } from '@reformer/core';
|
|
127
|
+
|
|
128
|
+
type MyFormFieldProps<T> = { control: FieldNode<T> }; // ← ОДИН prop
|
|
129
|
+
|
|
130
|
+
function MyFormField<T>({ control }: MyFormFieldProps<T>) {
|
|
131
|
+
const { value, errors, disabled, shouldShowError, componentProps } = useFormControl(control);
|
|
132
|
+
// componentProps = { label, placeholder, type, options, ... } — из СХЕМЫ
|
|
133
|
+
const cp = (componentProps ?? {}) as Record<string, unknown>;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<label>
|
|
137
|
+
{cp.label && <span>{cp.label as string}</span>}
|
|
138
|
+
<input
|
|
139
|
+
type={(cp.type as string) ?? 'text'}
|
|
140
|
+
value={(value ?? '') as string}
|
|
141
|
+
placeholder={cp.placeholder as string | undefined}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
onChange={(e) => (control.setValue as (v: unknown) => void)(e.target.value)}
|
|
144
|
+
onBlur={() => control.markAsTouched()}
|
|
145
|
+
/>
|
|
146
|
+
{shouldShowError && errors[0] && <span>{errors[0].message}</span>}
|
|
147
|
+
</label>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Использование — как у `FormField`:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<MyFormField control={form.email} /> // ← без label-prop
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Integration with UI libraries (shadcn etc.)
|
|
159
|
+
|
|
160
|
+
> **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
|
|
161
|
+
> `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
|
|
162
|
+
> компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
|
|
163
|
+
> контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
|
|
164
|
+
|
|
165
|
+
Если есть существующая design system — оборачивай её компоненты в один
|
|
166
|
+
`MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
|
|
167
|
+
тип input'а — пусть `componentProps.type` диспатчит внутри.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { Input } from '@/components/ui/input';
|
|
171
|
+
import { Label } from '@/components/ui/label';
|
|
172
|
+
|
|
173
|
+
function ShadcnFormField({ control }: { control: FieldNode<string> }) {
|
|
174
|
+
const { value, errors, disabled, componentProps } = useFormControl(control);
|
|
175
|
+
const cp = (componentProps ?? {}) as Record<string, unknown>;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="space-y-2">
|
|
179
|
+
{cp.label && <Label>{cp.label as string}</Label>}
|
|
180
|
+
<Input
|
|
181
|
+
value={(value ?? '') as string}
|
|
182
|
+
onChange={(e) => control.setValue(e.target.value)}
|
|
183
|
+
disabled={disabled}
|
|
184
|
+
/>
|
|
185
|
+
{errors[0] && <p className="text-red-500">{errors[0].message}</p>}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
## 15. NON-EXISTENT API (DO NOT USE)
|
|
2
|
+
|
|
3
|
+
**Следующего API НЕТ в @reformer/core** (наследие старой path-based архитектуры,
|
|
4
|
+
удалено при переходе на M1 и при разделении валидации/поведения):
|
|
5
|
+
|
|
6
|
+
> ⚠️ Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`
|
|
7
|
+
> **существуют** — но живут в сабпути `@reformer/core/validation` (ambient-схема
|
|
8
|
+
> `defineValidationSchema(({ model }) => …)`, раннер `validateModel`), а НЕ в корне и НЕ
|
|
9
|
+
> в `@reformer/core/validators`. Ниже перечислено то, чего действительно нет.
|
|
10
|
+
|
|
11
|
+
| Wrong | Correct | Notes |
|
|
12
|
+
| -------------------------------- | ------------------------------------------------ | ---------------------------------------------- |
|
|
13
|
+
| `useForm` | `createModel` + `createForm` | Хука useForm нет |
|
|
14
|
+
| `validateForm`, `validateFormModel` | `validateModel(model, schema)` из `@reformer/core/validation` | Legacy-движок дерева `{ value, validators }` удалён; внешний раннер — `validateModel` |
|
|
15
|
+
| `validateModelSync` | `await validateModel(model, schema)` | Синхронного раннера нет — прогон асинхронный (`Promise<boolean>`) |
|
|
16
|
+
| `applyWhen` | `validateWhen(() => cond, () => { … })` | Условная валидация — оператором `validateWhen`, не узлом `{ when, children }` |
|
|
17
|
+
| `validateItems`, `validateGroup`, `validateTree` | `each(model.arr, (im) => { … })` | Per-item — оператором `each`; под-схема — прямой вызов `sub({ model: model.child })` |
|
|
18
|
+
| `ValidationSchemaFn`, `BehaviorSchemaFn` | `ValidationSchema<T>` (`defineValidationSchema`) / `defineFormBehavior` | Типы path-схем удалены |
|
|
19
|
+
| `equalTo`, `custom`, `notEmpty` (validators) | inline `Rule<T>` `(value) => err \| null` в `validate(sig, [...])`; сравнение полей — `cross(sig, fn)` | Таких фабрик нет; кастомное правило — обычная функция значения, cross-field — `cross` |
|
|
20
|
+
| `form.submit()` / `form.validate()` прогоняют схему | `await validateModel(model, schema)` перед submit/шагом | submit/validate НЕ запускают ambient-схему валидации |
|
|
21
|
+
| `FieldPath`, `FieldPathNode` | `model.$.field` (`PathAwareSignal`) | Пути заменены сигналами |
|
|
22
|
+
| `ctx.form.x.value.value` | `model.x` / `model.$.x.value` | В behaviors читаем модель напрямую |
|
|
23
|
+
| `ctx.setFieldValue(name, value)` | `model.x = value` / `compute(...)` | Не существует |
|
|
24
|
+
| `transformers`, `createTransformer` | `transformValue(signal, fn)` | Готового набора трансформеров нет |
|
|
25
|
+
| `useHiddenCondition` | `useFormControlValue` + условный рендер в JSX | Хука нет |
|
|
26
|
+
| `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
|
|
27
|
+
| `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |
|
|
28
|
+
|
|
29
|
+
### Common Import Errors
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// WRONG - этих символов / путей НЕ существует
|
|
33
|
+
import { useForm, validateForm, validateFormModel } from '@reformer/core'; // NO!
|
|
34
|
+
import { validate, applyWhen, equalTo } from '@reformer/core/validators'; // NO! операторов тут нет
|
|
35
|
+
import { transformers } from '@reformer/core/behaviors'; // NO!
|
|
36
|
+
import type { FieldPath, ValidationSchemaFn } from '@reformer/core'; // NO!
|
|
37
|
+
|
|
38
|
+
// CORRECT
|
|
39
|
+
import { createModel, createForm, useFormControl } from '@reformer/core';
|
|
40
|
+
import type { FormModel, ValidationError, FieldConfig } from '@reformer/core';
|
|
41
|
+
// Операторы валидации + раннер + defineValidationSchema — отдельный сабпуть:
|
|
42
|
+
import {
|
|
43
|
+
validate, validateAsync, validateWhen, cross, each, apply,
|
|
44
|
+
defineValidationSchema, validateModel,
|
|
45
|
+
type Rule, type AsyncRule, type ValidationSchema,
|
|
46
|
+
} from '@reformer/core/validation';
|
|
47
|
+
// Фабрики-валидаторы (value-only, принимают nullable):
|
|
48
|
+
import { required, email, min, minLength, pattern } from '@reformer/core/validators';
|
|
49
|
+
// Поведение (отдельный слой, контракт не менялся):
|
|
50
|
+
import { defineFormBehavior, compute, onChange, revalidateWhen } from '@reformer/core/behaviors';
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Schema Common Mistakes
|
|
54
|
+
|
|
55
|
+
Валидация и layout — **разные** контракты. Layout-узел привязывает поле к сигналу и
|
|
56
|
+
НЕ несёт `validators`; правила живут в отдельной `defineValidationSchema`.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// WRONG - примитив/литерал вместо сигнала, и validators прямо в layout-узле
|
|
60
|
+
const schema = {
|
|
61
|
+
name: '', // нет привязки к сигналу
|
|
62
|
+
email: { value: '', validators: [required()] }, // value — литерал, а validators в layout больше нет
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// CORRECT - layout только связывает поле с сигналом модели...
|
|
66
|
+
const layout = {
|
|
67
|
+
name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
|
|
68
|
+
email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
|
|
72
|
+
const validation = defineValidationSchema<Form>(({ model }) => {
|
|
73
|
+
validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Behaviors Common Mistakes
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// WRONG - строковые пути и (form) => ... — старый API
|
|
81
|
+
enableWhen(path.city, (form) => Boolean(form.country));
|
|
82
|
+
|
|
83
|
+
// CORRECT - сигналы модели, условие читает model
|
|
84
|
+
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Поведение **не владеет** валидацией. Чтобы поведение инициировало прогон схемы — мост
|
|
88
|
+
`revalidateWhen` (validate/submit сами схему не запускают):
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// CORRECT - поведение дёргает внешний раннер валидации при изменении зависимости
|
|
92
|
+
revalidateWhen([model.$.password], () => void validateModel(model, validation));
|
|
93
|
+
```
|