@reformer/core 11.0.0-beta.4 → 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.
- package/dist/form/create-form.d.ts +6 -4
- package/dist/form/factories/node-factory.d.ts +1 -0
- package/dist/form/nodes/field-node.d.ts +3 -0
- package/dist/form/validators/email.d.ts +5 -6
- package/dist/form/validators/file-type.d.ts +5 -6
- package/dist/form/validators/future-date.d.ts +5 -6
- package/dist/form/validators/index.d.ts +3 -2
- package/dist/form/validators/integer.d.ts +5 -6
- package/dist/form/validators/is-date.d.ts +5 -6
- package/dist/form/validators/is-number.d.ts +5 -6
- package/dist/form/validators/max-age.d.ts +5 -6
- package/dist/form/validators/max-date.d.ts +5 -6
- package/dist/form/validators/max-file-size.d.ts +5 -6
- package/dist/form/validators/max-files.d.ts +5 -6
- package/dist/form/validators/max-length.d.ts +6 -7
- package/dist/form/validators/max-total-file-size.d.ts +5 -6
- package/dist/form/validators/max.d.ts +6 -7
- package/dist/form/validators/min-age.d.ts +5 -6
- package/dist/form/validators/min-date.d.ts +5 -6
- package/dist/form/validators/min-file-size.d.ts +5 -6
- package/dist/form/validators/min-files.d.ts +5 -6
- package/dist/form/validators/min-length.d.ts +6 -7
- package/dist/form/validators/min.d.ts +6 -7
- package/dist/form/validators/multiple-of.d.ts +5 -6
- package/dist/form/validators/non-negative.d.ts +5 -6
- package/dist/form/validators/non-zero.d.ts +5 -6
- package/dist/form/validators/past-date.d.ts +5 -6
- package/dist/form/validators/pattern.d.ts +6 -14
- package/dist/form/validators/phone.d.ts +5 -6
- package/dist/form/validators/required.d.ts +7 -12
- package/dist/form/validators/url.d.ts +7 -12
- package/dist/index.js +423 -376
- package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
- package/dist/platforms/react/field-binding/field-handle.d.ts +50 -0
- package/dist/platforms/react/index.d.ts +4 -0
- package/docs/llms/02-quick-start.md +3 -3
- package/docs/llms/03-api-signatures.md +4 -4
- package/docs/llms/09-formschema.md +5 -5
- package/docs/llms/10-arrays.md +2 -2
- package/docs/llms/15-project-structure.md +19 -16
- package/docs/llms/16-ui-components.md +34 -7
- package/docs/llms/17-nonexistent-api.md +14 -0
- package/docs/llms/29-async-preload.md +3 -3
- package/docs/llms/30-type-safety-recipes.md +1 -1
- package/llms-index.json +1 -0
- package/llms.txt +2146 -68
- package/package.json +2 -1
|
@@ -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';
|
|
@@ -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:
|
|
116
|
+
component: SelectAsync,
|
|
117
117
|
componentProps: { label: 'Тип', options: [/* ... */] },
|
|
118
118
|
},
|
|
119
119
|
description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
|
|
120
120
|
estimatedValue: {
|
|
121
121
|
value: item.$.estimatedValue,
|
|
122
|
-
component:
|
|
123
|
-
componentProps: { label: 'Стоимость'
|
|
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:
|
|
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
75
|
email: { value: model.$.email, component: Input },
|
|
76
|
-
age: { value: model.$.age, component:
|
|
77
|
-
amount: { value: model.$.amount, component:
|
|
76
|
+
age: { value: model.$.age, component: InputNumber },
|
|
77
|
+
amount: { value: model.$.amount, component: InputNumber },
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
80
|
|
|
@@ -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 { Input,
|
|
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
|
|
|
@@ -45,17 +45,17 @@ const schema = {
|
|
|
45
45
|
},
|
|
46
46
|
age: {
|
|
47
47
|
value: model.$.age,
|
|
48
|
-
component:
|
|
49
|
-
componentProps: {
|
|
48
|
+
component: InputNumber,
|
|
49
|
+
componentProps: { label: 'Age' },
|
|
50
50
|
},
|
|
51
51
|
agree: {
|
|
52
52
|
value: model.$.agree,
|
|
53
|
-
component:
|
|
53
|
+
component: CheckboxWithLabel,
|
|
54
54
|
componentProps: { label: 'I agree to terms' },
|
|
55
55
|
},
|
|
56
56
|
status: {
|
|
57
57
|
value: model.$.status,
|
|
58
|
-
component:
|
|
58
|
+
component: SelectAsync,
|
|
59
59
|
componentProps: {
|
|
60
60
|
label: 'Status',
|
|
61
61
|
options: [
|
package/docs/llms/10-arrays.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
```typescript
|
|
9
9
|
import { createModel, createForm, type FormModel } from '@reformer/core';
|
|
10
|
-
import { Input } 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[] };
|
|
@@ -18,7 +18,7 @@ const model = createModel<MyForm>({ items: [] });
|
|
|
18
18
|
const itemSchema = (item: FormModel<Item>) => ({
|
|
19
19
|
id: { value: item.$.id, component: Input },
|
|
20
20
|
name: { value: item.$.name, component: Input },
|
|
21
|
-
price: { value: item.$.price, component:
|
|
21
|
+
price: { value: item.$.price, component: InputNumber },
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
const schema = {
|
|
@@ -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`.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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:
|
|
52
|
+
loanType: { value: model.$.loanType, component: SelectAsync, componentProps: { /* ... */ } },
|
|
51
53
|
personalData: personalDataNodes(model.$.personalData),
|
|
52
54
|
properties: { array: model.properties, item: propertyItem },
|
|
53
55
|
});
|
|
@@ -94,9 +96,9 @@ forms/
|
|
|
94
96
|
│
|
|
95
97
|
├── schema/ # The form definition
|
|
96
98
|
│ ├── model.ts # createModel factory + initial values + array-element factories
|
|
97
|
-
│ ├── schema.ts
|
|
98
|
-
│ ├── validation.ts
|
|
99
|
-
│ ├── behavior.ts
|
|
99
|
+
│ ├── form.schema.ts # schema builder (model) => tree ({ value: model.$.x, component })
|
|
100
|
+
│ ├── form.validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
|
|
101
|
+
│ ├── form.behavior.ts # defineFormBehavior(...)
|
|
100
102
|
│ └── create-form.ts # Assembly: createCoreForm({ model, schema, behavior, validation })
|
|
101
103
|
│
|
|
102
104
|
└── components/
|
|
@@ -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 `schema.ts` / `validation.ts` / `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,7 +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 `
|
|
127
|
-
> renderer-json `
|
|
128
|
-
>
|
|
129
|
-
> **form-directory-layout** guide (`@reformer/mcp`)
|
|
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
|
|
131
|
+
> choice, and the full reuse map live in the **form-directory-layout** guide (`@reformer/mcp`) —
|
|
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} />`. Свои
|
|
5
|
+
> строкой `<FormField control={form.x} />`. Свои обёртки поля пиши ТОЛЬКО если
|
|
6
6
|
> ui-kit не подходит (другая design system, особый low-level input).
|
|
7
7
|
>
|
|
8
8
|
> Канонический schema-driven подход:
|
|
9
9
|
>
|
|
10
|
-
> - **компонент** объявляется в схеме как `component: Input` (или `
|
|
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, Input,
|
|
23
|
+
import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
|
|
23
24
|
|
|
24
25
|
type RegistrationForm = {
|
|
25
26
|
email: string;
|
|
@@ -38,7 +39,7 @@ function RegistrationPage() {
|
|
|
38
39
|
},
|
|
39
40
|
country: {
|
|
40
41
|
value: model.$.country,
|
|
41
|
-
component:
|
|
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:
|
|
53
|
+
component: CheckboxWithLabel,
|
|
53
54
|
componentProps: { label: 'I agree to terms' },
|
|
54
55
|
},
|
|
55
56
|
};
|
|
@@ -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
|
-
> контрола.
|
|
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
|
|
@@ -31,7 +31,7 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
|
|
|
31
31
|
|
|
32
32
|
```typescript
|
|
33
33
|
import { createModel, createForm } from '@reformer/core';
|
|
34
|
-
import { Input,
|
|
34
|
+
import { Input, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
|
|
35
35
|
|
|
36
36
|
type ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ const schema = {
|
|
|
40
40
|
username: { value: model.$.username, component: Input, componentProps: { label: 'Username' } },
|
|
41
41
|
language: {
|
|
42
42
|
value: model.$.language,
|
|
43
|
-
component:
|
|
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:
|
|
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:
|
|
62
|
+
loanAmount: { value: model.$.loanAmount, component: InputNumber },
|
|
63
63
|
// вложенная группа — builder, принимающий ModelSignals<Sub>
|
|
64
64
|
personalData: personalDataNodes(model.$.personalData),
|
|
65
65
|
// массив — { array, item }
|