@reformer/core 11.0.0-beta.5 → 11.0.0-beta.6

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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Адаптер поля — как свести value-based seam формы (`value` + `onChange(value)` + `onBlur`) к
3
+ * контракту конкретного контрола. Чистые данные и чистые функции (без React): один механизм на
4
+ * всех, кто связывает поле с контролом, — `FormField.Control` из `@reformer/cdk` и рендерер
5
+ * `@reformer/renderer-react`.
6
+ *
7
+ * Контрол объявляет свой диалект статикой `reformerAdapter` (см. {@link getFieldAdapter}), поэтому
8
+ * отдельные «field-версии» компонентов не нужны: в `component` поля кладётся сам компонент.
9
+ *
10
+ * @module platforms/react/field-binding
11
+ */
12
+ /**
13
+ * Адаптер поля: описывает диалект контрола (`checked`/`onCheckedChange`, `value`/`onValueChange`,
14
+ * DOM-событие в `onChange`, …). Все ключи необязательны — по умолчанию seam проходит как есть.
15
+ *
16
+ * @example Radix Checkbox
17
+ * ```ts
18
+ * const checkedAdapter: FieldAdapter = {
19
+ * valueProp: 'checked',
20
+ * changeProp: 'onCheckedChange',
21
+ * fromEmit: (c) => c === true,
22
+ * toValue: (v) => v ?? false,
23
+ * };
24
+ * ```
25
+ */
26
+ export interface FieldAdapter {
27
+ /** Проп, из которого контрол читает значение (по умолчанию `'value'`). */
28
+ valueProp?: string;
29
+ /** Колбэк, через который контрол эмитит изменение (по умолчанию `'onChange'`). */
30
+ changeProp?: string;
31
+ /**
32
+ * Эмит контрола → значение поля (по умолчанию — как есть). `rest` — прочие props контрола
33
+ * (например, чтобы достать `options` при резолве значения).
34
+ */
35
+ fromEmit?: (arg: unknown, rest: Record<string, unknown>) => unknown;
36
+ /** Значение поля → `valueProp` контрола (коэрсия `null`/`undefined`; по умолчанию — как есть). */
37
+ toValue?: (value: unknown) => unknown;
38
+ /** Проброс blur нестандартным каналом (по умолчанию — `onBlur`). */
39
+ bindBlur?: (onBlur: () => void) => Record<string, unknown>;
40
+ /** Ключи, которые убрать из `componentProps` перед спредом в контрол. */
41
+ strip?: string[];
42
+ /**
43
+ * Передавать ли контролу ноду формы пропом `control` (только рендерер). По умолчанию `false`:
44
+ * errors/touched обслуживает обёртка поля. Ставь `true`, если контрол сам вызывает
45
+ * `useFormControl(control)`.
46
+ */
47
+ passControl?: boolean;
48
+ }
49
+ /** Компонент, объявивший свой диалект статикой. */
50
+ export interface FieldAdapterCarrier {
51
+ reformerAdapter?: FieldAdapter;
52
+ }
53
+ /**
54
+ * Props, которые адресованы обёртке поля, а не контролу: `labelTooltip` рисует `FormField` у
55
+ * подписи. Они приходят в общем мешке `componentProps`, и без среза утекли бы в DOM контрола.
56
+ */
57
+ export declare const FIELD_WRAPPER_ONLY_PROPS: readonly string[];
58
+ /**
59
+ * Адаптер, объявленный компонентом статикой `reformerAdapter`, либо `undefined`.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * Checkbox.reformerAdapter = checkedAdapter;
64
+ * getFieldAdapter(Checkbox); // → checkedAdapter
65
+ * ```
66
+ */
67
+ export declare function getFieldAdapter(component: unknown): FieldAdapter | undefined;
68
+ /** Seam поля: текущее значение и value-based колбэки. */
69
+ export interface FieldSeam {
70
+ value: unknown;
71
+ onChange: (value: unknown) => void;
72
+ onBlur: () => void;
73
+ }
74
+ /**
75
+ * Props контрола под адаптер: срезает служебные ключи обёртки и `strip` адаптера, кладёт значение
76
+ * в `valueProp` (через `toValue`), вешает `changeProp`, который прогоняет эмит контрола через
77
+ * `fromEmit` в value-based `onChange` поля, и пробрасывает blur (`bindBlur` либо `onBlur`).
78
+ * Без адаптера seam проходит как есть (`value` / `onChange` / `onBlur`).
79
+ *
80
+ * `componentProps` спредятся ПЕРВЫМИ — seam их перекрывает при совпадении ключей.
81
+ *
82
+ * @param adapter - Адаптер контрола или `undefined` (value-based контрол).
83
+ * @param seam - Значение поля и его колбэки.
84
+ * @param componentProps - Прочие props контрола.
85
+ * @returns Плоский объект props для спреда в контрол.
86
+ */
87
+ export declare function bindFieldProps(adapter: FieldAdapter | undefined, seam: FieldSeam, componentProps?: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,50 @@
1
+ import { Ref, RefObject } from 'react';
2
+ /**
3
+ * Базовый императивный контракт поля формы.
4
+ *
5
+ * Обёртка поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`)
6
+ * строит его из DOM-узла контрола. Композиты со своим handle (`open`/`close`/`reload`/…) отдают
7
+ * его как есть — он расширяет этот контракт.
8
+ *
9
+ * Достаётся из render-схемы по селектору:
10
+ * `schema.node(sel).getRef<FieldHandle>().current?.focus()`.
11
+ *
12
+ * Покрывает ТОЛЬКО императивные действия. Реактивное состояние (value / disabled / options /
13
+ * validation) живёт в behaviors и через handle не дублируется.
14
+ */
15
+ export interface FieldHandle {
16
+ /** Сфокусировать поле (делегирует на DOM-элемент контрола). */
17
+ focus(): void;
18
+ /** Снять фокус. */
19
+ blur(): void;
20
+ /** Проскроллить поле в область видимости. */
21
+ scrollIntoView(opts?: ScrollIntoViewOptions): void;
22
+ /** Живой DOM-элемент поля (или `null` до монтирования / для размонтированной ноды). */
23
+ getElement(): HTMLElement | null;
24
+ }
25
+ /**
26
+ * Собирает базовый {@link FieldHandle}, делегирующий на DOM-элемент по ссылке `el`.
27
+ * Все вызовы null-safe: до монтирования (`el.current === null`) — no-op, без исключений.
28
+ */
29
+ export declare function makeElementFieldHandle(el: RefObject<HTMLElement | null>): FieldHandle;
30
+ /**
31
+ * Ref для контрола поля + императивный handle для потребителя.
32
+ *
33
+ * Возвращённый ref вешается на контрол. На `outerRef` публикуется:
34
+ * - handle самого контрола, если тот его реализует (`useImperativeHandle` композита);
35
+ * - иначе — базовый {@link FieldHandle}, построенный из DOM-узла контрола.
36
+ *
37
+ * Хук вызывается безусловно; вешать ref на контрол стоит только при заданном `outerRef` —
38
+ * так function-компонент без ref-поддержки не получает лишний ref.
39
+ *
40
+ * @param outerRef - ref потребителя (из `forwardRef` или `schema.node(sel).getRef()`).
41
+ * @returns ref, который нужно передать контролу.
42
+ */
43
+ export declare function useFieldHandleRef<H = FieldHandle>(outerRef: Ref<H> | undefined): RefObject<unknown>;
44
+ /**
45
+ * Что публиковать потребителю по ref контрола: handle композита как есть, иначе (DOM-узел или
46
+ * ещё пусто) — базовый {@link FieldHandle}, читающий узел лениво из `inner`.
47
+ *
48
+ * @internal Экспортируется для юнит-тестов.
49
+ */
50
+ export declare function resolveFieldHandle(inner: RefObject<unknown>): unknown;
@@ -21,3 +21,7 @@ export type { UseFormValidationArgs, UseFormValidationResult } from './hooks/use
21
21
  export { useFormBundle } from './hooks/use-form-bundle';
22
22
  export type { FormBundleLike } from './hooks/use-form-bundle';
23
23
  export type { FieldControlState, ArrayControlState } from './hooks/types';
24
+ export { getFieldAdapter, bindFieldProps, FIELD_WRAPPER_ONLY_PROPS, } from './field-binding/field-adapter';
25
+ export type { FieldAdapter, FieldAdapterCarrier, FieldSeam } from './field-binding/field-adapter';
26
+ export { makeElementFieldHandle, useFieldHandleRef } from './field-binding/field-handle';
27
+ export type { FieldHandle } from './field-binding/field-handle';
@@ -18,7 +18,7 @@
18
18
  import { createCoreForm, createModel, useFormBundle, type FormProxy } from '@reformer/core';
19
19
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
20
20
  import { required, email } from '@reformer/core/validators';
21
- import { FormField, InputField, Button } from '@reformer/ui-kit';
21
+ import { FormField, Input, Button } from '@reformer/ui-kit';
22
22
 
23
23
  // 1. Define form type as `type` alias (not `interface` — see Recipe 2)
24
24
  type ContactForm = {
@@ -34,12 +34,12 @@ const model = createModel<ContactForm>({ name: '', email: '' });
34
34
  const schema = {
35
35
  name: {
36
36
  value: model.$.name,
37
- component: InputField,
37
+ component: Input,
38
38
  componentProps: { label: 'Name', placeholder: 'Your name' },
39
39
  },
40
40
  email: {
41
41
  value: model.$.email,
42
- component: InputField,
42
+ component: Input,
43
43
  componentProps: { label: 'Email', type: 'email' },
44
44
  },
45
45
  };
@@ -113,14 +113,14 @@ const model = createModel<MyForm>({ properties: [] });
113
113
  const propertyItem = (item: FormModel<PropertyItem>) => ({
114
114
  type: {
115
115
  value: item.$.type,
116
- component: SelectField,
116
+ component: SelectAsync,
117
117
  componentProps: { label: 'Тип', options: [/* ... */] },
118
118
  },
119
- description: { value: item.$.description, component: TextareaField, componentProps: { label: 'Описание' } },
119
+ description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
120
120
  estimatedValue: {
121
121
  value: item.$.estimatedValue,
122
- component: InputField,
123
- componentProps: { label: 'Стоимость', type: 'number' },
122
+ component: InputNumber,
123
+ componentProps: { label: 'Стоимость' },
124
124
  },
125
125
  });
126
126
 
@@ -65,16 +65,16 @@ import { createModel, createForm } from '@reformer/core';
65
65
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
66
66
  import { required, min, max, email } from '@reformer/core/validators';
67
67
 
68
- type Loan = { email: string; age: number; amount: number };
69
- const model = createModel<Loan>({ email: '', age: 0, amount: 0 });
68
+ type Loan = { email: string; age: number | null; amount: number | null };
69
+ const model = createModel<Loan>({ email: '', age: null, amount: null });
70
70
 
71
71
  // layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
72
72
  const form = createForm({
73
73
  model,
74
74
  schema: {
75
- email: { value: model.$.email, component: InputField },
76
- age: { value: model.$.age, component: InputField },
77
- amount: { value: model.$.amount, component: InputField },
75
+ email: { value: model.$.email, component: Input },
76
+ age: { value: model.$.age, component: InputNumber },
77
+ amount: { value: model.$.amount, component: InputNumber },
78
78
  },
79
79
  });
80
80
 
@@ -93,7 +93,7 @@ computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => pr
93
93
  ```typescript
94
94
  // ❌ WRONG - дерево { value, validators } и позиционная строка удалены
95
95
  const schema = {
96
- email: { value: model.$.email, component: InputField, validators: [required(), email()] },
96
+ email: { value: model.$.email, component: Input, validators: [required(), email()] },
97
97
  };
98
98
  validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
99
99
 
@@ -10,7 +10,7 @@ layout-схеме НЕТ — правила живут в отдельной с
10
10
  ```typescript
11
11
  {
12
12
  value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
13
- component: InputField, // React-компонент
13
+ component: Input, // React-компонент
14
14
  componentProps?: object, // пропсы (label, placeholder, options, type, ...)
15
15
  disabled?: boolean,
16
16
  updateOn?: 'change' | 'blur' | 'submit',
@@ -28,7 +28,7 @@ layout-схеме НЕТ — правила живут в отдельной с
28
28
  import { createModel, createForm } from '@reformer/core';
29
29
  import { defineValidationSchema, validate } from '@reformer/core/validation';
30
30
  import { required } from '@reformer/core/validators';
31
- import { InputField, SelectField, CheckboxField } from '@reformer/ui-kit';
31
+ import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
32
32
 
33
33
  const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
34
34
 
@@ -40,22 +40,22 @@ const myValidation = defineValidationSchema<MyForm>(({ model }) => {
40
40
  const schema = {
41
41
  name: {
42
42
  value: model.$.name,
43
- component: InputField,
43
+ component: Input,
44
44
  componentProps: { label: 'Name', placeholder: 'Enter name' },
45
45
  },
46
46
  age: {
47
47
  value: model.$.age,
48
- component: InputField,
49
- componentProps: { type: 'number', label: 'Age' },
48
+ component: InputNumber,
49
+ componentProps: { label: 'Age' },
50
50
  },
51
51
  agree: {
52
52
  value: model.$.agree,
53
- component: CheckboxField,
53
+ component: CheckboxWithLabel,
54
54
  componentProps: { label: 'I agree to terms' },
55
55
  },
56
56
  status: {
57
57
  value: model.$.status,
58
- component: SelectField,
58
+ component: SelectAsync,
59
59
  componentProps: {
60
60
  label: 'Status',
61
61
  options: [
@@ -79,9 +79,9 @@ const form = createForm<MyForm>({ model, schema });
79
79
  import type { FormModel } from '@reformer/core';
80
80
 
81
81
  const addressNodes = (m: FormModel<Address>) => ({
82
- street: { value: m.$.street, component: InputField, componentProps: { label: 'Street' } },
83
- city: { value: m.$.city, component: InputField, componentProps: { label: 'City' } },
84
- zip: { value: m.$.zip, component: InputField, componentProps: { label: 'ZIP' } },
82
+ street: { value: m.$.street, component: Input, componentProps: { label: 'Street' } },
83
+ city: { value: m.$.city, component: Input, componentProps: { label: 'City' } },
84
+ zip: { value: m.$.zip, component: Input, componentProps: { label: 'ZIP' } },
85
85
  });
86
86
 
87
87
  const schema = {
@@ -98,8 +98,8 @@ const schema = {
98
98
  import type { FormModel } from '@reformer/core';
99
99
 
100
100
  const itemSchema = (item: FormModel<Item>) => ({
101
- id: { value: item.$.id, component: InputField, componentProps: { label: 'ID' } },
102
- name: { value: item.$.name, component: InputField, componentProps: { label: 'Name' } },
101
+ id: { value: item.$.id, component: Input, componentProps: { label: 'ID' } },
102
+ name: { value: item.$.name, component: Input, componentProps: { label: 'Name' } },
103
103
  });
104
104
 
105
105
  const schema = {
@@ -7,7 +7,7 @@
7
7
 
8
8
  ```typescript
9
9
  import { createModel, createForm, type FormModel } from '@reformer/core';
10
- import { InputField } from '@reformer/ui-kit';
10
+ import { Input, InputNumber } from '@reformer/ui-kit';
11
11
 
12
12
  type Item = { id: string; name: string; price: number };
13
13
  type MyForm = { items: Item[] };
@@ -16,9 +16,9 @@ const model = createModel<MyForm>({ items: [] });
16
16
 
17
17
  // под-схема одного элемента (item.$.field — сигнал под-модели)
18
18
  const itemSchema = (item: FormModel<Item>) => ({
19
- id: { value: item.$.id, component: InputField },
20
- name: { value: item.$.name, component: InputField },
21
- price: { value: item.$.price, component: InputField, componentProps: { type: 'number' } },
19
+ id: { value: item.$.id, component: Input },
20
+ name: { value: item.$.name, component: Input },
21
+ price: { value: item.$.price, component: InputNumber },
22
22
  });
23
23
 
24
24
  const schema = {
@@ -71,7 +71,7 @@ import { revalidateWhen, type ValidationError } from '@reformer/core';
71
71
  // ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
72
72
  const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
73
73
  root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;
74
- const legacySchema = { field1: { value: model.$.field1, component: InputField, validators: [legacyRule] } };
74
+ const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
75
75
  validateFormModel(model, legacySchema);
76
76
 
77
77
  // ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root
@@ -2,11 +2,13 @@
2
2
 
3
3
  One form is one module folder. **Default layout — flat minimalist:** every concern is a single
4
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`
5
+ `index.tsx`. Naming rule: **`form.<role>` is a form artefact, the suffix names its role** —
6
+ `form.schema.*` (layout), `form.behavior.ts` (model behavior), `form.render.ts` (render-layer
7
+ behavior, renderer targets only), `form.validation.ts` (validation rules); every other file has
8
+ no prefix. A wizard may instead keep each
9
+ step's code in `steps/<slug>/` (same file names inside) with a `steps/index.ts` aggregator. The old
10
+ `renderer.schema.*` / `renderer.behavior.ts` / `renderer.wizard.tsx` / `validation.ts` names still
11
+ work with a warning. No `lib/` / `schema/` / `components/` nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
10
12
  and rendered with `FormArraySection` — no per-step component files.
11
13
 
12
14
  ```
@@ -20,13 +22,13 @@ src/
20
22
  │ ├── model.ts # createModel + initial values + empty-array-element factories
21
23
  │ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
22
24
  │ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
23
- │ ├── validation.ts # ALL validation → { validateStep, validateAll }
25
+ │ ├── form.validation.ts # ALL validation → { validateStep, validateAll }
24
26
  │ ├── data-sources.ts # options + async loaders (dataSources)
25
27
  │ └── api.ts # submit + prefill/load
26
28
  ```
27
29
 
28
30
  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
31
+ `index.tsx`; validation is a single `form.validation.ts`.** This is the working default for
30
32
  almost every form — reach for folders only when a file stops fitting on a screen.
31
33
 
32
34
  ### Key Files
@@ -47,7 +49,7 @@ export const createCreditApplicationModel = (): FormModel<CreditApplicationForm>
47
49
  // forms/credit-application/form.schema.ts
48
50
  import type { FormModel } from '@reformer/core';
49
51
  export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
50
- loanType: { value: model.$.loanType, component: SelectField, componentProps: { /* ... */ } },
52
+ loanType: { value: model.$.loanType, component: SelectAsync, componentProps: { /* ... */ } },
51
53
  personalData: personalDataNodes(model.$.personalData),
52
54
  properties: { array: model.properties, item: propertyItem },
53
55
  });
@@ -95,7 +97,7 @@ forms/
95
97
  ├── schema/ # The form definition
96
98
  │ ├── model.ts # createModel factory + initial values + array-element factories
97
99
  │ ├── form.schema.ts # schema builder (model) => tree ({ value: model.$.x, component })
98
- │ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
100
+ │ ├── form.validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
99
101
  │ ├── form.behavior.ts # defineFormBehavior(...)
100
102
  │ └── create-form.ts # Assembly: createCoreForm({ model, schema, behavior, validation })
101
103
  │
@@ -107,7 +109,7 @@ forms/
107
109
 
108
110
  Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
109
111
  `schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
110
- may co-locate each step's `form.schema.ts` / `validation.ts` / `form.behavior.ts` inside its
112
+ may co-locate each step's `form.schema.ts` / `form.validation.ts` / `form.behavior.ts` inside its
111
113
  `steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
112
114
  the guide below.
113
115
 
@@ -123,8 +125,8 @@ the guide below.
123
125
  > when registering the MCP server to choose which structure the generators lead with. Default
124
126
  > is `minimalist`.
125
127
 
126
- > Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
127
- > renderer-json `renderer.schema.ts` + `renderer.behavior.ts` + `registry.ts`, plus an optional
128
- > `renderer.wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located
128
+ > Cross-target variants (renderer-react `form.schema.ts` + `form.render.ts` /
129
+ > renderer-json `form.schema.ts` + `form.render.ts` + `registry.ts`, plus an optional
130
+ > `wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located
129
131
  > choice, and the full reuse map live in the **form-directory-layout** guide (`@reformer/mcp`) —
130
132
  > `find_recipe directory-layout`.
@@ -2,12 +2,13 @@
2
2
 
3
3
  > **Default rule (read first)**: для UI используй `FormField` из
4
4
  > [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
5
- > строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если
5
+ > строкой `<FormField control={form.x} />`. Свои обёртки поля пиши ТОЛЬКО если
6
6
  > ui-kit не подходит (другая design system, особый low-level input).
7
7
  >
8
8
  > Канонический schema-driven подход:
9
9
  >
10
- > - **компонент** объявляется в схеме как `component: InputField` (или `Select`, `Checkbox`, etc.)
10
+ > - **компонент** объявляется в схеме как `component: Input` (или `SelectAsync`, `CheckboxWithLabel`, …) —
11
+ > сам компонент кита, отдельных «field-версий» нет: связывает его с полем обёртка (`FormField`)
11
12
  > - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
12
13
  > - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
13
14
  >
@@ -19,7 +20,7 @@
19
20
  ```tsx
20
21
  import { useMemo } from 'react';
21
22
  import { createModel, createForm } from '@reformer/core';
22
- import { FormField, InputField, SelectField, CheckboxField, Button } from '@reformer/ui-kit';
23
+ import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
23
24
 
24
25
  type RegistrationForm = {
25
26
  email: string;
@@ -33,12 +34,12 @@ function RegistrationPage() {
33
34
  const schema = {
34
35
  email: {
35
36
  value: model.$.email,
36
- component: InputField,
37
+ component: Input,
37
38
  componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
38
39
  },
39
40
  country: {
40
41
  value: model.$.country,
41
- component: SelectField,
42
+ component: SelectAsync,
42
43
  componentProps: {
43
44
  label: 'Country',
44
45
  options: [
@@ -49,7 +50,7 @@ function RegistrationPage() {
49
50
  },
50
51
  agree: {
51
52
  value: model.$.agree,
52
- component: CheckboxField,
53
+ component: CheckboxWithLabel,
53
54
  componentProps: { label: 'I agree to terms' },
54
55
  },
55
56
  };
@@ -92,7 +93,7 @@ function RegistrationPage() {
92
93
  ✅ Всё это в схеме:
93
94
 
94
95
  ```ts
95
- { email: { component: InputField, componentProps: { label: 'Email' } } }
96
+ { email: { component: Input, componentProps: { label: 'Email' } } }
96
97
  ```
97
98
 
98
99
  ```tsx
@@ -113,7 +114,8 @@ import { InputMask } from 'react-input-mask';
113
114
  ```
114
115
 
115
116
  `children` оборачивается в `CdkFormField.Control asChild` и получает все нужные
116
- props (`value`, `onChange`, `onBlur`, `aria-invalid`).
117
+ props (`value`, `onChange`, `onBlur`, `aria-invalid`) — в диалекте ребёнка, если тот объявил
118
+ статику `reformerAdapter` (`defineFieldControl` из `@reformer/ui-kit/fields`), иначе value-based.
117
119
 
118
120
  ### Advanced — write your own from scratch (rare)
119
121
 
@@ -155,12 +157,37 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
155
157
  <MyFormField control={form.email} /> // ← без label-prop
156
158
  ```
157
159
 
160
+ ### Связывание поля с контролом — `FieldAdapter` (`@reformer/core`)
161
+
162
+ Контролы говорят на разных диалектах (`checked` + `onCheckedChange`, `onValueChange`, DOM-событие
163
+ в `onChange`), форма — на value-based seam (`value` + `onChange(value)` + `onBlur`). Перевод
164
+ описывает `FieldAdapter` (`valueProp`, `changeProp`, `fromEmit`, `toValue`, `bindBlur`, `strip`,
165
+ `passControl` — все необязательны). Компонент объявляет его статикой `reformerAdapter`, а обёртка
166
+ поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`) читает статику
167
+ и связывает поле сама — отдельные «field-версии» компонентов не нужны.
168
+
169
+ | Символ | Назначение |
170
+ | ----------------------------- | --------------------------------------------------------------------------- |
171
+ | `FieldAdapter` | Тип адаптера: диалект контрола |
172
+ | `getFieldAdapter(component)` | Статика `component.reformerAdapter` либо `undefined` |
173
+ | `bindFieldProps(adapter, seam, props)` | Props контрола под адаптер (без адаптера — seam как есть) |
174
+ | `FieldHandle` | Императивный handle поля: `focus` / `blur` / `scrollIntoView` / `getElement` |
175
+ | `makeElementFieldHandle(ref)` | Базовый `FieldHandle` из DOM-узла |
176
+ | `useFieldHandleRef(outerRef)` | Ref для контрола + публикация handle потребителю (строит ОБЁРТКА поля) |
177
+
178
+ В приложении обычно достаточно `defineFieldControl(MyControl, { adapter })` из
179
+ `@reformer/ui-kit/fields` (плюс готовые пресеты `checkedAdapter`, `valueChangeAdapter`, …); для
180
+ компонентов чужой библиотеки без статики — `RendererSettings.resolveFieldAdapter`.
181
+
158
182
  ### Integration with UI libraries (shadcn etc.)
159
183
 
160
184
  > **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
161
185
  > `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
162
186
  > компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
163
- > контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
187
+ > контрола. Свой компонент можно подготовить и один раз — статикой
188
+ > `defineFieldControl(MyInput, { adapter })` из `@reformer/ui-kit/fields`: его поймут и рендерер,
189
+ > и `FormField.Control` из `@reformer/cdk`. Ручная обёртка на `useFormControl` (ниже) нужна для
190
+ > прямого JSX без `FormField` и рендерера.
164
191
 
165
192
  Если есть существующая design system — оборачивай её компоненты в один
166
193
  `MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
@@ -26,6 +26,20 @@
26
26
  | `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
27
27
  | `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |
28
28
 
29
+ ### UI: удалённые field-версии компонентов (`@reformer/ui-kit`)
30
+
31
+ Отдельных «field-версий» у компонентов кита больше нет: в `component` поля кладётся сам
32
+ компонент, диалект он объявляет статикой `reformerAdapter`, связывает поле обёртка
33
+ (`FormField.Control` / рендерер).
34
+
35
+ | Wrong | Correct |
36
+ | ---------------------------------------------- | --------------------------------------------------------- |
37
+ | `InputField`, `SelectField`, `CheckboxField`, … (любой `*Field`) | `Input`, `SelectAsync`, `CheckboxWithLabel`, … — таблица в ui-kit `01-overview.md` |
38
+ | `InputField` + `type: 'number'` | `InputNumber` |
39
+ | `InputField` + `suggestions` | `InputSuggest` |
40
+ | `FileUploadField` + `variant: 'dropzone' \| 'input'` | `FileUploadDropzone` / `FileUploadInput` (без `variant`) |
41
+ | `withFormControl(MyControl, adapter)` | `defineFieldControl(MyControl, { adapter })` из `@reformer/ui-kit/fields` |
42
+
29
43
  ### Common Import Errors
30
44
 
31
45
  ```typescript
@@ -64,8 +78,8 @@ const schema = {
64
78
 
65
79
  // CORRECT - layout только связывает поле с сигналом модели...
66
80
  const layout = {
67
- name: { value: model.$.name, component: InputField, componentProps: { label: 'Name' } },
68
- email: { value: model.$.email, component: InputField, componentProps: { label: 'Email' } },
81
+ name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
82
+ email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
69
83
  };
70
84
 
71
85
  // ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
@@ -31,16 +31,16 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
31
31
 
32
32
  ```typescript
33
33
  import { createModel, createForm } from '@reformer/core';
34
- import { InputField, SelectField } from '@reformer/ui-kit';
34
+ import { Input, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
35
35
 
36
36
  type ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };
37
37
 
38
38
  const model = createModel<ProfileForm>({ username: '', language: 'ru', marketing: true });
39
39
  const schema = {
40
- username: { value: model.$.username, component: InputField, componentProps: { label: 'Username' } },
40
+ username: { value: model.$.username, component: Input, componentProps: { label: 'Username' } },
41
41
  language: {
42
42
  value: model.$.language,
43
- component: SelectField,
43
+ component: SelectAsync,
44
44
  componentProps: {
45
45
  label: 'Язык',
46
46
  options: [
@@ -49,7 +49,7 @@ const schema = {
49
49
  ],
50
50
  },
51
51
  },
52
- marketing: { value: model.$.marketing, component: InputField, componentProps: { type: 'checkbox' } },
52
+ marketing: { value: model.$.marketing, component: CheckboxWithLabel, componentProps: { label: 'Рассылка' } },
53
53
  };
54
54
  const form = createForm({ model, schema });
55
55
  // model.get() === { username: '', language: 'ru', marketing: true }
@@ -59,7 +59,7 @@ Layout-схема БЕЗ валидаторов — правила живут в
59
59
  const model = createModel<CreditApplicationForm>(initial);
60
60
 
61
61
  const schema = {
62
- loanAmount: { value: model.$.loanAmount, component: InputField },
62
+ loanAmount: { value: model.$.loanAmount, component: InputNumber },
63
63
  // вложенная группа — builder, принимающий ModelSignals<Sub>
64
64
  personalData: personalDataNodes(model.$.personalData),
65
65
  // массив — { array, item }