@reformer/core 11.0.0-beta.7 → 11.0.0-beta.8
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 +7 -3
- package/dist/behaviors.js +211 -174
- package/dist/create-model-BBRHhHwK.js +30 -0
- package/dist/form/behaviors/collections.d.ts +14 -1
- package/dist/form/behaviors/context.d.ts +18 -2
- package/dist/form/behaviors/index.d.ts +3 -1
- package/dist/form/behaviors/internals.d.ts +0 -1
- package/dist/form/behaviors/schema-operators.d.ts +76 -0
- package/dist/form/behaviors/types.d.ts +15 -3
- package/dist/form/create-core-form.d.ts +5 -3
- package/dist/form/create-form.d.ts +28 -53
- package/dist/form/form-bundle.d.ts +136 -0
- package/dist/form/index.d.ts +6 -1
- package/dist/form/nodes/field-node.d.ts +2 -2
- package/dist/form/nodes/group-node.d.ts +1 -1
- package/dist/form/nodes/model-array-node.d.ts +10 -5
- package/dist/form/schema-controller.d.ts +137 -0
- package/dist/form/schema-subtree.d.ts +28 -0
- package/dist/form/signal-node-registry.d.ts +22 -16
- package/dist/form/types/deep-schema.d.ts +2 -2
- package/dist/form/types/form-proxy.d.ts +1 -1
- package/dist/form/types/schema-node.d.ts +37 -12
- package/dist/form/validation/context.d.ts +6 -1
- package/dist/form/validation/index.d.ts +1 -1
- package/dist/form/validation/operators.d.ts +70 -8
- package/dist/form/validation/types.d.ts +2 -1
- package/dist/index.js +345 -254
- package/dist/model/index.d.ts +5 -0
- package/dist/model/model-nodes.d.ts +67 -4
- package/dist/model/model-signals-proxy.d.ts +8 -1
- package/dist/model/model-value-proxy.d.ts +76 -0
- package/dist/model/types.d.ts +7 -4
- package/dist/model-value-proxy-caTmAiUd.js +419 -0
- package/dist/model.js +19 -17
- package/dist/node-BeV_lV5i.js +100 -0
- package/dist/platforms/react/form-bundle-context.d.ts +27 -0
- package/dist/platforms/react/index.d.ts +1 -0
- package/dist/strategy-DgzELWno.js +204 -0
- package/dist/validation.js +7 -6
- package/docs/llms/03-api-signatures.md +4 -4
- package/docs/llms/09-formschema.md +5 -5
- package/docs/llms/10-arrays.md +23 -4
- package/docs/llms/16-ui-components.md +2 -2
- package/docs/llms/18-conditional-fields.md +1 -1
- package/docs/llms/21-array-operations.md +26 -0
- package/docs/llms/28-submit-and-reset.md +3 -3
- package/docs/llms/29-async-preload.md +4 -4
- package/docs/llms/33-validation-strategy.md +2 -2
- package/docs/llms/35-unified-contract.md +246 -0
- package/llms-index.json +1 -1
- package/llms.txt +1148 -122
- package/package.json +1 -1
- package/dist/create-model-B8U-COv-.js +0 -103
- package/dist/model-signals-proxy-2vqS243J.js +0 -250
- package/dist/node-DGEFMTQu.js +0 -18
- package/dist/strategy-BXS_gBfz.js +0 -170
|
@@ -72,7 +72,7 @@ function createFormValidation<T>(
|
|
|
72
72
|
`markAsTouched` не нужен.
|
|
73
73
|
|
|
74
74
|
```tsx
|
|
75
|
-
import { createModel,
|
|
75
|
+
import { createModel, createFormFromModel, useFormValidation } from '@reformer/core';
|
|
76
76
|
import { defineValidationSchema, validate } from '@reformer/core/validation';
|
|
77
77
|
import { required, email, minLength } from '@reformer/core/validators';
|
|
78
78
|
|
|
@@ -87,7 +87,7 @@ const registrationValidation = defineValidationSchema<RegistrationData>(({ model
|
|
|
87
87
|
|
|
88
88
|
function RegistrationForm() {
|
|
89
89
|
const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);
|
|
90
|
-
const form = useMemo(() =>
|
|
90
|
+
const form = useMemo(() => createFormFromModel({ model }), [model]);
|
|
91
91
|
|
|
92
92
|
const { submit, isValidating } = useFormValidation({
|
|
93
93
|
model,
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Единый контракт формы — одна схема, одна сборка, одно поведение
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Форма описывается одинаково для всех способов реализации — разметка в JSX, рендерер по TS-схеме,
|
|
6
|
+
рендерер по JSON. Схема одна (дерево узлов), привязка одна (ручка `model.$.…`), сборка одна
|
|
7
|
+
(`createForm`), поведение одно (`defineFormBehavior`). Способы отличаются только видом схемы
|
|
8
|
+
(билдер или документ) и тем, кто рисует результат.
|
|
9
|
+
|
|
10
|
+
Прежние записи — ключи `value` / `array`, оператор `each`, `createCoreForm` — пока принимаются.
|
|
11
|
+
Новый код пишется так, как описано здесь.
|
|
12
|
+
|
|
13
|
+
| Слой | поле | массив под-форм | подформа |
|
|
14
|
+
| --------- | --------------------------------- | ----------------------------------------- | ------------------------------------- |
|
|
15
|
+
| схема | `{ model: model.$.x, component }` | `{ model: model.$.items, item }` | `{ model: model.$.group, part }` |
|
|
16
|
+
| валидация | `validate(model.$.x, rules)` | `applyEach(model.$.items, itemRules)` | `apply(model.$.group, groupRules)` |
|
|
17
|
+
| поведение | `compute(model.$.x, …)` | `applyEach(model.$.items, itemBehavior)` | `apply(model.$.group, groupBehavior)` |
|
|
18
|
+
|
|
19
|
+
Правило привязки: `model.$.…` — привязка (схема, правило, оператор). Без `$` — чтение значения
|
|
20
|
+
(`model.loanType === 'mortgage'`), мутация (`model.items.push()`) и под-модель как область.
|
|
21
|
+
|
|
22
|
+
## Модель — начальные значения и шаблоны элементов
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { arrayOf, createModel } from '@reformer/core';
|
|
26
|
+
|
|
27
|
+
const blankPhone = (): Phone => ({ number: '' });
|
|
28
|
+
const blankCoBorrower = (): CoBorrower => ({ name: '', phones: arrayOf(blankPhone) });
|
|
29
|
+
|
|
30
|
+
export const createCreditModel = () =>
|
|
31
|
+
createModel<CreditForm>({
|
|
32
|
+
loanType: 'consumer',
|
|
33
|
+
registrationAddress: { city: '', street: '' },
|
|
34
|
+
coBorrowers: arrayOf(blankCoBorrower), // пустой массив + шаблон нового элемента
|
|
35
|
+
tags: [], // массив как одно значение поля — шаблон не нужен
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`arrayOf(blank, items?)` возвращает обычный массив `U[]` и запоминает фабрику нового элемента.
|
|
40
|
+
После этого `push()` и `insertAt(i)` без значения кладут элемент по шаблону — в модели, в ноде
|
|
41
|
+
массива формы и в кнопке «Добавить» секции массива.
|
|
42
|
+
|
|
43
|
+
- Шаблон переживает `set` / `patch` / `reset` модели.
|
|
44
|
+
- Вложенный массив объявляется внутри шаблона элемента; элементы, пришедшие обычными данными
|
|
45
|
+
(загрузка с сервера), получают его шаблон из шаблона родителя.
|
|
46
|
+
- У массива без шаблона `push()` без значения бросает ошибку с подсказкой.
|
|
47
|
+
- Шаблон привязан к самому массиву: до `createModel` его нельзя копировать. `structuredClone`,
|
|
48
|
+
`JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона. Начальные значения с
|
|
49
|
+
`arrayOf` держат в фабрике, а не в константе, которую клонируют.
|
|
50
|
+
|
|
51
|
+
## Схема — одно дерево
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import type { FormModel } from '@reformer/core';
|
|
55
|
+
|
|
56
|
+
// подформа: объявлена один раз, получает под-модель
|
|
57
|
+
const address = (model: FormModel<Address>) => ({
|
|
58
|
+
component: Box,
|
|
59
|
+
children: [
|
|
60
|
+
{ model: model.$.city, component: Input, componentProps: { label: 'Город' } },
|
|
61
|
+
{ model: model.$.street, component: Input, componentProps: { label: 'Улица' } },
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// строка массива — такая же функция от под-модели
|
|
66
|
+
const coBorrower = (model: FormModel<CoBorrower>) => ({
|
|
67
|
+
selector: 'co-borrower',
|
|
68
|
+
component: Box,
|
|
69
|
+
children: [{ model: model.$.name, component: Input, componentProps: { label: 'ФИО' } }],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const creditSchema = (model: FormModel<CreditForm>) => ({
|
|
73
|
+
component: Box,
|
|
74
|
+
children: [
|
|
75
|
+
{ model: model.$.loanType, component: SelectAsync, componentProps: { label: 'Тип' } }, // поле
|
|
76
|
+
{ model: model.$.tags, component: SelectMulti }, // массив как одно значение
|
|
77
|
+
{
|
|
78
|
+
selector: 'registration',
|
|
79
|
+
component: Section,
|
|
80
|
+
componentProps: { title: 'Адрес регистрации' },
|
|
81
|
+
children: [{ model: model.$.registrationAddress, part: address }], // подформа
|
|
82
|
+
},
|
|
83
|
+
{ model: model.$.coBorrowers, component: FormArray, item: coBorrower }, // массив под-форм
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Чем узел является, решают ручка и соседние ключи:
|
|
89
|
+
|
|
90
|
+
| Узел | Как отличить |
|
|
91
|
+
| --------------- | --------------------------------------------- |
|
|
92
|
+
| поле | `model` — лист или массив; нет `item`, `part` |
|
|
93
|
+
| массив под-форм | `model` — массив, `item` — функция |
|
|
94
|
+
| подформа | `model` — группа, `part` — функция |
|
|
95
|
+
| контейнер | есть `children`, нет `model` |
|
|
96
|
+
|
|
97
|
+
- Узел узнаётся по ЗНАЧЕНИЮ: в записи «имя поля → узел» поле данных может называться `model`,
|
|
98
|
+
`item`, `part` или `value` — под таким ключом лежит обычный вложенный узел.
|
|
99
|
+
- Поля подформы принадлежат той же форме: `form.registrationAddress.city`.
|
|
100
|
+
- Билдер `item` / `part` вызывается один раз на под-модель; сборка и рендерер получают одно и то
|
|
101
|
+
же поддерево (`schemaSubtree(builder, subModel)`).
|
|
102
|
+
- `initialValue` узла-массива — запасной шаблон нового элемента для форм, чья модель создаётся из
|
|
103
|
+
данных без кода. Шаблон модели (`arrayOf`) главнее.
|
|
104
|
+
|
|
105
|
+
## Валидация — подформа и массив подключаются привязкой
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import {
|
|
109
|
+
apply,
|
|
110
|
+
applyEach,
|
|
111
|
+
cross,
|
|
112
|
+
defineValidationSchema,
|
|
113
|
+
validate,
|
|
114
|
+
validateWhen,
|
|
115
|
+
} from '@reformer/core/validation';
|
|
116
|
+
|
|
117
|
+
const addressRules = defineValidationSchema<Address>(({ model }) => {
|
|
118
|
+
validate(model.$.city, [required()]);
|
|
119
|
+
cross<Address>(model.$.street, (address) =>
|
|
120
|
+
address.city !== '' && address.street === '' ? streetRequired : null
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const coBorrowerRules = defineValidationSchema<CoBorrower>(({ model }) => {
|
|
125
|
+
validate(model.$.name, [required()]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const contactsRules = defineValidationSchema<CreditForm>(({ model }) => {
|
|
129
|
+
apply(model.$.registrationAddress, addressRules);
|
|
130
|
+
validateWhen(
|
|
131
|
+
() => !model.sameAsRegistration,
|
|
132
|
+
() => apply(model.$.residenceAddress, addressRules)
|
|
133
|
+
);
|
|
134
|
+
applyEach(model.$.coBorrowers, coBorrowerRules);
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- У схемы, подключённой через `apply(ручка, схема)` / `applyEach`, своя область: `model` — под-модель,
|
|
139
|
+
`cross` получает её снапшот. Захватывать снапшот в замыкание больше не нужно.
|
|
140
|
+
- Одна и та же схема подключается и к группе, и к элементам массива.
|
|
141
|
+
- `apply(model.$.a, rules)` принимает и массив ручек: `apply([model.$.a, model.$.b], rules)`.
|
|
142
|
+
- `apply(schemaA, schemaB)` — композиция схем над той же моделью — остаётся.
|
|
143
|
+
- Правило получает `(value, scope, root)`: модель области и корень прогона.
|
|
144
|
+
- `each(model.items, (item) => …)` — прежняя запись `applyEach`: принимает фасад и ручку, области
|
|
145
|
+
не создаёт.
|
|
146
|
+
|
|
147
|
+
## Поведение — модель, форма и схема
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import {
|
|
151
|
+
apply,
|
|
152
|
+
applyEach,
|
|
153
|
+
defineFormBehavior,
|
|
154
|
+
enableWhen,
|
|
155
|
+
hideWhen,
|
|
156
|
+
onComponentEvent,
|
|
157
|
+
onMount,
|
|
158
|
+
} from '@reformer/core/behaviors';
|
|
159
|
+
|
|
160
|
+
const addressBehavior = defineFormBehavior<Address>(({ model, schema }) => {
|
|
161
|
+
hideWhen(schema.node('street'), () => model.city === ''); // узел ищется внутри части
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, schema }) => {
|
|
165
|
+
const isMortgage = () => model.loanType === 'mortgage';
|
|
166
|
+
|
|
167
|
+
enableWhen(model.$.propertyValue, isMortgage, { resetOnDisable: true });
|
|
168
|
+
hideWhen(schema.node('mortgage'), () => !isMortgage());
|
|
169
|
+
onComponentEvent(schema.node('wizard'), 'onSubmit', () => submitApplication(model.get()));
|
|
170
|
+
onMount(schema.node('data-boundary'), () => void loadApplication(model));
|
|
171
|
+
|
|
172
|
+
apply([model.$.registrationAddress, model.$.residenceAddress], addressBehavior);
|
|
173
|
+
applyEach(model.$.coBorrowers, coBorrowerBehavior);
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Операторы узлов схемы:
|
|
178
|
+
|
|
179
|
+
| Оператор | Что записывает |
|
|
180
|
+
| ----------------------------------------- | ---------------------------------------------------------- |
|
|
181
|
+
| `hideWhen(node, condition)` | узел скрыт, пока условие истинно |
|
|
182
|
+
| `onComponentEvent(node, event, handler)` | обработчик пропа-события компонента |
|
|
183
|
+
| `onMount(node, fn)` / `onUnmount(node, fn)` | хуки монтирования узла |
|
|
184
|
+
| `onInit(node, fn)` | `fn` вызывается сразу, до первого рендера |
|
|
185
|
+
| `renderEffect(schema, fn)` | реактивный эффект, пока схема области смонтирована |
|
|
186
|
+
| `schema.node(sel).setHidden / patchProps` | принудительная видимость и пропсы узла |
|
|
187
|
+
| `schema.node(sel).getRef()` | ref на компонент узла — объект `{ current }` |
|
|
188
|
+
|
|
189
|
+
- **Области изолированы.** `schema.node(selector)` ищет узел только в своей области: корневое
|
|
190
|
+
поведение — в корневом дереве, поведение подформы и строки массива — в своём поддереве. До узла
|
|
191
|
+
строки или части корень дотягивается через `applyEach` / `apply`. Два монтирования одной части
|
|
192
|
+
не конфликтуют.
|
|
193
|
+
- **Исключение — ref по пути модели**: `schema.node('phones.0.number').getRef()` ищется от корня.
|
|
194
|
+
- **Операторы узлов только записывают правило — исполняет его рендерер.** При разметке в JSX они
|
|
195
|
+
ничего не делают: видимость, отправка и загрузка данных остаются в JSX.
|
|
196
|
+
- Правило на селектор, которого нет в дереве, ничего не делает; в dev — предупреждение.
|
|
197
|
+
|
|
198
|
+
## Сборка — `createForm`
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { createForm, useFormBundle } from '@reformer/core';
|
|
202
|
+
|
|
203
|
+
const credit = useFormBundle(() =>
|
|
204
|
+
createForm<CreditForm>({
|
|
205
|
+
model: createCreditModel(), // либо initial: { … }
|
|
206
|
+
schema: creditSchema, // (model) => узел; для JSON — документ + registry
|
|
207
|
+
behavior: creditBehavior,
|
|
208
|
+
validation: { steps: { loan: loanRules, contacts: contactsRules }, extras: crossRules },
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
// credit = { model, form, validation, render }
|
|
212
|
+
|
|
213
|
+
<FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />; // рендерер
|
|
214
|
+
<FormField control={credit.form.loanType} />; // разметка в JSX — та же сборка
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- Порядок: модель → `seed` → дерево схемы → ноды формы → поведение → валидация → `setup`. Дерево
|
|
218
|
+
строится один раз.
|
|
219
|
+
- `validation` в бандле типизирована по конфигу: правила переданы — поле есть всегда.
|
|
220
|
+
- `render` — `{ tree, controller, node(selector) }`: готовое дерево и схема-контроллер сборки.
|
|
221
|
+
- JSON: `createForm({ model, schema: document, registry })` — дерево собирает
|
|
222
|
+
`registry.resolveSchema`. Ядро документ не разбирает.
|
|
223
|
+
- Внутри рендерера бандл доступен компонентам через `useFormBundleContext()` — так визард берёт
|
|
224
|
+
форму и валидацию сам.
|
|
225
|
+
|
|
226
|
+
### Низкоуровневые фабрики
|
|
227
|
+
|
|
228
|
+
| Задача | Фабрика |
|
|
229
|
+
| ---------------------------------------------- | ---------------------------------------- |
|
|
230
|
+
| форма из модели и готового дерева, без бандла | `createFormFromModel({ model, schema })` |
|
|
231
|
+
| форма без модели — плоская схема полей | `createLegacyForm(schema)` |
|
|
232
|
+
|
|
233
|
+
`createForm` с готовым деревом вместо билдера или без модели бросает ошибку с отсылкой к нужной
|
|
234
|
+
фабрике.
|
|
235
|
+
|
|
236
|
+
## Частые ошибки
|
|
237
|
+
|
|
238
|
+
| Ошибка | Как правильно |
|
|
239
|
+
| -------------------------------------------------- | ------------------------------------------------------- |
|
|
240
|
+
| `model: model.address` (под-модель у поля) | `model: model.$.address.city` — привязка всегда с `$` |
|
|
241
|
+
| `{ model: model.$.address, component }` без `part` | группа полем не бывает — нужен `part` |
|
|
242
|
+
| `createForm({ model, schema: дерево })` | `schema` — билдер `(model) => узел` |
|
|
243
|
+
| `hideWhen(schema.node('row-узел'), …)` из корня | внутри `applyEach(model.$.items, itemBehavior)` |
|
|
244
|
+
| `push({})` для новой строки | `arrayOf(blank)` в модели и `push()` без значения |
|
|
245
|
+
| `createModel(structuredClone(INITIAL))` с `arrayOf` | фабрика начальных значений — клон теряет шаблон массива |
|
|
246
|
+
| `cross` в подформе читает корень | `apply(model.$.group, rules)` — область подформы |
|