@reformer/core 9.1.1 → 10.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -28
- package/dist/behaviors-node-DUrqAqp1.js +18 -0
- package/dist/behaviors.js +1 -1
- package/dist/form/create-form.d.ts +13 -17
- package/dist/form/index.d.ts +4 -6
- package/dist/form/nodes/group-node.d.ts +2 -15
- package/dist/form/types/schema-node.d.ts +13 -9
- package/dist/form/types/validation-schema.d.ts +7 -29
- package/dist/form/validation/index.d.ts +7 -0
- package/dist/form/validation/validators/email.d.ts +1 -1
- package/dist/form/validation/validators/file-type.d.ts +31 -0
- package/dist/form/validation/validators/file-utils.d.ts +51 -0
- package/dist/form/validation/validators/future-date.d.ts +1 -1
- package/dist/form/validation/validators/index.d.ts +7 -0
- package/dist/form/validation/validators/is-date.d.ts +1 -1
- package/dist/form/validation/validators/max-file-size.d.ts +28 -0
- package/dist/form/validation/validators/max-files.d.ts +26 -0
- package/dist/form/validation/validators/max-total-file-size.d.ts +26 -0
- package/dist/form/validation/validators/min-file-size.d.ts +28 -0
- package/dist/form/validation/validators/min-files.d.ts +26 -0
- package/dist/form/validation/validators/past-date.d.ts +1 -1
- package/dist/form/validation/validators/pattern.d.ts +1 -1
- package/dist/form/validation/validators/phone.d.ts +1 -1
- package/dist/form/validation/validators/url.d.ts +1 -1
- package/dist/form/validation-schema.d.ts +93 -0
- package/dist/index.js +352 -454
- package/dist/signal-node-registry-tiwel-TV.js +11 -0
- package/dist/state/behaviors-value.d.ts +3 -3
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +115 -0
- package/dist/validators.js +196 -65
- package/llms.txt +1203 -582
- package/package.json +5 -1
- package/dist/behaviors-node-BMOI2JJF.js +0 -26
- package/dist/form/validate-model-core.d.ts +0 -72
- package/dist/form/validate-model.d.ts +0 -27
package/llms.txt
CHANGED
|
@@ -48,11 +48,12 @@
|
|
|
48
48
|
| What | Where |
|
|
49
49
|
| ------------------------------------------------------------------------------------------------------ | --------------------------- |
|
|
50
50
|
| `createModel`, `createForm` | `@reformer/core` |
|
|
51
|
-
| `validateModel`, `
|
|
51
|
+
| `validateModel`, `defineValidationSchema`, `validate`, `validateAsync`, `validateWhen`, `cross`, `each`, `apply` | `@reformer/core/validation` |
|
|
52
|
+
| `Rule`, `AsyncRule`, `ValidationSchema` (типы) | `@reformer/core/validation` |
|
|
52
53
|
| `useFormControl`, `useFormControlValue`, `useArrayLength` | `@reformer/core` |
|
|
53
54
|
| `FormModel`, `FormProxy`, `FieldNode`, `GroupNode`, `ArrayNode`, `ModelArrayNode` | `@reformer/core` |
|
|
54
55
|
| `ModelSignals`, `ModelArray`, `ModelValue`, `ModelObject`, `PathAwareSignal` | `@reformer/core` |
|
|
55
|
-
| `
|
|
56
|
+
| `ValidationError`, `FieldConfig`, `FormSchema`, `FieldControlState` | `@reformer/core` |
|
|
56
57
|
| `computeFrom`, `copyFrom`, `watchField`, `enableWhen`, `disableWhen` | `@reformer/core` (примитивы) |
|
|
57
58
|
| `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen` | `@reformer/core` (примитивы) |
|
|
58
59
|
| `required`, `min`, `max`, `minLength`, `maxLength`, `email`, `pattern`, `url`, `phone` | `@reformer/core/validators` |
|
|
@@ -68,9 +69,12 @@
|
|
|
68
69
|
> (`defineFormBehavior` + операторы) экспортируется из `@reformer/core/behaviors`, регистрирует cleanup
|
|
69
70
|
> сам и передаётся в `createForm({ behavior })`. См. `20-compute-vs-watch.md`.
|
|
70
71
|
|
|
71
|
-
>
|
|
72
|
-
> `(
|
|
73
|
-
>
|
|
72
|
+
> **Валидация — отдельный слой.** Layout-схема `createForm` НЕ несёт валидаторов. Правила живут в
|
|
73
|
+
> `defineValidationSchema<T>(({ model }) => { validate(model.$.x, [required(), min(50000)]); ... })`
|
|
74
|
+
> из `@reformer/core/validation`; фабрики `required()`, `min(50000)`, `email()` возвращают
|
|
75
|
+
> `Rule<T> = (value) => ValidationError | null` и передаются массивом в `validate(sig, [...])`.
|
|
76
|
+
> Запуск — только внешним раннером `await validateModel(model, schema)` (`Promise<boolean>`, ошибки
|
|
77
|
+
> сам роутит в ноды; `form.validate()`/`form.submit()` schema-валидацию НЕ гоняют).
|
|
74
78
|
|
|
75
79
|
### Type Values
|
|
76
80
|
|
|
@@ -110,11 +114,13 @@ const { value, errors, disabled } = useFormControl(control.loanType);
|
|
|
110
114
|
|
|
111
115
|
Архитектура M1: сначала создаётся **модель данных** (`createModel`), затем **форма**
|
|
112
116
|
(`createForm({ model, schema })`), где схема привязывает поля к сигналам модели
|
|
113
|
-
(`model.$.field`).
|
|
114
|
-
|
|
117
|
+
(`model.$.field`). Layout-схема НЕ несёт валидаторов — валидация живёт в отдельной
|
|
118
|
+
схеме `defineValidationSchema` из `@reformer/core/validation` и запускается внешним
|
|
119
|
+
раннером `validateModel(model, schema)`.
|
|
115
120
|
|
|
116
121
|
```typescript
|
|
117
|
-
import { createModel, createForm,
|
|
122
|
+
import { createModel, createForm, type FormProxy } from '@reformer/core';
|
|
123
|
+
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
118
124
|
import { required, email } from '@reformer/core/validators';
|
|
119
125
|
import { FormField, Input, Button } from '@reformer/ui-kit';
|
|
120
126
|
|
|
@@ -127,31 +133,37 @@ type ContactForm = {
|
|
|
127
133
|
// 2. Model (источник истины значений)
|
|
128
134
|
const model = createModel<ContactForm>({ name: '', email: '' });
|
|
129
135
|
|
|
130
|
-
// 3.
|
|
136
|
+
// 3. Layout-schema: привязка поля к сигналу (model.$.field) + component/componentProps.
|
|
137
|
+
// БЕЗ validators — валидация в отдельной схеме (шаг 4).
|
|
131
138
|
const schema = {
|
|
132
139
|
name: {
|
|
133
140
|
value: model.$.name,
|
|
134
141
|
component: Input,
|
|
135
142
|
componentProps: { label: 'Name', placeholder: 'Your name' },
|
|
136
|
-
validators: [required({ message: 'Name is required' })],
|
|
137
143
|
},
|
|
138
144
|
email: {
|
|
139
145
|
value: model.$.email,
|
|
140
146
|
component: Input,
|
|
141
147
|
componentProps: { label: 'Email', type: 'email' },
|
|
142
|
-
validators: [required({ message: 'Email is required' }), email({ message: 'Invalid email' })],
|
|
143
148
|
},
|
|
144
149
|
};
|
|
145
150
|
|
|
146
|
-
// 4.
|
|
151
|
+
// 4. Validation-schema — отдельный слой (@reformer/core/validation)
|
|
152
|
+
const contactValidation = defineValidationSchema<ContactForm>(({ model }) => {
|
|
153
|
+
validate(model.$.name, [required({ message: 'Name is required' })]);
|
|
154
|
+
validate(model.$.email, [required({ message: 'Email is required' }), email({ message: 'Invalid email' })]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 5. Form — ноды поверх сигналов модели
|
|
147
158
|
const form = createForm<ContactForm>({ model, schema });
|
|
148
159
|
|
|
149
|
-
//
|
|
160
|
+
// 6. Use in React component — thin JSX, FormField does ALL heavy lifting
|
|
150
161
|
function ContactFormComponent() {
|
|
151
162
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
152
163
|
e.preventDefault();
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
// form.submit()/validate() НЕ гоняют schema-валидацию — только внешний validateModel
|
|
165
|
+
const ok = await validateModel(model, contactValidation); // Promise<boolean>, ошибки сам роутит в ноды
|
|
166
|
+
if (ok) {
|
|
155
167
|
console.log('Form submitted:', model.get());
|
|
156
168
|
}
|
|
157
169
|
};
|
|
@@ -165,7 +177,7 @@ function ContactFormComponent() {
|
|
|
165
177
|
);
|
|
166
178
|
}
|
|
167
179
|
|
|
168
|
-
//
|
|
180
|
+
// 7. Pass form to child components via props (NOT context!)
|
|
169
181
|
type FormStepProps = {
|
|
170
182
|
form: FormProxy<ContactForm>;
|
|
171
183
|
};
|
|
@@ -254,16 +266,19 @@ createForm<T>({ model, schema, behavior? }): FormProxy<T>
|
|
|
254
266
|
// form.<field>.enable() / .disable() / .reset() / .markAsTouched() / .setErrors([...])
|
|
255
267
|
// form.<field>.updateComponentProps({ ... })
|
|
256
268
|
|
|
257
|
-
// Валидация данных
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
269
|
+
// Валидация данных — ОТДЕЛЬНЫЙ контракт `@reformer/core/validation` (НЕ дерево createForm: то несёт
|
|
270
|
+
// только layout, без validators). Схема — функция над моделью; внешний раннер разносит ошибки по нодам
|
|
271
|
+
// формы (setErrors), warning не блокирует submit, устаревший прогон отменяется.
|
|
272
|
+
validateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean> // из @reformer/core/validation
|
|
273
|
+
// schema = defineValidationSchema<T>(({ model }) => { validate(...); ... })
|
|
274
|
+
// ⚠️ form.submit() / form.validate() БОЛЬШЕ НЕ прогоняют schema-валидацию — гоняйте validateModel(model, schema) снаружи.
|
|
261
275
|
```
|
|
262
276
|
|
|
263
277
|
### Validators
|
|
264
278
|
|
|
265
|
-
Валидаторы — **чистые
|
|
266
|
-
|
|
279
|
+
Валидаторы — **чистые фабрики** из `@reformer/core/validators`: возвращают правило `Rule<T>` =
|
|
280
|
+
`(value) => ValidationError | null` (принимают nullable value). Передаются оператору
|
|
281
|
+
`validate(sig, [required(), min(50000)])` внутри схемы валидации, а **не** в layout-дерево `createForm`.
|
|
267
282
|
|
|
268
283
|
```typescript
|
|
269
284
|
required(options?: { message?: string })
|
|
@@ -291,89 +306,164 @@ minAge(years: number, options?: { message?: string })
|
|
|
291
306
|
maxAge(years: number, options?: { message?: string })
|
|
292
307
|
```
|
|
293
308
|
|
|
294
|
-
|
|
309
|
+
Использование: layout-дерево `createForm` привязывает поля к сигналам, правила живут отдельной
|
|
310
|
+
`ValidationSchema` и гоняются раннером `validateModel` по требованию.
|
|
295
311
|
|
|
296
312
|
```typescript
|
|
297
313
|
import { createModel, createForm } from '@reformer/core';
|
|
314
|
+
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
298
315
|
import { required, min, max, email } from '@reformer/core/validators';
|
|
299
316
|
|
|
300
|
-
|
|
301
|
-
|
|
317
|
+
type Loan = { email: string; age: number; amount: number };
|
|
318
|
+
const model = createModel<Loan>({ email: '', age: 0, amount: 0 });
|
|
319
|
+
|
|
320
|
+
// layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
|
|
321
|
+
const form = createForm({
|
|
322
|
+
model,
|
|
323
|
+
schema: {
|
|
324
|
+
email: { value: model.$.email, component: Input },
|
|
325
|
+
age: { value: model.$.age, component: Input },
|
|
326
|
+
amount: { value: model.$.amount, component: Input },
|
|
327
|
+
},
|
|
302
328
|
});
|
|
303
329
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
330
|
+
// правила — в ОТДЕЛЬНОЙ схеме валидации (не в layout)
|
|
331
|
+
const loanValidation = defineValidationSchema<Loan>(({ model }) => {
|
|
332
|
+
validate(model.$.email, [required(), email()]);
|
|
333
|
+
validate(model.$.age, [required(), min(18)]);
|
|
334
|
+
validate(model.$.amount, [min(0), max(1000)]);
|
|
335
|
+
});
|
|
309
336
|
|
|
310
|
-
const
|
|
337
|
+
const ok = await validateModel(model, loanValidation); // Promise<boolean>; ошибки сами доедут до form.<field>.errors
|
|
311
338
|
```
|
|
312
339
|
|
|
313
340
|
### Custom & cross-field validators
|
|
314
341
|
|
|
315
|
-
|
|
316
|
-
(
|
|
317
|
-
|
|
318
|
-
|
|
342
|
+
Контракт валидации — `@reformer/core/validation`. Схема (`ValidationSchema<T>`) — обычная функция над
|
|
343
|
+
(под)моделью; внутри вызываются свободные операторы, которые сами пишут ошибки в ноды текущего прогона
|
|
344
|
+
(`getNodeForSignal(sig).setErrors(...)` — автор коллектора не видит).
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
type Rule<T> = (value: T, scope: never, root: never) => ValidationError | null; // scope/root = never (см. ниже)
|
|
348
|
+
type AsyncRule<T> = (value: T, ctx: { signal: AbortSignal }) => Promise<ValidationError | null>;
|
|
349
|
+
type ValidationSchema<T> = (ctx: { model: FormModel<T> }) => void;
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Операторы (ambient — валидны только внутри прогона `validateModel`):
|
|
353
|
+
|
|
354
|
+
| Оператор | Назначение |
|
|
355
|
+
|---|---|
|
|
356
|
+
| `validate(sig, rules: Rule<T>[])` | синхронные правила поля |
|
|
357
|
+
| `validateAsync(sig, rules: AsyncRule<T>[])` | асинхронные правила (раннер дожидается, прокидывает `AbortSignal`) |
|
|
358
|
+
| `validateWhen(cond: () => boolean, cb: () => void)` | условная ветка: правила внутри активны/гасятся по `cond` (не трогает enable — это поведение) |
|
|
359
|
+
| `cross(sig, fn: (form) => err \| null)` | cross-field; `fn` получает снапшот модели текущего scope (`model.get()`) |
|
|
360
|
+
| `each(arr, itemFn: (im: FormModel<U>) => void)` | per-item по элементам массива модели |
|
|
361
|
+
| `apply(...schemas: ValidationSchema<T>[])` | композиция под-схем над той же моделью |
|
|
362
|
+
| `defineValidationSchema<T>(fn): ValidationSchema<T>` | тонкая identity-обёртка (типизация/discoverability) |
|
|
363
|
+
| `validateModel<T>(model, schema): Promise<boolean>` | внешний раннер (warning не блокирует, устаревший прогон отменяется) |
|
|
364
|
+
|
|
365
|
+
> `Rule<T>` намеренно несёт `scope: never, root: never` — так value-only фабрики (`required()` = `(value, model, root)`)
|
|
366
|
+
> и inline `(value) => err` ОБА присваиваются в `Rule<T>[]`, сохраняя проверку типа поля
|
|
367
|
+
> (`validate(model.$.age, [email()])` подсветится). Ментальная модель автора — `(value) => error`.
|
|
368
|
+
|
|
369
|
+
Кастомные правила, cross-field и async — в схеме:
|
|
319
370
|
|
|
320
371
|
```typescript
|
|
321
|
-
import type
|
|
372
|
+
import { type ValidationError } from '@reformer/core';
|
|
373
|
+
import {
|
|
374
|
+
defineValidationSchema, validate, validateAsync, cross,
|
|
375
|
+
type Rule, type AsyncRule,
|
|
376
|
+
} from '@reformer/core/validation';
|
|
377
|
+
import { required } from '@reformer/core/validators';
|
|
322
378
|
|
|
323
|
-
|
|
324
|
-
|
|
379
|
+
type Signup = { password: string; confirm: string; email: string };
|
|
380
|
+
|
|
381
|
+
// Value-only правило (Rule<T>)
|
|
382
|
+
const strongPassword: Rule<string> = (value) =>
|
|
325
383
|
!value || value.length < 8 ? { code: 'too-short', message: 'Минимум 8 символов' } : null;
|
|
326
384
|
|
|
327
|
-
// Cross-field:
|
|
328
|
-
const passwordsMatch
|
|
329
|
-
|
|
385
|
+
// Cross-field: обычная функция над снапшотом модели (model.get()), вешается через cross(sig, fn)
|
|
386
|
+
const passwordsMatch = (f: Signup): ValidationError | null =>
|
|
387
|
+
f.confirm && f.password && f.confirm !== f.password
|
|
330
388
|
? { code: 'mismatch', message: 'Пароли не совпадают' }
|
|
331
389
|
: null;
|
|
332
390
|
|
|
333
|
-
// Async (
|
|
334
|
-
const emailUnique:
|
|
391
|
+
// Async (AsyncRule<T>): получает { signal }; сетевой сбой → null (submit не блокируется)
|
|
392
|
+
const emailUnique: AsyncRule<string> = async (value, { signal }) => {
|
|
335
393
|
if (!value) return null;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
394
|
+
try {
|
|
395
|
+
const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
|
|
396
|
+
return (await res.json()).available ? null : { code: 'taken', message: 'Email уже зарегистрирован' };
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
339
400
|
};
|
|
340
401
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
};
|
|
402
|
+
const signupValidation = defineValidationSchema<Signup>(({ model }) => {
|
|
403
|
+
validate(model.$.password, [required(), strongPassword]);
|
|
404
|
+
validate(model.$.confirm, [required()]);
|
|
405
|
+
cross(model.$.confirm, passwordsMatch); // ошибка вешается на confirm
|
|
406
|
+
validateAsync(model.$.email, [emailUnique]);
|
|
407
|
+
});
|
|
347
408
|
```
|
|
348
409
|
|
|
349
|
-
### Conditional & array validation
|
|
410
|
+
### Conditional & array validation
|
|
350
411
|
|
|
351
|
-
|
|
412
|
+
Условные ветки и массивы — операторы `validateWhen` / `each` / `apply` внутри схемы (не узлы дерева):
|
|
352
413
|
|
|
353
|
-
-
|
|
354
|
-
|
|
355
|
-
-
|
|
356
|
-
|
|
414
|
+
- **условная ветка** `validateWhen(() => cond, () => { validate(...) })` — правила внутри активны только
|
|
415
|
+
при истинном условии; при ложном ошибки полей ветки гасятся (`setErrors([])`). Enable/сброс поля — дело поведения;
|
|
416
|
+
- **массив** `each(model.arr, (im) => { validate(im.$.field, [...]) })` — правила применяются к каждому
|
|
417
|
+
элементу; `im` — под-модель элемента. Для cross-field над элементом захватывайте снапшот в замыкание (`const item = im.get()`);
|
|
418
|
+
- **композиция** `apply(...schemas)` — объединяет под-схемы над той же моделью (напр. полную схему из per-step).
|
|
357
419
|
|
|
358
420
|
```typescript
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
421
|
+
import {
|
|
422
|
+
defineValidationSchema, validate, validateWhen, cross, each, apply, validateModel,
|
|
423
|
+
} from '@reformer/core/validation';
|
|
424
|
+
import { required, min } from '@reformer/core/validators';
|
|
425
|
+
|
|
426
|
+
// Условная валидация — ипотечная ветка активна только при loanType === 'mortgage'
|
|
427
|
+
const step1 = defineValidationSchema<LoanForm>(({ model }) => {
|
|
428
|
+
validate(model.$.loanType, [required()]);
|
|
429
|
+
validateWhen(
|
|
430
|
+
() => model.loanType === 'mortgage',
|
|
431
|
+
() => {
|
|
432
|
+
validate(model.$.propertyValue, [required(), min(1_000_000)]);
|
|
433
|
+
validate(model.$.initialPayment, [required()]);
|
|
369
434
|
},
|
|
370
|
-
|
|
371
|
-
};
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Per-item валидация элементов массива (each над ModelArray)
|
|
439
|
+
const step5 = defineValidationSchema<LoanForm>(({ model }) => {
|
|
440
|
+
each(model.coBorrowers, (im) => {
|
|
441
|
+
const item = im.get(); // снапшот элемента для cross
|
|
442
|
+
validate(im.$.income, [required(), min(10_000)]);
|
|
443
|
+
cross(im.$.income, () =>
|
|
444
|
+
item.income < item.loanShare ? { code: 'tooLow', message: 'Доход ниже доли' } : null);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Полная схема формы = композиция шагов (apply над той же моделью)
|
|
449
|
+
const formValidation = defineValidationSchema<LoanForm>(() => apply(step1, step5));
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Wizard-конфиг (per-step + полная валидация) — поверх того же раннера `validateModel`:
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
const STEP_SCHEMAS = [step1, /* step2, … */ step5] as const;
|
|
456
|
+
|
|
457
|
+
function makeValidationConfig(model: FormModel<LoanForm>) {
|
|
458
|
+
return {
|
|
459
|
+
validateStep: (n: number) => validateModel(model, STEP_SCHEMAS[n - 1]),
|
|
460
|
+
validateAll: () => validateModel(model, formValidation), // formValidation = apply(...STEP_SCHEMAS, extras)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
372
463
|
```
|
|
373
464
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
> `validateFormModel(model, schema)` и форма узлов выше.
|
|
465
|
+
Операторы `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply` экспортируются напрямую из
|
|
466
|
+
`@reformer/core/validation` — это публичный API, а не локальный сахар примера.
|
|
377
467
|
|
|
378
468
|
### Behaviors
|
|
379
469
|
|
|
@@ -424,10 +514,18 @@ DSL-операторы: `compute` (auto-tracking, без явного списк
|
|
|
424
514
|
`apply` (под-схема для группы), `applyEach` (per-item для массива), `exclusiveFlag`, `aggregateInto`.
|
|
425
515
|
См. `20-compute-vs-watch.md`.
|
|
426
516
|
|
|
517
|
+
Поведение **не владеет** валидацией — это отдельный слой. Мост «поведение инициирует валидацию» — через
|
|
518
|
+
`revalidateWhen`, который просто вызывает внешний раннер валидации:
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
revalidateWhen([model.$.dep], () => void validateModel(model, schema));
|
|
522
|
+
```
|
|
523
|
+
|
|
427
524
|
## 4. 3. COMMON PATTERNS
|
|
428
525
|
|
|
429
|
-
Все паттерны — на архитектуре M1: значения в модели (`model.$.field`),
|
|
430
|
-
валидация
|
|
526
|
+
Все паттерны — на архитектуре M1: значения в модели (`model.$.field`), поведение (`defineFormBehavior`)
|
|
527
|
+
на живых сигналах, валидация — отдельной ambient-схемой (`defineValidationSchema`, прогон по требованию
|
|
528
|
+
через `validateModel`). Слои раздельны: layout НЕ несёт validators.
|
|
431
529
|
|
|
432
530
|
### Conditional Fields with Auto-Reset
|
|
433
531
|
|
|
@@ -521,64 +619,104 @@ model.get(); // весь объект-снимок
|
|
|
521
619
|
|
|
522
620
|
### Submit + validation
|
|
523
621
|
|
|
622
|
+
Валидация — отдельная `ValidationSchema<T>` (`defineValidationSchema`), а не часть layout. Раннер
|
|
623
|
+
`validateModel(model, schema)` сам роутит ошибки в ноды формы и возвращает `Promise<boolean>`
|
|
624
|
+
(`false` = есть блокирующая ошибка; `severity: 'warning'` не блокирует).
|
|
625
|
+
|
|
524
626
|
```typescript
|
|
525
|
-
import {
|
|
627
|
+
import { validateModel } from '@reformer/core/validation';
|
|
526
628
|
|
|
527
629
|
async function handleSubmit(e: React.FormEvent) {
|
|
528
630
|
e.preventDefault();
|
|
529
|
-
const
|
|
530
|
-
if (!
|
|
631
|
+
const ok = await validateModel(model, schema); // ошибки сами доезжают до нод формы (UI подсветит)
|
|
632
|
+
if (!ok) return; // false → есть блокирующая ошибка
|
|
531
633
|
await api.send(model.get());
|
|
532
634
|
model.reset(); // к initial-снимку
|
|
533
635
|
}
|
|
534
636
|
```
|
|
535
637
|
|
|
536
|
-
Multi-step: держи отдельные под-схемы на шаг и вызывай `
|
|
537
|
-
|
|
638
|
+
Multi-step: держи отдельные под-схемы на шаг и вызывай `validateModel(model, stepSchema)`. Для wizard'а —
|
|
639
|
+
`makeValidationConfig(model)` → `{ validateStep, validateAll }` (`validateAll` прогоняет полную
|
|
640
|
+
`apply(...STEP_SCHEMAS, extras)`). См. `13-multi-step.md`, `28-submit-and-reset.md`.
|
|
538
641
|
|
|
539
|
-
### Cross-field validation — через `
|
|
642
|
+
### Cross-field validation — через `cross`
|
|
540
643
|
|
|
541
|
-
Cross-field правило —
|
|
542
|
-
поле-носитель
|
|
644
|
+
Cross-field правило — обычная функция над **снапшотом** модели (`fn` получает `model.get()`),
|
|
645
|
+
навешивается оператором `cross(sig, fn)` на поле-носитель ошибки внутри схемы:
|
|
543
646
|
|
|
544
647
|
```typescript
|
|
545
|
-
import
|
|
648
|
+
import { defineValidationSchema, validate, cross } from '@reformer/core/validation';
|
|
649
|
+
import { required, min } from '@reformer/core/validators';
|
|
650
|
+
import type { ValidationError } from '@reformer/core';
|
|
546
651
|
|
|
547
|
-
|
|
548
|
-
|
|
652
|
+
// снапшот формы читается напрямую — без каста, соседние поля доступны как поля объекта
|
|
653
|
+
const initialPaymentVsProperty = (f: MyForm): ValidationError | null =>
|
|
654
|
+
f.initialPayment && f.propertyValue && f.initialPayment > f.propertyValue
|
|
549
655
|
? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }
|
|
550
656
|
: null;
|
|
551
657
|
|
|
552
|
-
const schema = {
|
|
553
|
-
|
|
554
|
-
|
|
658
|
+
const schema = defineValidationSchema<MyForm>(({ model }) => {
|
|
659
|
+
validate(model.$.initialPayment, [required(), min(0)]);
|
|
660
|
+
cross(model.$.initialPayment, initialPaymentVsProperty); // ошибка сядет на initialPayment
|
|
661
|
+
});
|
|
555
662
|
```
|
|
556
663
|
|
|
557
|
-
|
|
664
|
+
> Для элемента массива / под-модели захвати нужный снапшот в замыкание (`const item = im.get();
|
|
665
|
+
> cross(im.$.x, () => rule(item))`) — `fn` всегда получает модель ТЕКУЩЕГО scope, а не под-модель.
|
|
666
|
+
|
|
667
|
+
### Conditional validation — `validateWhen`
|
|
668
|
+
|
|
669
|
+
Условная валидация — `validateWhen(() => cond, () => { … })`: правила внутри активны, только пока
|
|
670
|
+
условие истинно; при `false` ранее тронутые поля гасятся. Это не `enable` (то — поведение), а
|
|
671
|
+
включение/выключение самих проверок:
|
|
558
672
|
|
|
559
673
|
```typescript
|
|
560
|
-
import {
|
|
561
|
-
|
|
674
|
+
import { validateWhen, validate, cross } from '@reformer/core/validation';
|
|
675
|
+
|
|
676
|
+
validateWhen(
|
|
677
|
+
() => model.loanType === 'mortgage',
|
|
678
|
+
() => {
|
|
679
|
+
validate(model.$.propertyValue, [required(), min(1000000)]);
|
|
680
|
+
cross(model.$.initialPayment, initialPaymentVsProperty);
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Перезапуск прогона — `revalidateWhen` (мост поведение → валидация)
|
|
686
|
+
|
|
687
|
+
Схема прогоняется по требованию (submit/шаг). Чтобы перезапустить её при изменении зависимости —
|
|
688
|
+
`revalidateWhen` из **поведения** (единственный мост поведение → валидация):
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
692
|
+
import { validateModel } from '@reformer/core/validation';
|
|
693
|
+
|
|
694
|
+
const behavior = defineFormBehavior<MyForm>(({ model }) => {
|
|
695
|
+
revalidateWhen([model.$.propertyValue], () => void validateModel(model, schema));
|
|
696
|
+
});
|
|
562
697
|
```
|
|
563
698
|
|
|
564
699
|
### Extracting named rules
|
|
565
700
|
|
|
566
|
-
Когда тело
|
|
567
|
-
|
|
568
|
-
читается как оглавление:
|
|
701
|
+
Когда тело правила растёт — выноси в именованную функцию. Field-правило типизируй `Rule<T>`
|
|
702
|
+
(`(value) => ValidationError | null`), cross-field — обычной `(f: Root) => ValidationError | null`.
|
|
703
|
+
Схема остаётся плоской и читается как оглавление:
|
|
569
704
|
|
|
570
705
|
```typescript
|
|
571
|
-
import
|
|
706
|
+
import { defineValidationSchema, validate } from '@reformer/core/validation';
|
|
707
|
+
import { required } from '@reformer/core/validators';
|
|
708
|
+
import type { Rule } from '@reformer/core/validation';
|
|
572
709
|
|
|
573
|
-
|
|
710
|
+
// field-правило: (value) => error
|
|
711
|
+
const validateAdultAge: Rule<string> = (value) => {
|
|
574
712
|
if (!value) return null;
|
|
575
713
|
const age = new Date().getFullYear() - new Date(value).getFullYear();
|
|
576
714
|
return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;
|
|
577
715
|
};
|
|
578
716
|
|
|
579
|
-
const schema = {
|
|
580
|
-
|
|
581
|
-
};
|
|
717
|
+
const schema = defineValidationSchema<MyForm>(({ model }) => {
|
|
718
|
+
validate(model.$.birthDate, [required(), validateAdultAge]);
|
|
719
|
+
});
|
|
582
720
|
```
|
|
583
721
|
|
|
584
722
|
**Naming convention** (camelCase, семантика, не эхо оператора):
|
|
@@ -590,26 +728,41 @@ const schema = {
|
|
|
590
728
|
|
|
591
729
|
### Imports rule (#1 cause of cascading errors — read first)
|
|
592
730
|
|
|
593
|
-
-
|
|
594
|
-
-
|
|
595
|
-
-
|
|
731
|
+
- Модель/форма/хуки/типы (`FormModel`, `ValidationError`)/**примитивы behaviors** — из `@reformer/core`.
|
|
732
|
+
- Схема валидации (`defineValidationSchema` + операторы `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`) и раннер `validateModel` — из `@reformer/core/validation`.
|
|
733
|
+
- Чистые фабрики валидаторов (`required`/`min`/`email`/…) — из `@reformer/core/validators`.
|
|
734
|
+
- Декларативный DSL поведения (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
|
|
596
735
|
|
|
597
736
|
```typescript
|
|
598
737
|
// ✅ CORRECT
|
|
599
738
|
import {
|
|
600
739
|
createModel,
|
|
601
740
|
createForm,
|
|
602
|
-
validateFormModel,
|
|
603
741
|
useFormControl,
|
|
604
742
|
useFormControlValue,
|
|
605
743
|
type FormProxy,
|
|
606
744
|
type FieldConfig,
|
|
607
|
-
type
|
|
745
|
+
type FormModel,
|
|
746
|
+
type ValidationError,
|
|
608
747
|
} from '@reformer/core';
|
|
748
|
+
// схема валидации + раннер — отдельный слой:
|
|
749
|
+
import {
|
|
750
|
+
defineValidationSchema,
|
|
751
|
+
validate,
|
|
752
|
+
validateAsync,
|
|
753
|
+
validateWhen,
|
|
754
|
+
cross,
|
|
755
|
+
each,
|
|
756
|
+
apply,
|
|
757
|
+
validateModel,
|
|
758
|
+
type Rule,
|
|
759
|
+
type AsyncRule,
|
|
760
|
+
type ValidationSchema,
|
|
761
|
+
} from '@reformer/core/validation';
|
|
609
762
|
import { required, min, max, email } from '@reformer/core/validators';
|
|
610
763
|
// либо примитивы behaviors из основного пакета:
|
|
611
764
|
import { computeFrom, enableWhen, copyFrom } from '@reformer/core';
|
|
612
|
-
// либо декларативный DSL
|
|
765
|
+
// либо декларативный DSL поведения:
|
|
613
766
|
import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';
|
|
614
767
|
```
|
|
615
768
|
|
|
@@ -656,38 +809,56 @@ enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true })
|
|
|
656
809
|
computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);
|
|
657
810
|
```
|
|
658
811
|
|
|
659
|
-
### Валидаторы —
|
|
812
|
+
### Валидаторы — оператор `validate`, а не layout-нода
|
|
660
813
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
validate(
|
|
814
|
+
Валидация — **отдельный слой** (`@reformer/core/validation`), а не поле layout-ноды. Layout
|
|
815
|
+
(схема `createForm` / JSON-DSL) больше **не несёт** `validators`: правила живут в `ValidationSchema`,
|
|
816
|
+
а прогоняет их внешний раннер `validateModel`. ⚠️ `form.validate()`/`submit()` schema-валидацию
|
|
817
|
+
**НЕ запускают** — прогон только через `validateModel(model, schema)`.
|
|
664
818
|
|
|
665
|
-
|
|
819
|
+
```typescript
|
|
820
|
+
// ❌ WRONG - дерево { value, validators } и позиционная строка удалены
|
|
666
821
|
const schema = {
|
|
667
|
-
email: {
|
|
668
|
-
value: model.$.email,
|
|
669
|
-
component: Input,
|
|
670
|
-
validators: [required({ message: 'Email is required' }), email()],
|
|
671
|
-
},
|
|
822
|
+
email: { value: model.$.email, component: Input, validators: [required(), email()] },
|
|
672
823
|
};
|
|
824
|
+
validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
|
|
825
|
+
|
|
826
|
+
// ✅ CORRECT - операторы validate/validateAsync внутри defineValidationSchema; фабрики с options
|
|
827
|
+
const emailSchema = defineValidationSchema<MyForm>(({ model }) => {
|
|
828
|
+
validate(model.$.email, [required({ message: 'Email is required' }), email()]);
|
|
829
|
+
validateAsync(model.$.email, [
|
|
830
|
+
async (value, { signal }) => {
|
|
831
|
+
const res = await fetch(`/api/free?email=${value}`, { signal });
|
|
832
|
+
return (await res.json()).free ? null : { code: 'taken', message: 'Email занят' };
|
|
833
|
+
},
|
|
834
|
+
]);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// прогон по требованию (submit/шаг): ошибки сами доезжают до нод формы, warnings не блокируют
|
|
838
|
+
const ok = await validateModel(model, emailSchema);
|
|
673
839
|
```
|
|
674
840
|
|
|
675
|
-
### Cross-field —
|
|
841
|
+
### Cross-field — оператор `cross`, а не `ctx.form`
|
|
676
842
|
|
|
677
843
|
```typescript
|
|
678
|
-
// ❌ WRONG - ctx.form / ctx.setFieldValue / value.value — старый API,
|
|
844
|
+
// ❌ WRONG - ctx.form / ctx.setFieldValue / value.value — старый API; ModelValidator(value,scope,root) удалён
|
|
679
845
|
watchField(path.amount, (amount, ctx) => {
|
|
680
846
|
const rate = ctx.form.rate.value.value;
|
|
681
847
|
ctx.setFieldValue('total', amount * rate);
|
|
682
848
|
});
|
|
683
849
|
|
|
684
|
-
// ✅ CORRECT - compute на сигналах; cross-field
|
|
850
|
+
// ✅ CORRECT - compute (поведение) на сигналах; cross-field (валидация) через оператор cross
|
|
685
851
|
compute(model.$.total, () => (model.amount ?? 0) * (model.rate ?? 0));
|
|
686
852
|
|
|
687
|
-
|
|
688
|
-
|
|
853
|
+
// cross-field правило — обычная функция над снапшотом Root (`model.get()`), навешивается через cross(sig, fn):
|
|
854
|
+
const amountVsMax = (f: MyForm): ValidationError | null =>
|
|
855
|
+
f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
|
|
689
856
|
? { code: 'tooBig', message: 'Превышает лимит' }
|
|
690
857
|
: null;
|
|
858
|
+
|
|
859
|
+
const amountSchema = defineValidationSchema<MyForm>(({ model }) => {
|
|
860
|
+
cross(model.$.amount, amountVsMax); // fn получает model.get(), вешает ошибку на model.$.amount
|
|
861
|
+
});
|
|
691
862
|
```
|
|
692
863
|
|
|
693
864
|
### Form-shape types — `type` over `interface`
|
|
@@ -731,20 +902,16 @@ if (isFieldNode(node)) { ... }
|
|
|
731
902
|
| `enableWhen`/`disableWhen` не срабатывает | Поле не материализовано в форме (элемент массива) | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |
|
|
732
903
|
| `Cycle detected` | Взаимные `compute`/`computeFrom` без стабилизации | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |
|
|
733
904
|
| Значение поля не пишется | Пишем в форму вместо модели | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` |
|
|
734
|
-
| Ошибки не появляются после submit | Не вызвали `
|
|
905
|
+
| Ошибки не появляются после submit | Не вызвали `validateModel` (`form.submit()` НЕ гоняет schema-валидацию) | `await validateModel(model, schema)` из `@reformer/core/validation` — он роутит ошибки в ноды |
|
|
735
906
|
|
|
736
907
|
## 7. Import Patterns
|
|
737
908
|
|
|
738
909
|
```typescript
|
|
739
|
-
// Модель, форма,
|
|
910
|
+
// Модель, форма, хуки, типы, примитивы behaviors — из @reformer/core
|
|
740
911
|
import {
|
|
741
912
|
// фабрики
|
|
742
913
|
createModel,
|
|
743
914
|
createForm,
|
|
744
|
-
// валидация данных (headless)
|
|
745
|
-
validateModel,
|
|
746
|
-
validateModelSync,
|
|
747
|
-
validateFormModel,
|
|
748
915
|
// хуки
|
|
749
916
|
useFormControl,
|
|
750
917
|
useFormControlValue,
|
|
@@ -771,14 +938,30 @@ import type {
|
|
|
771
938
|
ModelArray, // реактивный массив модели (push/removeAt/at/map/length)
|
|
772
939
|
ModelSignals, // дерево сигналов ($)
|
|
773
940
|
PathAwareSignal, // сигнал, знающий свой путь
|
|
774
|
-
ModelValidator, // (value, scope, root) => ValidationError | null
|
|
775
941
|
ValidationError,
|
|
776
|
-
FieldConfig, // { value, component, componentProps?,
|
|
942
|
+
FieldConfig, // { value, component, componentProps?, ... } — layout, БЕЗ валидаторов
|
|
777
943
|
FormSchema,
|
|
778
944
|
FieldControlState,
|
|
779
945
|
} from '@reformer/core';
|
|
780
946
|
|
|
781
|
-
//
|
|
947
|
+
// Схема валидации: операторы + раннер — из /validation
|
|
948
|
+
import {
|
|
949
|
+
defineValidationSchema,
|
|
950
|
+
validate,
|
|
951
|
+
validateAsync,
|
|
952
|
+
validateWhen,
|
|
953
|
+
cross,
|
|
954
|
+
each,
|
|
955
|
+
apply,
|
|
956
|
+
validateModel, // раннер: validateModel(model, schema) => Promise<boolean>
|
|
957
|
+
} from '@reformer/core/validation';
|
|
958
|
+
import type {
|
|
959
|
+
Rule, // (value) => ValidationError | null
|
|
960
|
+
AsyncRule, // (value, { signal }) => Promise<ValidationError | null>
|
|
961
|
+
ValidationSchema,
|
|
962
|
+
} from '@reformer/core/validation';
|
|
963
|
+
|
|
964
|
+
// Валидаторы — чистые фабрики из /validators (кладутся в validate(sig, [...]))
|
|
782
965
|
import { required, min, max, email, minLength, pattern } from '@reformer/core/validators';
|
|
783
966
|
|
|
784
967
|
// Декларативный DSL behaviors — из /behaviors
|
|
@@ -872,7 +1055,9 @@ const model = createModel<MyForm>({
|
|
|
872
1055
|
## 9. 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)
|
|
873
1056
|
|
|
874
1057
|
Под M1 схема **привязывает поле к сигналу модели** (`value: model.$.field`) и держит
|
|
875
|
-
UI-конфиг (`component`/`componentProps`)
|
|
1058
|
+
UI-конфиг (`component`/`componentProps`). Значения принадлежат модели. Валидаторов в
|
|
1059
|
+
layout-схеме НЕТ — правила живут в отдельной схеме `defineValidationSchema` из
|
|
1060
|
+
`@reformer/core/validation` и запускаются раннером `validateModel(model, schema)`.
|
|
876
1061
|
|
|
877
1062
|
### Field node
|
|
878
1063
|
|
|
@@ -881,8 +1066,6 @@ UI-конфиг (`component`/`componentProps`) + валидаторы. Знач
|
|
|
881
1066
|
value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
|
|
882
1067
|
component: Input, // React-компонент
|
|
883
1068
|
componentProps?: object, // пропсы (label, placeholder, options, type, ...)
|
|
884
|
-
validators?: [...], // чистые фабрики / ModelValidator
|
|
885
|
-
asyncValidators?: [...], // async ModelValidator
|
|
886
1069
|
disabled?: boolean,
|
|
887
1070
|
updateOn?: 'change' | 'blur' | 'submit',
|
|
888
1071
|
debounce?: number,
|
|
@@ -897,17 +1080,22 @@ UI-конфиг (`component`/`componentProps`) + валидаторы. Знач
|
|
|
897
1080
|
|
|
898
1081
|
```typescript
|
|
899
1082
|
import { createModel, createForm } from '@reformer/core';
|
|
1083
|
+
import { defineValidationSchema, validate } from '@reformer/core/validation';
|
|
900
1084
|
import { required } from '@reformer/core/validators';
|
|
901
1085
|
import { Input, Select, Checkbox } from '@reformer/ui-kit';
|
|
902
1086
|
|
|
903
1087
|
const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
|
|
904
1088
|
|
|
1089
|
+
// Валидация — отдельным слоем (запуск: await validateModel(model, myValidation))
|
|
1090
|
+
const myValidation = defineValidationSchema<MyForm>(({ model }) => {
|
|
1091
|
+
validate(model.$.name, [required()]);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
905
1094
|
const schema = {
|
|
906
1095
|
name: {
|
|
907
1096
|
value: model.$.name,
|
|
908
1097
|
component: Input,
|
|
909
1098
|
componentProps: { label: 'Name', placeholder: 'Enter name' },
|
|
910
|
-
validators: [required()],
|
|
911
1099
|
},
|
|
912
1100
|
age: {
|
|
913
1101
|
value: model.$.age,
|
|
@@ -1057,13 +1245,16 @@ const form = createForm<MyForm>({ model, schema });
|
|
|
1057
1245
|
properties: { array: model.properties, item: propertyItem },
|
|
1058
1246
|
```
|
|
1059
1247
|
|
|
1060
|
-
2. **Validation-схема `
|
|
1061
|
-
`
|
|
1062
|
-
|
|
1248
|
+
2. **Validation-схема (`@reformer/core/validation`)** — per-item правила пишутся оператором
|
|
1249
|
+
`each(arr, (im) => {...})` внутри `defineValidationSchema`; `im` — под-модель элемента
|
|
1250
|
+
(`im.$.field` — его сигналы). Запуск — внешним `validateModel(model, schema)`.
|
|
1063
1251
|
|
|
1064
1252
|
```typescript
|
|
1065
|
-
//
|
|
1066
|
-
|
|
1253
|
+
// внутри defineValidationSchema<MyForm>(({ model }) => { ... })
|
|
1254
|
+
each(model.properties, (im) => {
|
|
1255
|
+
validate(im.$.description, [required()]);
|
|
1256
|
+
validate(im.$.estimatedValue, [required(), min(1)]);
|
|
1257
|
+
});
|
|
1067
1258
|
```
|
|
1068
1259
|
|
|
1069
1260
|
3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`ModelArrayNode`),
|
|
@@ -1071,9 +1262,9 @@ const form = createForm<MyForm>({ model, schema });
|
|
|
1071
1262
|
`@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).
|
|
1072
1263
|
|
|
1073
1264
|
> **Не путай форму по движку.** `createForm` принимает **только** `{ array, item }`;
|
|
1074
|
-
>
|
|
1075
|
-
> control={form.x}` (или `FormArraySection`) — рендер. `
|
|
1076
|
-
>
|
|
1265
|
+
> per-item валидация — это `each(model.<array>, (im) => ...)` в validation-схеме; `FormArray.Root
|
|
1266
|
+
> control={form.x}` (или `FormArraySection`) — рендер. `each` в layout-схеме `createForm`
|
|
1267
|
+
> не подхватится, а `{ array, item }` в validation-схеме не обходится.
|
|
1077
1268
|
|
|
1078
1269
|
### Array operations — на модели
|
|
1079
1270
|
|
|
@@ -1141,25 +1332,30 @@ function ItemsList({ form }: { form: FormProxy<MyForm> }) {
|
|
|
1141
1332
|
|
|
1142
1333
|
### Array Cross-Validation
|
|
1143
1334
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1335
|
+
Whole-array правило пишется оператором `cross(sig, (f) => ...)`: `f` — снапшот `model.get()`
|
|
1336
|
+
(плоские значения), ошибка вешается на поле-носитель `sig`. Per-item правила — `each`
|
|
1337
|
+
(см. выше, оба — из `@reformer/core/validation`).
|
|
1338
|
+
|
|
1339
|
+
Носитель — любое **скалярное** поле формы (флаг `hasItems`, итоговая сумма и т.п.), у которого
|
|
1340
|
+
есть сигнал `model.$.<field>` и нода в форме:
|
|
1147
1341
|
|
|
1148
1342
|
```typescript
|
|
1149
|
-
import
|
|
1343
|
+
import { cross } from '@reformer/core/validation';
|
|
1150
1344
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1345
|
+
type MyForm = { hasItems: boolean; items: Item[] };
|
|
1346
|
+
|
|
1347
|
+
// внутри defineValidationSchema<MyForm>(({ model }) => { ... })
|
|
1348
|
+
cross(model.$.hasItems, (f: MyForm) => {
|
|
1349
|
+
const names = f.items.map((i) => i.name);
|
|
1154
1350
|
return names.length !== new Set(names).size
|
|
1155
1351
|
? { code: 'duplicate', message: 'Item names must be unique' }
|
|
1156
1352
|
: null;
|
|
1157
|
-
};
|
|
1353
|
+
});
|
|
1158
1354
|
```
|
|
1159
1355
|
|
|
1160
1356
|
## 11. See also
|
|
1161
1357
|
|
|
1162
|
-
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item
|
|
1358
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item валидация `each`
|
|
1163
1359
|
- CDK / ui-kit form-array (`FormArray.Root`, `FormArraySection`) — `find_recipe(topic="form-array")`
|
|
1164
1360
|
|
|
1165
1361
|
## 12. 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)
|
|
@@ -1250,54 +1446,69 @@ clearWhenOff(model.$.hasProperty, model.properties);
|
|
|
1250
1446
|
|
|
1251
1447
|
## 14. 12. MULTI-STEP FORM VALIDATION
|
|
1252
1448
|
|
|
1253
|
-
Каждый шаг — своя
|
|
1254
|
-
следующему шагу проверяется `
|
|
1255
|
-
общей схеме (
|
|
1256
|
-
UI подсветит проблемные поля
|
|
1449
|
+
Каждый шаг — своя `ValidationSchema<Form>` (обычная функция `({ model }) => void`, обёрнутая
|
|
1450
|
+
`defineValidationSchema`). Переход к следующему шагу проверяется `validateModel(model, stepSchema)`;
|
|
1451
|
+
полный submit — по общей схеме (композиция шагов через `apply(...)`). `validateModel` сам разносит
|
|
1452
|
+
ошибки по нодам формы (`getNodeForSignal(sig).setErrors(...)`), поэтому UI подсветит проблемные поля
|
|
1453
|
+
текущего шага автоматически. Валидация живёт ОТДЕЛЬНО от layout: сам `RenderNode`/JSON-узел валидаторов
|
|
1454
|
+
не несёт — схема инъектируется в рантайме и роутит ошибки по тем же нодам.
|
|
1257
1455
|
|
|
1258
1456
|
```typescript
|
|
1259
|
-
import {
|
|
1457
|
+
import { type FormModel } from '@reformer/core';
|
|
1458
|
+
import {
|
|
1459
|
+
validate,
|
|
1460
|
+
apply,
|
|
1461
|
+
defineValidationSchema,
|
|
1462
|
+
validateModel,
|
|
1463
|
+
type ValidationSchema,
|
|
1464
|
+
} from '@reformer/core/validation';
|
|
1260
1465
|
import { required, min } from '@reformer/core/validators';
|
|
1261
1466
|
|
|
1262
|
-
// Под-схема
|
|
1263
|
-
const step1Schema =
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
{ value: m.$.loanAmount, validators: [required(), min(50000)] },
|
|
1267
|
-
],
|
|
1467
|
+
// Под-схема шага — обычная функция ({ model }) => void. Значения проверяет оператор validate(sig, [rules]).
|
|
1468
|
+
const step1Schema = defineValidationSchema<Form>(({ model }) => {
|
|
1469
|
+
validate(model.$.loanType, [required()]);
|
|
1470
|
+
validate(model.$.loanAmount, [required(), min(50000)]);
|
|
1268
1471
|
});
|
|
1269
1472
|
|
|
1270
|
-
const step2Schema =
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
{ value: m.$.personalData.lastName, validators: [required()] },
|
|
1274
|
-
],
|
|
1473
|
+
const step2Schema = defineValidationSchema<Form>(({ model }) => {
|
|
1474
|
+
validate(model.$.personalData.firstName, [required()]);
|
|
1475
|
+
validate(model.$.personalData.lastName, [required()]);
|
|
1275
1476
|
});
|
|
1276
1477
|
|
|
1277
|
-
// Карта шагов + полная схема (
|
|
1278
|
-
const STEP_SCHEMAS = [step1Schema, step2Schema];
|
|
1279
|
-
const fullSchema =
|
|
1280
|
-
children: STEP_SCHEMAS.map((build) => build(m)),
|
|
1281
|
-
});
|
|
1478
|
+
// Карта шагов + полная схема (композиция под-схем через apply — заменяет пошаговую группировку деревом)
|
|
1479
|
+
const STEP_SCHEMAS: readonly ValidationSchema<Form>[] = [step1Schema, step2Schema];
|
|
1480
|
+
const fullSchema = defineValidationSchema<Form>(() => apply(...STEP_SCHEMAS));
|
|
1282
1481
|
```
|
|
1283
1482
|
|
|
1284
1483
|
```typescript
|
|
1285
|
-
// Переход к следующему
|
|
1484
|
+
// Переход к следующему шагу. validateModel возвращает Promise<boolean> (true = нет блокирующих ошибок;
|
|
1485
|
+
// severity:'warning' не блокирует). Устаревшие прогоны той же (model, schema) отменяются автоматически.
|
|
1286
1486
|
const goToNextStep = async () => {
|
|
1287
|
-
const
|
|
1288
|
-
if (!
|
|
1487
|
+
const ok = await validateModel(model, STEP_SCHEMAS[currentStep - 1]);
|
|
1488
|
+
if (!ok) return; // ошибки уже проставлены в ноды текущего шага
|
|
1289
1489
|
setCurrentStep(currentStep + 1);
|
|
1290
1490
|
};
|
|
1291
1491
|
|
|
1292
|
-
// Полный submit
|
|
1492
|
+
// Полный submit — по общей схеме
|
|
1293
1493
|
const handleSubmit = async () => {
|
|
1294
|
-
const
|
|
1295
|
-
if (
|
|
1494
|
+
const ok = await validateModel(model, fullSchema);
|
|
1495
|
+
if (ok) {
|
|
1296
1496
|
await onSubmit(model.get());
|
|
1297
1497
|
}
|
|
1298
1498
|
};
|
|
1299
1499
|
```
|
|
1300
1500
|
|
|
1501
|
+
Слой-потребитель (`FormWizard`) обычно оборачивает это в конфиг с per-step и полной валидацией:
|
|
1502
|
+
|
|
1503
|
+
```typescript
|
|
1504
|
+
function makeValidationConfig(model: FormModel<Form>) {
|
|
1505
|
+
return {
|
|
1506
|
+
validateStep: (n: number): Promise<boolean> => validateModel(model, STEP_SCHEMAS[n - 1]),
|
|
1507
|
+
validateAll: (): Promise<boolean> => validateModel(model, fullSchema),
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1301
1512
|
### Multi-Step Component Example
|
|
1302
1513
|
|
|
1303
1514
|
```tsx
|
|
@@ -1305,8 +1516,8 @@ function MultiStepForm() {
|
|
|
1305
1516
|
const [step, setStep] = useState(1);
|
|
1306
1517
|
|
|
1307
1518
|
const nextStep = async () => {
|
|
1308
|
-
const
|
|
1309
|
-
if (
|
|
1519
|
+
const ok = await validateModel(model, STEP_SCHEMAS[step - 1]);
|
|
1520
|
+
if (ok) setStep(step + 1);
|
|
1310
1521
|
};
|
|
1311
1522
|
|
|
1312
1523
|
return (
|
|
@@ -1385,27 +1596,44 @@ compute(model.$.b, () => model.a + 1); // расходится → Cycle detecte
|
|
|
1385
1596
|
compute(model.$.total, () => model.price * model.qty); // одно направление
|
|
1386
1597
|
```
|
|
1387
1598
|
|
|
1388
|
-
### Cross-field валидация — через `
|
|
1599
|
+
### Cross-field валидация — через `cross(sig, fn)`
|
|
1600
|
+
|
|
1601
|
+
Cross-field живёт в схеме валидации (`@reformer/core/validation`), а не в layout: правило вешается на
|
|
1602
|
+
ПОЛЕ-НОСИТЕЛЬ ошибки оператором `cross(sig, fn)`, а `fn` читает снапшот текущего scope (`model.get()`).
|
|
1389
1603
|
|
|
1390
1604
|
```typescript
|
|
1391
|
-
import
|
|
1605
|
+
import { defineValidationSchema, validate, cross, validateModel } from '@reformer/core/validation';
|
|
1606
|
+
import { required } from '@reformer/core/validators';
|
|
1607
|
+
import { revalidateWhen, type ValidationError } from '@reformer/core';
|
|
1392
1608
|
|
|
1393
|
-
//
|
|
1394
|
-
const
|
|
1609
|
+
// ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
|
|
1610
|
+
const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
|
|
1395
1611
|
root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;
|
|
1612
|
+
const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
|
|
1613
|
+
validateFormModel(model, legacySchema);
|
|
1396
1614
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
};
|
|
1615
|
+
// ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root
|
|
1616
|
+
const field1LessThanField2 = (f: Form): ValidationError | null =>
|
|
1617
|
+
f.field1 > f.field2 ? { code: 'error', message: 'Invalid' } : null;
|
|
1400
1618
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1619
|
+
const schema = defineValidationSchema<Form>(({ model }) => {
|
|
1620
|
+
validate(model.$.field1, [required()]);
|
|
1621
|
+
cross(model.$.field1, field1LessThanField2); // правило на поле-носителе ошибки
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
// Прогон — ТОЛЬКО внешним раннером; ошибки сами доезжают до нод формы.
|
|
1625
|
+
// ⚠️ form.validate()/submit() схему валидации больше НЕ прогоняют.
|
|
1626
|
+
await validateModel(model, schema);
|
|
1627
|
+
|
|
1628
|
+
// Перезапуск правила при изменении соседнего поля — мост «поведение → валидация»:
|
|
1629
|
+
revalidateWhen([model.$.field2], () => void validateModel(model, schema));
|
|
1404
1630
|
```
|
|
1405
1631
|
|
|
1406
|
-
>
|
|
1407
|
-
> `
|
|
1408
|
-
>
|
|
1632
|
+
> **Актуальный vs удалённый API.** Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`
|
|
1633
|
+
> ТЕПЕРЬ существуют — в `@reformer/core/validation` с новыми сигнатурами (ambient, активны только внутри прогона
|
|
1634
|
+
> `validateModel`). УДАЛЕНЫ: `applyWhen`, `validateGroup`/`validateTree`/`validateForm`/`validateFormModel`,
|
|
1635
|
+
> типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn` и `ctx.form.*`/`ctx.setFieldValue` (наследие
|
|
1636
|
+
> path-based архитектуры). См. `17-nonexistent-api.md`.
|
|
1409
1637
|
|
|
1410
1638
|
## 16. 14. PROJECT STRUCTURE (COLOCATION)
|
|
1411
1639
|
|
|
@@ -1500,7 +1728,7 @@ forms/
|
|
|
1500
1728
|
├── schema/ # The form definition
|
|
1501
1729
|
│ ├── model.ts # createModel factory + initial values + array-element factories
|
|
1502
1730
|
│ ├── schema.ts # createForm schema tree ({ value: model.$.x, component })
|
|
1503
|
-
│ ├── validation.ts #
|
|
1731
|
+
│ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
|
|
1504
1732
|
│ ├── behavior.ts # defineFormBehavior(...)
|
|
1505
1733
|
│ └── create-form.ts # Assembly: createForm({ model, schema, behavior })
|
|
1506
1734
|
│
|
|
@@ -1692,6 +1920,11 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
|
|
|
1692
1920
|
|
|
1693
1921
|
### Integration with UI libraries (shadcn etc.)
|
|
1694
1922
|
|
|
1923
|
+
> **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
|
|
1924
|
+
> `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
|
|
1925
|
+
> компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
|
|
1926
|
+
> контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
|
|
1927
|
+
|
|
1695
1928
|
Если есть существующая design system — оборачивай её компоненты в один
|
|
1696
1929
|
`MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
|
|
1697
1930
|
тип input'а — пусть `componentProps.type` диспатчит внутри.
|
|
@@ -1720,22 +1953,28 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
|
|
|
1720
1953
|
|
|
1721
1954
|
## 18. 15. NON-EXISTENT API (DO NOT USE)
|
|
1722
1955
|
|
|
1723
|
-
**Следующего API НЕТ в @reformer/core** (
|
|
1724
|
-
удалено при переходе на M1):
|
|
1956
|
+
**Следующего API НЕТ в @reformer/core** (наследие старой path-based архитектуры,
|
|
1957
|
+
удалено при переходе на M1 и при разделении валидации/поведения):
|
|
1958
|
+
|
|
1959
|
+
> ⚠️ Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`
|
|
1960
|
+
> **существуют** — но живут в сабпути `@reformer/core/validation` (ambient-схема
|
|
1961
|
+
> `defineValidationSchema(({ model }) => …)`, раннер `validateModel`), а НЕ в корне и НЕ
|
|
1962
|
+
> в `@reformer/core/validators`. Ниже перечислено то, чего действительно нет.
|
|
1725
1963
|
|
|
1726
1964
|
| Wrong | Correct | Notes |
|
|
1727
1965
|
| -------------------------------- | ------------------------------------------------ | ---------------------------------------------- |
|
|
1728
1966
|
| `useForm` | `createModel` + `createForm` | Хука useForm нет |
|
|
1729
|
-
| `
|
|
1730
|
-
| `
|
|
1731
|
-
| `
|
|
1732
|
-
| `
|
|
1733
|
-
| `ValidationSchemaFn`, `BehaviorSchemaFn` | `
|
|
1967
|
+
| `validateForm`, `validateFormModel` | `validateModel(model, schema)` из `@reformer/core/validation` | Legacy-движок дерева `{ value, validators }` удалён; внешний раннер — `validateModel` |
|
|
1968
|
+
| `validateModelSync` | `await validateModel(model, schema)` | Синхронного раннера нет — прогон асинхронный (`Promise<boolean>`) |
|
|
1969
|
+
| `applyWhen` | `validateWhen(() => cond, () => { … })` | Условная валидация — оператором `validateWhen`, не узлом `{ when, children }` |
|
|
1970
|
+
| `validateItems`, `validateGroup`, `validateTree` | `each(model.arr, (im) => { … })` | Per-item — оператором `each`; под-схема — прямой вызов `sub({ model: model.child })` |
|
|
1971
|
+
| `ValidationSchemaFn`, `BehaviorSchemaFn` | `ValidationSchema<T>` (`defineValidationSchema`) / `defineFormBehavior` | Типы path-схем удалены |
|
|
1972
|
+
| `equalTo`, `custom`, `notEmpty` (validators) | inline `Rule<T>` `(value) => err \| null` в `validate(sig, [...])`; сравнение полей — `cross(sig, fn)` | Таких фабрик нет; кастомное правило — обычная функция значения, cross-field — `cross` |
|
|
1973
|
+
| `form.submit()` / `form.validate()` прогоняют схему | `await validateModel(model, schema)` перед submit/шагом | submit/validate НЕ запускают ambient-схему валидации |
|
|
1734
1974
|
| `FieldPath`, `FieldPathNode` | `model.$.field` (`PathAwareSignal`) | Пути заменены сигналами |
|
|
1735
1975
|
| `ctx.form.x.value.value` | `model.x` / `model.$.x.value` | В behaviors читаем модель напрямую |
|
|
1736
1976
|
| `ctx.setFieldValue(name, value)` | `model.x = value` / `compute(...)` | Не существует |
|
|
1737
1977
|
| `transformers`, `createTransformer` | `transformValue(signal, fn)` | Готового набора трансформеров нет |
|
|
1738
|
-
| `equalTo`, `custom`, `notEmpty`, `date` (validators) | кастомный `ModelValidator` | Таких фабрик нет; пиши функцию `(value, scope, root)` |
|
|
1739
1978
|
| `useHiddenCondition` | `useFormControlValue` + условный рендер в JSX | Хука нет |
|
|
1740
1979
|
| `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
|
|
1741
1980
|
| `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |
|
|
@@ -1743,33 +1982,49 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
|
|
|
1743
1982
|
### Common Import Errors
|
|
1744
1983
|
|
|
1745
1984
|
```typescript
|
|
1746
|
-
// WRONG -
|
|
1747
|
-
import { useForm, validateForm } from '@reformer/core';
|
|
1748
|
-
import { validate, applyWhen, equalTo } from '@reformer/core/validators';
|
|
1749
|
-
import { transformers } from '@reformer/core/behaviors';
|
|
1750
|
-
import type { FieldPath, ValidationSchemaFn } from '@reformer/core';
|
|
1985
|
+
// WRONG - этих символов / путей НЕ существует
|
|
1986
|
+
import { useForm, validateForm, validateFormModel } from '@reformer/core'; // NO!
|
|
1987
|
+
import { validate, applyWhen, equalTo } from '@reformer/core/validators'; // NO! операторов тут нет
|
|
1988
|
+
import { transformers } from '@reformer/core/behaviors'; // NO!
|
|
1989
|
+
import type { FieldPath, ValidationSchemaFn } from '@reformer/core'; // NO!
|
|
1751
1990
|
|
|
1752
1991
|
// CORRECT
|
|
1753
|
-
import { createModel, createForm,
|
|
1754
|
-
import {
|
|
1755
|
-
|
|
1756
|
-
import
|
|
1992
|
+
import { createModel, createForm, useFormControl } from '@reformer/core';
|
|
1993
|
+
import type { FormModel, ValidationError, FieldConfig } from '@reformer/core';
|
|
1994
|
+
// Операторы валидации + раннер + defineValidationSchema — отдельный сабпуть:
|
|
1995
|
+
import {
|
|
1996
|
+
validate, validateAsync, validateWhen, cross, each, apply,
|
|
1997
|
+
defineValidationSchema, validateModel,
|
|
1998
|
+
type Rule, type AsyncRule, type ValidationSchema,
|
|
1999
|
+
} from '@reformer/core/validation';
|
|
2000
|
+
// Фабрики-валидаторы (value-only, принимают nullable):
|
|
2001
|
+
import { required, email, min, minLength, pattern } from '@reformer/core/validators';
|
|
2002
|
+
// Поведение (отдельный слой, контракт не менялся):
|
|
2003
|
+
import { defineFormBehavior, compute, onChange, revalidateWhen } from '@reformer/core/behaviors';
|
|
1757
2004
|
```
|
|
1758
2005
|
|
|
1759
2006
|
### Schema Common Mistakes
|
|
1760
2007
|
|
|
2008
|
+
Валидация и layout — **разные** контракты. Layout-узел привязывает поле к сигналу и
|
|
2009
|
+
НЕ несёт `validators`; правила живут в отдельной `defineValidationSchema`.
|
|
2010
|
+
|
|
1761
2011
|
```typescript
|
|
1762
|
-
// WRONG -
|
|
2012
|
+
// WRONG - примитив/литерал вместо сигнала, и validators прямо в layout-узле
|
|
1763
2013
|
const schema = {
|
|
1764
|
-
name:
|
|
1765
|
-
email: { value: '',
|
|
2014
|
+
name: '', // нет привязки к сигналу
|
|
2015
|
+
email: { value: '', validators: [required()] }, // value — литерал, а validators в layout больше нет
|
|
1766
2016
|
};
|
|
1767
2017
|
|
|
1768
|
-
// CORRECT -
|
|
1769
|
-
const
|
|
2018
|
+
// CORRECT - layout только связывает поле с сигналом модели...
|
|
2019
|
+
const layout = {
|
|
1770
2020
|
name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
|
|
1771
2021
|
email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
|
|
1772
2022
|
};
|
|
2023
|
+
|
|
2024
|
+
// ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
|
|
2025
|
+
const validation = defineValidationSchema<Form>(({ model }) => {
|
|
2026
|
+
validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);
|
|
2027
|
+
});
|
|
1773
2028
|
```
|
|
1774
2029
|
|
|
1775
2030
|
### Behaviors Common Mistakes
|
|
@@ -1782,6 +2037,14 @@ enableWhen(path.city, (form) => Boolean(form.country));
|
|
|
1782
2037
|
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
|
|
1783
2038
|
```
|
|
1784
2039
|
|
|
2040
|
+
Поведение **не владеет** валидацией. Чтобы поведение инициировало прогон схемы — мост
|
|
2041
|
+
`revalidateWhen` (validate/submit сами схему не запускают):
|
|
2042
|
+
|
|
2043
|
+
```typescript
|
|
2044
|
+
// CORRECT - поведение дёргает внешний раннер валидации при изменении зависимости
|
|
2045
|
+
revalidateWhen([model.$.password], () => void validateModel(model, validation));
|
|
2046
|
+
```
|
|
2047
|
+
|
|
1785
2048
|
## 19. Purpose
|
|
1786
2049
|
|
|
1787
2050
|
«Показать поле X только если Y» под M1 решается тремя независимыми механизмами — выбирай по
|
|
@@ -1789,12 +2052,13 @@ enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true })
|
|
|
1789
2052
|
|
|
1790
2053
|
1. **Видимость / доступность** (поле остаётся в модели, но выключается/включается и не валидируется)
|
|
1791
2054
|
— behaviors: `enableWhen`, а для условной записи значения — `compute` / `copyFrom` с опцией `{ when }`.
|
|
1792
|
-
2. **Условная валидация** (правила применяются только в истинной ветке) —
|
|
1793
|
-
|
|
2055
|
+
2. **Условная валидация** (правила применяются только в истинной ветке) — оператор
|
|
2056
|
+
`validateWhen(() => cond, () => {…})` внутри схемы `defineValidationSchema`, прогоняется раннером `validateModel`.
|
|
1794
2057
|
3. **Скрытие из разметки** (узел вообще не рендерится) — `useFormControlValue` + условный рендер в JSX.
|
|
1795
2058
|
|
|
1796
|
-
>
|
|
1797
|
-
>
|
|
2059
|
+
> Условная **валидация** — оператор `validateWhen` из `@reformer/core/validation`; старого branch-узла
|
|
2060
|
+
> `{ when, children }` и хелпера `applyWhen` больше нет — см.
|
|
2061
|
+
> [Anti-patterns удалённого контракта валидации](#anti-patterns-удалённого-контракта-валидации) и `17-nonexistent-api.md`.
|
|
1798
2062
|
|
|
1799
2063
|
## 20. Видимость и доступность
|
|
1800
2064
|
|
|
@@ -1850,35 +2114,42 @@ export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
|
|
|
1850
2114
|
|
|
1851
2115
|
## 21. Условная валидация
|
|
1852
2116
|
|
|
1853
|
-
Правила, действующие только при выполнении условия,
|
|
1854
|
-
`
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2117
|
+
Правила, действующие только при выполнении условия, оборачиваются оператором
|
|
2118
|
+
`validateWhen(() => cond, () => {…})` внутри схемы `defineValidationSchema<T>(({ model }) => …)`.
|
|
2119
|
+
Условие `cond` читает модель напрямую (`model.field` — снимок на момент прогона): при истинном условии
|
|
2120
|
+
раннер применяет вложенные `validate`/`cross`/…, при ложном — правила ветки **гасятся** (поля,
|
|
2121
|
+
затронутые ранее, получают `setErrors([])`). Прогон — внешним раннером
|
|
2122
|
+
`validateModel(model, schema): Promise<boolean>` (ошибки сами роутятся в ноды формы).
|
|
1858
2123
|
|
|
1859
2124
|
```typescript
|
|
1860
|
-
import {
|
|
2125
|
+
import {
|
|
2126
|
+
validate,
|
|
2127
|
+
validateWhen,
|
|
2128
|
+
defineValidationSchema,
|
|
2129
|
+
validateModel,
|
|
2130
|
+
} from '@reformer/core/validation';
|
|
1861
2131
|
import { required, min } from '@reformer/core/validators';
|
|
1862
2132
|
|
|
1863
|
-
const schema = {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
],
|
|
2133
|
+
const schema = defineValidationSchema<CreditForm>(({ model }) => {
|
|
2134
|
+
validate(model.$.loanType, [required()]);
|
|
2135
|
+
|
|
2136
|
+
// ветка применяется только для ипотеки
|
|
2137
|
+
validateWhen(
|
|
2138
|
+
() => model.loanType === 'mortgage',
|
|
2139
|
+
() => {
|
|
2140
|
+
validate(model.$.propertyValue, [required(), min(1_000_000)]);
|
|
2141
|
+
validate(model.$.initialPayment, [required()]);
|
|
1873
2142
|
},
|
|
1874
|
-
|
|
1875
|
-
};
|
|
2143
|
+
);
|
|
2144
|
+
});
|
|
1876
2145
|
|
|
1877
|
-
const
|
|
2146
|
+
const ok: boolean = await validateModel(model, schema);
|
|
1878
2147
|
```
|
|
1879
2148
|
|
|
1880
2149
|
Переключение `loanType` с `mortgage` на другой тип автоматически снимает ошибки `required` с
|
|
1881
|
-
`propertyValue`/`initialPayment` —
|
|
2150
|
+
`propertyValue`/`initialPayment` — при ложном условии ветка `validateWhen` гасит их (`setErrors([])`).
|
|
2151
|
+
`validateWhen` управляет только **валидацией**; чтобы поля ещё и выпадали из состояния (сброс
|
|
2152
|
+
значения), держи их под `enableWhen` из раздела выше — слои независимы.
|
|
1882
2153
|
|
|
1883
2154
|
## 22. Скрытие в JSX
|
|
1884
2155
|
|
|
@@ -1902,42 +2173,57 @@ function MortgageFields({ form }: { form: CreditForm }) {
|
|
|
1902
2173
|
```
|
|
1903
2174
|
|
|
1904
2175
|
Скрытие в JSX — чисто визуальное: если поле должно ещё и **выпадать из валидации/состояния**,
|
|
1905
|
-
сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или
|
|
2176
|
+
сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или `validateWhen` (валидация).
|
|
1906
2177
|
|
|
1907
|
-
## 23.
|
|
2178
|
+
## 23. Anti-patterns удалённого контракта валидации
|
|
1908
2179
|
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2180
|
+
Условную валидацию раньше собирали иначе; эти формы удалены при переходе на `@reformer/core/validation`.
|
|
2181
|
+
Ниже — **как НЕ надо** (чтобы не регрессировать); рабочий вариант — `validateWhen` из раздела выше.
|
|
2182
|
+
|
|
2183
|
+
- **Нет branch-узла `{ when, children }` в дереве схемы.** Старая схема была деревом
|
|
2184
|
+
`{ value, validators: [...] }` с ветками `{ when: (scope, root) => boolean, children: [...] }`,
|
|
2185
|
+
которое обходил `validateFormModel`. Теперь схема — функция `({ model }) => void`, а условие —
|
|
2186
|
+
оператор `validateWhen(() => cond, () => {…})`; отдельного объекта-узла нет.
|
|
1912
2187
|
|
|
1913
2188
|
```typescript
|
|
1914
|
-
//
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2189
|
+
// УДАЛЕНО — так больше нельзя
|
|
2190
|
+
const schema = {
|
|
2191
|
+
children: [
|
|
2192
|
+
{ value: model.$.loanType, validators: [required()] },
|
|
2193
|
+
{ when: (_s, root) => root.loanType === 'mortgage', children: [ /* … */ ] },
|
|
2194
|
+
],
|
|
2195
|
+
};
|
|
2196
|
+
await validateFormModel(model, schema); // символа нет
|
|
1919
2197
|
```
|
|
1920
2198
|
|
|
1921
|
-
|
|
1922
|
-
|
|
2199
|
+
- **`applyWhen` не существует** — ни как экспорт, ни как локальный сахар: он эмитил branch-узел,
|
|
2200
|
+
которого больше нет. Оборачивай условные правила оператором `validateWhen`.
|
|
2201
|
+
|
|
2202
|
+
- **`ValidationSchemaFn` удалён** (тип старой path-схемы). Тип схемы — `ValidationSchema<T>`
|
|
2203
|
+
(`(ctx: { model: FormModel<T> }) => void`), фабрика-обёртка — `defineValidationSchema<T>(fn)`.
|
|
1923
2204
|
|
|
1924
|
-
-
|
|
1925
|
-
|
|
2205
|
+
- **Cross-field — обычная функция над снапшотом, а не `ModelValidator (value, scope, root)`.**
|
|
2206
|
+
Соседние поля читаются из снимка модели (`f: Root`, эквивалент `model.get()`), а правило навешивается
|
|
2207
|
+
оператором `cross(sig, fn)`. Каноничное использование — `complex-multy-step-form/schemas/validation.ts`.
|
|
1926
2208
|
|
|
1927
2209
|
```typescript
|
|
1928
|
-
import
|
|
2210
|
+
import { cross } from '@reformer/core/validation';
|
|
2211
|
+
import type { ValidationError } from '@reformer/core';
|
|
1929
2212
|
|
|
1930
|
-
// (
|
|
1931
|
-
const passwordsMatch
|
|
1932
|
-
|
|
2213
|
+
// (f: Root) => error — сравнение с соседним полем, без scope/root-параметров
|
|
2214
|
+
const passwordsMatch = (f: { password: string; confirmPassword: string }): ValidationError | null =>
|
|
2215
|
+
f.confirmPassword && f.password && f.confirmPassword !== f.password
|
|
1933
2216
|
? { code: 'mismatch', message: 'Пароли не совпадают' }
|
|
1934
2217
|
: null;
|
|
2218
|
+
|
|
2219
|
+
// внутри defineValidationSchema — fn получает снапшот текущего scope (model.get()):
|
|
2220
|
+
cross(model.$.confirmPassword, passwordsMatch);
|
|
1935
2221
|
```
|
|
1936
2222
|
|
|
1937
2223
|
## 24. See also
|
|
1938
2224
|
|
|
1939
|
-
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom
|
|
1940
|
-
- [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, …)
|
|
2225
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom` (поведение) и `validate`/`validateWhen`/`cross`/`validateModel` (валидация)
|
|
2226
|
+
- [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, `validateFormModel`, …)
|
|
1941
2227
|
- [25-reset-when.md](./25-reset-when.md) — `resetWhen` как альтернатива, когда поле остаётся enabled
|
|
1942
2228
|
- [19-reading-values.md](./19-reading-values.md) — `useFormControlValue` и чтение значений в React
|
|
1943
2229
|
|
|
@@ -2235,17 +2521,23 @@ aggregateInto(model.$.rows, (rows) => {
|
|
|
2235
2521
|
|
|
2236
2522
|
### Array Cross-Validation
|
|
2237
2523
|
|
|
2238
|
-
|
|
2524
|
+
Whole-array правило — оператор `cross(sig, (f) => ...)` из `@reformer/core/validation`:
|
|
2525
|
+
`f` — снапшот `model.get()`, ошибка вешается на скалярное поле-носитель `sig`. Per-item
|
|
2526
|
+
правила — `each(model.<array>, (im) => ...)`.
|
|
2239
2527
|
|
|
2240
2528
|
```typescript
|
|
2241
|
-
import
|
|
2529
|
+
import { cross } from '@reformer/core/validation';
|
|
2530
|
+
import type { ValidationError } from '@reformer/core';
|
|
2242
2531
|
|
|
2243
|
-
const percentagesSumTo100
|
|
2244
|
-
const total =
|
|
2532
|
+
const percentagesSumTo100 = (f: MyForm): ValidationError | null => {
|
|
2533
|
+
const total = f.items.reduce((sum, i) => sum + (i.percentage || 0), 0);
|
|
2245
2534
|
return Math.abs(total - 100) > 0.01
|
|
2246
2535
|
? { code: 'invalid_total', message: 'Percentages must sum to 100%' }
|
|
2247
2536
|
: null;
|
|
2248
2537
|
};
|
|
2538
|
+
|
|
2539
|
+
// внутри defineValidationSchema<MyForm>(({ model }) => { ... })
|
|
2540
|
+
cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
|
|
2249
2541
|
```
|
|
2250
2542
|
|
|
2251
2543
|
## 28. Как устроено под M1
|
|
@@ -2840,12 +3132,14 @@ A: Симптом частых записей. Форматирование с
|
|
|
2840
3132
|
`revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
|
|
2841
3133
|
зависимостей. Применяется, когда правило зависит от значений других полей
|
|
2842
3134
|
(`amount <= maxAmount`, `confirmPassword === password`, `initialPayment >= propertyValue * 0.2`).
|
|
2843
|
-
|
|
2844
|
-
колбэком, а не автоматической
|
|
3135
|
+
Валидация — отдельная функция-схема (`@reformer/core/validation`), прогоняемая по требованию через
|
|
3136
|
+
`validateModel(model, schema)`. Поэтому ревалидация выражается явным колбэком, а не автоматической
|
|
3137
|
+
привязкой к полю: поведение (`revalidateWhen`) лишь _инициирует_ прогон схемы валидации при изменении зависимости.
|
|
2845
3138
|
|
|
2846
3139
|
## 57. API
|
|
2847
3140
|
|
|
2848
|
-
|
|
3141
|
+
`revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core`).
|
|
3142
|
+
Его сигнатура НЕ зависит от контракта валидации:
|
|
2849
3143
|
|
|
2850
3144
|
```typescript
|
|
2851
3145
|
// примитив: возвращает cleanup
|
|
@@ -2856,8 +3150,8 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
|
|
|
2856
3150
|
```
|
|
2857
3151
|
|
|
2858
3152
|
`deps` — массив сигналов-триггеров (`model.$.field`). `revalidate` — колбэк (обычно
|
|
2859
|
-
`() =>
|
|
2860
|
-
(НЕ на инициализации), вне effect-контекста.
|
|
3153
|
+
`() => void validateModel(model, schema)`, где `schema` — `defineValidationSchema<T>(({ model }) => …)`).
|
|
3154
|
+
Вызывается при изменении любой зависимости (НЕ на инициализации), вне effect-контекста.
|
|
2861
3155
|
|
|
2862
3156
|
## 58. Examples
|
|
2863
3157
|
|
|
@@ -2865,27 +3159,29 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
|
|
|
2865
3159
|
|
|
2866
3160
|
```typescript
|
|
2867
3161
|
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
2868
|
-
import {
|
|
3162
|
+
import { validateModel } from '@reformer/core/validation';
|
|
2869
3163
|
|
|
2870
3164
|
export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
|
|
2871
3165
|
revalidateWhen([model.$.maxAmount], () => {
|
|
2872
|
-
void
|
|
3166
|
+
void validateModel(model, paymentSchema);
|
|
2873
3167
|
});
|
|
2874
3168
|
});
|
|
2875
3169
|
```
|
|
2876
3170
|
|
|
2877
|
-
Пример: `revalidateWhen([model.$.maxAmount], () =>
|
|
3171
|
+
Пример: `revalidateWhen([model.$.maxAmount], () => void validateModel(model, paymentSchema))`.
|
|
3172
|
+
Схема `paymentSchema` — стабильный module-level `const` (важно для отмены устаревших прогонов
|
|
3173
|
+
в `validateModel`), где правило amount навешано через `cross(model.$.amount, amountVsMax)`.
|
|
2878
3174
|
|
|
2879
3175
|
### Несколько триггеров
|
|
2880
3176
|
|
|
2881
3177
|
```typescript
|
|
2882
3178
|
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
2883
|
-
import {
|
|
3179
|
+
import { validateModel } from '@reformer/core/validation';
|
|
2884
3180
|
|
|
2885
3181
|
export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) => {
|
|
2886
3182
|
// initialPayment зависит и от propertyValue, и от loanAmount
|
|
2887
3183
|
revalidateWhen([model.$.propertyValue, model.$.loanAmount], () => {
|
|
2888
|
-
void
|
|
3184
|
+
void validateModel(model, mortgageSchema);
|
|
2889
3185
|
});
|
|
2890
3186
|
});
|
|
2891
3187
|
```
|
|
@@ -2894,13 +3190,14 @@ export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) =>
|
|
|
2894
3190
|
|
|
2895
3191
|
```typescript
|
|
2896
3192
|
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
2897
|
-
import {
|
|
3193
|
+
import { validateModel } from '@reformer/core/validation';
|
|
2898
3194
|
|
|
2899
|
-
// правило совпадения — cross-field
|
|
3195
|
+
// правило совпадения — cross-field в схеме: cross(model.$.confirmPassword, passwordsMatch),
|
|
3196
|
+
// где passwordsMatch читает снапшот формы (см. 03-api-signatures.md)
|
|
2900
3197
|
export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ model }) => {
|
|
2901
3198
|
// при смене password перевалидируем схему (confirm перепроверится)
|
|
2902
3199
|
revalidateWhen([model.$.password], () => {
|
|
2903
|
-
void
|
|
3200
|
+
void validateModel(model, registrationSchema);
|
|
2904
3201
|
});
|
|
2905
3202
|
});
|
|
2906
3203
|
```
|
|
@@ -2908,55 +3205,68 @@ export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ mode
|
|
|
2908
3205
|
### Как примитив (вне defineFormBehavior)
|
|
2909
3206
|
|
|
2910
3207
|
```typescript
|
|
2911
|
-
import { revalidateWhen
|
|
2912
|
-
|
|
3208
|
+
import { revalidateWhen } from '@reformer/core';
|
|
3209
|
+
import { validateModel } from '@reformer/core/validation';
|
|
3210
|
+
|
|
3211
|
+
const stop = revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
|
|
2913
3212
|
```
|
|
2914
3213
|
|
|
2915
3214
|
## 59. Anti-patterns
|
|
2916
3215
|
|
|
2917
3216
|
```typescript
|
|
2918
3217
|
// ❌ Триггер == поле, которое и так меняется само
|
|
2919
|
-
revalidateWhen([model.$.amount], () =>
|
|
3218
|
+
revalidateWhen([model.$.amount], () => validateModel(model, schema));
|
|
2920
3219
|
// amount и так валидируется при собственном изменении в общем прогоне
|
|
2921
3220
|
|
|
2922
3221
|
// ✅ Триггеры — ДРУГИЕ поля, от которых зависит правило amount
|
|
2923
|
-
revalidateWhen([model.$.maxAmount, model.$.discount], () =>
|
|
3222
|
+
revalidateWhen([model.$.maxAmount, model.$.discount], () => validateModel(model, schema));
|
|
2924
3223
|
```
|
|
2925
3224
|
|
|
2926
3225
|
```typescript
|
|
2927
3226
|
// ❌ Правило только через revalidateWhen, но в схеме нет зависимости от триггера
|
|
2928
|
-
revalidateWhen([model.$.maxAmount], () =>
|
|
3227
|
+
revalidateWhen([model.$.maxAmount], () => validateModel(model, schema));
|
|
2929
3228
|
// а валидатор amount — статичный max(1000), не читает maxAmount
|
|
2930
3229
|
|
|
2931
|
-
// ✅ Сначала cross-field
|
|
2932
|
-
const amountVsMax
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3230
|
+
// ✅ Сначала cross-field правило (читает снапшот формы) в схеме валидации, потом revalidateWhen
|
|
3231
|
+
const amountVsMax = (f: Form): ValidationError | null =>
|
|
3232
|
+
f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
|
|
3233
|
+
? { code: 'tooBig', message: '...' }
|
|
3234
|
+
: null;
|
|
3235
|
+
|
|
3236
|
+
const schema = defineValidationSchema<Form>(({ model }) => {
|
|
3237
|
+
cross(model.$.amount, amountVsMax); // навешиваем правило на поле amount
|
|
3238
|
+
});
|
|
3239
|
+
revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
|
|
2936
3240
|
```
|
|
2937
3241
|
|
|
2938
3242
|
## 60. Troubleshooting
|
|
2939
3243
|
|
|
2940
3244
|
**Q: Ошибка target не пропадает после изменения триггера.**
|
|
2941
|
-
A: Проверьте, что (1) правило реально читает значение триггера
|
|
2942
|
-
`
|
|
2943
|
-
`
|
|
3245
|
+
A: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, fn)`,
|
|
3246
|
+
где `fn` берёт снапшот формы (`model.get()` / `f`); (2) в `deps` передан именно сигнал (`model.$.trigger`);
|
|
3247
|
+
(3) колбэк вызывает `validateModel` (роутит ошибки в ноды через `getNodeForSignal(sig).setErrors`).
|
|
2944
3248
|
|
|
2945
3249
|
**Q: Как перевалидировать только одно поле, а не всю схему?**
|
|
2946
|
-
A:
|
|
3250
|
+
A: Схема — обычная функция, поэтому опиши стабильную под-схему только для этого поля и прогони её:
|
|
3251
|
+
`validateModel(model, oneFieldSchema)`, где
|
|
3252
|
+
`const oneFieldSchema = defineValidationSchema<Form>(({ model }) => cross(model.$.amount, amountVsMax))`.
|
|
3253
|
+
Раннер выставит/погасит ошибки только затронутых полей (гашение накапливается на пару `(model, schema)`).
|
|
2947
3254
|
|
|
2948
3255
|
## 61. See also
|
|
2949
3256
|
|
|
2950
|
-
- [03-api-signatures.md](./03-api-signatures.md) — cross-field `
|
|
3257
|
+
- [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross(sig, fn)` и раннер `validateModel`
|
|
2951
3258
|
- [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
|
|
2952
3259
|
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения
|
|
2953
3260
|
|
|
2954
3261
|
## 62. Purpose
|
|
2955
3262
|
|
|
2956
|
-
Канонический submit-флоу под M1: «запустить полную валидацию данных (`
|
|
2957
|
-
проверить `
|
|
2958
|
-
Валидация —
|
|
2959
|
-
|
|
3263
|
+
Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateModel`) →
|
|
3264
|
+
проверить `boolean`-результат → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
|
|
3265
|
+
Валидация — **отдельный слой** (`@reformer/core/validation`, схема через `defineValidationSchema`), она
|
|
3266
|
+
НЕ входит в layout-дерево, которое получает `createForm`. Раннер `validateModel(model, schema)` сам роутит
|
|
3267
|
+
ошибки в ноды формы, поэтому UI подсветит проблемные поля, а наружу вернёт лишь `boolean`
|
|
3268
|
+
(false = есть блокирующая ошибка; `severity:'warning'` показывается, но submit не блокирует).
|
|
3269
|
+
`model.reset()` возвращает значения к initial-снимку.
|
|
2960
3270
|
|
|
2961
3271
|
## 63. API
|
|
2962
3272
|
|
|
@@ -2971,13 +3281,23 @@ interface ModelApi<T> {
|
|
|
2971
3281
|
captureInitial(): void; // зафиксировать текущие как новый initial
|
|
2972
3282
|
}
|
|
2973
3283
|
|
|
2974
|
-
// Валидация данных
|
|
2975
|
-
|
|
3284
|
+
// Валидация данных — headless-раннер: находит ноды по сигналам модели, роутит в них ошибки,
|
|
3285
|
+
// отменяет устаревшие прогоны той же (model, schema). Возвращает ТОЛЬКО boolean:
|
|
3286
|
+
validateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean>
|
|
3287
|
+
// true = нет блокирующих ошибок (severity:'warning' не блокирует, но показывается).
|
|
3288
|
+
// false = есть блокирующая ошибка; она уже проставлена в ноду → UI подсветит поле.
|
|
2976
3289
|
|
|
2977
|
-
//
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3290
|
+
// Схема валидации — отдельный слой над моделью (не смешивается с layout):
|
|
3291
|
+
defineValidationSchema<T>(({ model }) => {
|
|
3292
|
+
validate(model.$.field, [rules]); // синхронные правила поля
|
|
3293
|
+
// validateAsync / validateWhen / cross / each / apply — см. contract-spec
|
|
3294
|
+
}): ValidationSchema<T>
|
|
3295
|
+
|
|
3296
|
+
// Нода поля/формы (для точечной работы с UI-состоянием):
|
|
3297
|
+
form.markAsTouched(); // тронуть все поля (показать ошибки)
|
|
3298
|
+
form.clearErrors(); // снять ошибки со всех нод
|
|
3299
|
+
form.<field>.setErrors([{ code, message }]); // серверная/бизнес-ошибка в конкретное поле
|
|
3300
|
+
form.<field>.clearErrors();
|
|
2981
3301
|
```
|
|
2982
3302
|
|
|
2983
3303
|
## 64. Examples
|
|
@@ -2986,23 +3306,34 @@ form.<field>.reset();
|
|
|
2986
3306
|
|
|
2987
3307
|
```tsx
|
|
2988
3308
|
import { useMemo } from 'react';
|
|
2989
|
-
import { createModel, createForm
|
|
3309
|
+
import { createModel, createForm } from '@reformer/core';
|
|
3310
|
+
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
3311
|
+
import { required, email, minLength } from '@reformer/core/validators';
|
|
2990
3312
|
|
|
2991
3313
|
type RegistrationFormData = { username: string; email: string; password: string };
|
|
2992
3314
|
|
|
3315
|
+
// Валидация — отдельная схема над моделью. НЕ входит в layout, который получает createForm.
|
|
3316
|
+
// Стабильная module-level `const`-ссылка — чтобы validateModel мог отменять устаревшие прогоны.
|
|
3317
|
+
const registrationSchema = defineValidationSchema<RegistrationFormData>(({ model }) => {
|
|
3318
|
+
validate(model.$.username, [required({ message: 'Имя обязательно' }), minLength(3)]);
|
|
3319
|
+
validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);
|
|
3320
|
+
validate(model.$.password, [required({ message: 'Пароль обязателен' }), minLength(8)]);
|
|
3321
|
+
});
|
|
3322
|
+
|
|
2993
3323
|
function RegistrationForm() {
|
|
2994
|
-
const { model, form
|
|
3324
|
+
const { model, form } = useMemo(() => {
|
|
2995
3325
|
const m = createModel<RegistrationFormData>({ username: '', email: '', password: '' });
|
|
2996
|
-
|
|
2997
|
-
return { model: m, form: createForm({ model: m, schema:
|
|
3326
|
+
// schema здесь — layout-дерево нод (компоненты/раскладка), без валидаторов.
|
|
3327
|
+
return { model: m, form: createForm({ model: m, schema: buildLayout(m) }) };
|
|
2998
3328
|
}, []);
|
|
2999
3329
|
|
|
3000
3330
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
3001
3331
|
e.preventDefault();
|
|
3332
|
+
form.markAsTouched(); // показать ошибки на всех полях
|
|
3002
3333
|
|
|
3003
|
-
// Шаг 1:
|
|
3004
|
-
const
|
|
3005
|
-
if (!
|
|
3334
|
+
// Шаг 1: прогон валидации данных. Ошибки автоматически проставятся в ноды → UI подсветит.
|
|
3335
|
+
const valid = await validateModel(model, registrationSchema);
|
|
3336
|
+
if (!valid) return;
|
|
3006
3337
|
|
|
3007
3338
|
// Шаг 2: достать чистые данные и отправить
|
|
3008
3339
|
const payload = model.get();
|
|
@@ -3032,9 +3363,10 @@ function RegistrationForm() {
|
|
|
3032
3363
|
```tsx
|
|
3033
3364
|
async function handleSubmit(e: React.FormEvent) {
|
|
3034
3365
|
e.preventDefault();
|
|
3366
|
+
form.markAsTouched();
|
|
3035
3367
|
|
|
3036
|
-
const
|
|
3037
|
-
if (!
|
|
3368
|
+
const valid = await validateModel(model, registrationSchema);
|
|
3369
|
+
if (!valid) return;
|
|
3038
3370
|
|
|
3039
3371
|
try {
|
|
3040
3372
|
const response = await api.register(model.get());
|
|
@@ -3078,39 +3410,61 @@ function ActionButtons({ model }: { model: FormModel<RegistrationFormData> }) {
|
|
|
3078
3410
|
|
|
3079
3411
|
### Использование вне React (server action / Node)
|
|
3080
3412
|
|
|
3081
|
-
|
|
3413
|
+
Раннер headless — работает без UI-компонентов, только с моделью и схемой:
|
|
3082
3414
|
|
|
3083
3415
|
```typescript
|
|
3084
|
-
import { createModel
|
|
3416
|
+
import { createModel } from '@reformer/core';
|
|
3417
|
+
import { validateModel } from '@reformer/core/validation';
|
|
3085
3418
|
|
|
3086
3419
|
async function processFormPayload(rawData: Partial<MyForm>) {
|
|
3087
3420
|
const model = createModel<MyForm>(initialValues);
|
|
3088
3421
|
model.patch(rawData); // частичный load данных
|
|
3089
3422
|
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
return { ok:
|
|
3423
|
+
// validateModel находит ноды по сигналам модели и роутит в них ошибки, возвращая boolean.
|
|
3424
|
+
const valid = await validateModel(model, myFormSchema);
|
|
3425
|
+
if (!valid) return { ok: false as const };
|
|
3426
|
+
return { ok: true as const, data: model.get() };
|
|
3093
3427
|
}
|
|
3094
3428
|
```
|
|
3095
3429
|
|
|
3096
3430
|
## 65. Anti-patterns
|
|
3097
3431
|
|
|
3098
3432
|
```typescript
|
|
3099
|
-
// ❌
|
|
3433
|
+
// ❌ Обращение к .valid: раннер возвращает boolean, а не { valid, errors }
|
|
3434
|
+
const result = await validateModel(model, schema);
|
|
3435
|
+
if (!result.valid) return; // result — boolean, .valid === undefined → guard никогда не сработает
|
|
3436
|
+
|
|
3437
|
+
// ✅ Результат — сам boolean
|
|
3438
|
+
const valid = await validateModel(model, schema);
|
|
3439
|
+
if (!valid) return;
|
|
3440
|
+
```
|
|
3441
|
+
|
|
3442
|
+
```typescript
|
|
3443
|
+
// ❌ Чтение результата валидации без await — сабмитим по неполному вердикту
|
|
3100
3444
|
const handleSubmit = (e) => {
|
|
3101
3445
|
e.preventDefault();
|
|
3102
|
-
|
|
3446
|
+
validateModel(model, schema); // Promise проигнорирован, async-валидаторы не дождались
|
|
3103
3447
|
submit(model.get());
|
|
3104
3448
|
};
|
|
3105
3449
|
|
|
3106
|
-
// ✅ Дождаться и проверить
|
|
3450
|
+
// ✅ Дождаться и проверить boolean
|
|
3107
3451
|
const handleSubmit = async (e) => {
|
|
3108
3452
|
e.preventDefault();
|
|
3109
|
-
|
|
3110
|
-
if (result.valid) submit(model.get());
|
|
3453
|
+
if (await validateModel(model, schema)) submit(model.get());
|
|
3111
3454
|
};
|
|
3112
3455
|
```
|
|
3113
3456
|
|
|
3457
|
+
```typescript
|
|
3458
|
+
// ❌ Валидаторы в layout-дереве, отдаваемом в createForm — layout не несёт правил
|
|
3459
|
+
createForm({ model, schema: { username: { value: '', validators: [required()] } } });
|
|
3460
|
+
|
|
3461
|
+
// ✅ Валидация — отдельная схема, прогоняется раннером
|
|
3462
|
+
const schema = defineValidationSchema<T>(({ model }) => {
|
|
3463
|
+
validate(model.$.username, [required()]);
|
|
3464
|
+
});
|
|
3465
|
+
await validateModel(model, schema);
|
|
3466
|
+
```
|
|
3467
|
+
|
|
3114
3468
|
```typescript
|
|
3115
3469
|
// ❌ model.reset() до ответа сервера — потеря данных при ошибке
|
|
3116
3470
|
await api.send(model.get());
|
|
@@ -3137,7 +3491,13 @@ model.reset();
|
|
|
3137
3491
|
|
|
3138
3492
|
**Q: После `reset()` в UI остались старые ошибки.**
|
|
3139
3493
|
A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
|
|
3140
|
-
`
|
|
3494
|
+
`validateModel(model, schema)` после reset (валидные поля погаснут сами), либо очисти напрямую —
|
|
3495
|
+
`form.clearErrors()` / `form.<field>.clearErrors()`.
|
|
3496
|
+
|
|
3497
|
+
**Q: `validateModel` вернул `true`, но в поле висит ошибка.**
|
|
3498
|
+
A: Это правило с `severity: 'warning'` — оно показывается, но submit не блокирует, поэтому раннер
|
|
3499
|
+
и вернул `true`. Блокируют только ошибки без `severity` (default). Чтобы не пускать submit при
|
|
3500
|
+
warning — проверяй его отдельно, вне `validateModel`.
|
|
3141
3501
|
|
|
3142
3502
|
**Q: `reset()` не возвращает данные, загруженные с сервера.**
|
|
3143
3503
|
A: `reset()` возвращает к initial-снимку (значения на момент `createModel`). `set/patch` НЕ
|
|
@@ -3151,8 +3511,9 @@ A: `model.get()` возвращает значения всех полей. Фи
|
|
|
3151
3511
|
## 67. See also
|
|
3152
3512
|
|
|
3153
3513
|
- [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
|
|
3154
|
-
- [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `
|
|
3155
|
-
- [
|
|
3514
|
+
- [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateModel` (`validateStep`/`validateAll`)
|
|
3515
|
+
- [27-revalidate-when.md](./27-revalidate-when.md) — мост «поведение → валидация»: `revalidateWhen([deps], () => void validateModel(model, schema))`
|
|
3516
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateModel`
|
|
3156
3517
|
- [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки
|
|
3157
3518
|
|
|
3158
3519
|
## 68. Purpose
|
|
@@ -3330,7 +3691,7 @@ useEffect(() => {
|
|
|
3330
3691
|
## 72. Troubleshooting
|
|
3331
3692
|
|
|
3332
3693
|
**Q: После `set`/`patch` не показываются ошибки.**
|
|
3333
|
-
A: `set`/`patch` не запускают валидацию. После загрузки вызови `await
|
|
3694
|
+
A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateModel(model, schema)` (внешний раннер из `@reformer/core/validation`).
|
|
3334
3695
|
|
|
3335
3696
|
**Q: Опции в `<Select>` не появляются после `updateComponentProps`.**
|
|
3336
3697
|
A: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что компонент подписан через `useFormControl`;
|
|
@@ -3353,7 +3714,8 @@ A: `reset()` возвращает к initial-снимку из `createModel`, н
|
|
|
3353
3714
|
|
|
3354
3715
|
### Recipe 1 — Imports (root cause prevention)
|
|
3355
3716
|
|
|
3356
|
-
-
|
|
3717
|
+
- Модель/форма/хуки/типы/**примитивы behaviors** — из `@reformer/core`.
|
|
3718
|
+
- Схема валидации (операторы + раннер `validateModel`) — из `@reformer/core/validation`.
|
|
3357
3719
|
- Чистые фабрики валидаторов — из `@reformer/core/validators`.
|
|
3358
3720
|
- Декларативный DSL (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
|
|
3359
3721
|
|
|
@@ -3361,12 +3723,17 @@ A: `reset()` возвращает к initial-снимку из `createModel`, н
|
|
|
3361
3723
|
import {
|
|
3362
3724
|
createModel,
|
|
3363
3725
|
createForm,
|
|
3364
|
-
validateFormModel,
|
|
3365
3726
|
type FormModel,
|
|
3366
3727
|
type FormProxy,
|
|
3367
3728
|
type ModelSignals,
|
|
3368
|
-
type ModelValidator,
|
|
3369
3729
|
} from '@reformer/core';
|
|
3730
|
+
import {
|
|
3731
|
+
defineValidationSchema,
|
|
3732
|
+
validate,
|
|
3733
|
+
cross,
|
|
3734
|
+
validateModel,
|
|
3735
|
+
type Rule,
|
|
3736
|
+
} from '@reformer/core/validation';
|
|
3370
3737
|
import { required, min, max, email } from '@reformer/core/validators';
|
|
3371
3738
|
import { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';
|
|
3372
3739
|
```
|
|
@@ -3396,38 +3763,40 @@ export type CreditApplicationForm = {
|
|
|
3396
3763
|
### Recipe 3 — Схема привязана к сигналам модели
|
|
3397
3764
|
|
|
3398
3765
|
`value` поля — это сигнал модели (`model.$.field`), а не литерал. Тип поля выводится из сигнала.
|
|
3766
|
+
Layout-схема БЕЗ валидаторов — правила живут в отдельной validation-схеме.
|
|
3399
3767
|
|
|
3400
3768
|
```typescript
|
|
3401
3769
|
const model = createModel<CreditApplicationForm>(initial);
|
|
3402
3770
|
|
|
3403
3771
|
const schema = {
|
|
3404
|
-
loanAmount: { value: model.$.loanAmount, component: Input
|
|
3772
|
+
loanAmount: { value: model.$.loanAmount, component: Input },
|
|
3405
3773
|
// вложенная группа — builder, принимающий ModelSignals<Sub>
|
|
3406
3774
|
personalData: personalDataNodes(model.$.personalData),
|
|
3407
3775
|
// массив — { array, item }
|
|
3408
3776
|
properties: { array: model.properties, item: propertyItem },
|
|
3409
3777
|
};
|
|
3778
|
+
|
|
3779
|
+
// правила — отдельно; типы полей выводятся из сигналов
|
|
3780
|
+
const validation = defineValidationSchema<CreditApplicationForm>(({ model }) => {
|
|
3781
|
+
validate(model.$.loanAmount, [required(), min(50000)]);
|
|
3782
|
+
});
|
|
3410
3783
|
```
|
|
3411
3784
|
|
|
3412
|
-
### Recipe 4 — Cross-field валидаторы:
|
|
3785
|
+
### Recipe 4 — Cross-field валидаторы: `cross` над типизированным снапшотом
|
|
3413
3786
|
|
|
3414
|
-
Правило — `
|
|
3415
|
-
читаются без `as`:
|
|
3787
|
+
Правило — обычная функция `(f: Root) => ValidationError | null` над снапшотом `model.get()`.
|
|
3788
|
+
Соседние поля читаются без `as`; ошибка вешается на поле-носитель `sig`:
|
|
3416
3789
|
|
|
3417
3790
|
```typescript
|
|
3418
|
-
import type {
|
|
3791
|
+
import type { ValidationError } from '@reformer/core';
|
|
3419
3792
|
|
|
3420
|
-
const initialPaymentVsProperty
|
|
3421
|
-
|
|
3422
|
-
_scope,
|
|
3423
|
-
root
|
|
3424
|
-
) =>
|
|
3425
|
-
root.initialPayment && root.propertyValue && root.initialPayment > root.propertyValue
|
|
3793
|
+
const initialPaymentVsProperty = (f: CreditApplicationForm): ValidationError | null =>
|
|
3794
|
+
f.initialPayment && f.propertyValue && f.initialPayment > f.propertyValue
|
|
3426
3795
|
? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }
|
|
3427
3796
|
: null;
|
|
3428
3797
|
|
|
3429
|
-
//
|
|
3430
|
-
|
|
3798
|
+
// внутри defineValidationSchema<CreditApplicationForm>(({ model }) => { ... }):
|
|
3799
|
+
cross(model.$.initialPayment, initialPaymentVsProperty);
|
|
3431
3800
|
```
|
|
3432
3801
|
|
|
3433
3802
|
### Recipe 5 — `compute` читает модель напрямую (без аннотаций)
|
|
@@ -3476,18 +3845,19 @@ function MyField<T>({ control }: MyFieldProps<T>) {
|
|
|
3476
3845
|
|
|
3477
3846
|
### Recipe 8 — Вынос правил в именованные функции
|
|
3478
3847
|
|
|
3479
|
-
|
|
3848
|
+
Field-правила — именованные `Rule<T>`, validation-схема остаётся плоской:
|
|
3480
3849
|
|
|
3481
3850
|
```typescript
|
|
3482
|
-
|
|
3851
|
+
import type { Rule } from '@reformer/core/validation';
|
|
3852
|
+
|
|
3853
|
+
const validateAdultAge: Rule<string | null> = (value) => {
|
|
3483
3854
|
if (!value) return null;
|
|
3484
3855
|
const age = new Date().getFullYear() - new Date(value).getFullYear();
|
|
3485
3856
|
return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;
|
|
3486
3857
|
};
|
|
3487
3858
|
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
};
|
|
3859
|
+
// внутри defineValidationSchema<MyForm>(({ model }) => { ... }):
|
|
3860
|
+
validate(model.$.birthDate, [validateAdultAge]);
|
|
3491
3861
|
```
|
|
3492
3862
|
|
|
3493
3863
|
### Anti-patterns to avoid
|
|
@@ -3501,76 +3871,138 @@ const schema = {
|
|
|
3501
3871
|
## 75. 31. ASYNC VALIDATOR
|
|
3502
3872
|
|
|
3503
3873
|
Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
|
|
3504
|
-
async
|
|
3505
|
-
|
|
3506
|
-
|
|
3874
|
+
async-правило это `AsyncRule<T>` = `(value, { signal }) => Promise<ValidationError | null>`.
|
|
3875
|
+
Оно регистрируется оператором `validateAsync(sig, [asyncRules])` внутри схемы валидации
|
|
3876
|
+
(`@reformer/core/validation`) и исполняется внешним раннером `validateModel` (async-правила
|
|
3877
|
+
прогоняются параллельно через `Promise.all`, раннер их дожидается).
|
|
3878
|
+
|
|
3879
|
+
Слои разделены: layout (`createForm`-схема / JSON) НЕ несёт валидаторов — правила живут в
|
|
3880
|
+
отдельной функции-схеме `defineValidationSchema<T>(({ model }) => …)`.
|
|
3507
3881
|
|
|
3508
3882
|
```ts
|
|
3509
|
-
import { createModel
|
|
3883
|
+
import { createModel } from '@reformer/core';
|
|
3884
|
+
import {
|
|
3885
|
+
validate,
|
|
3886
|
+
validateAsync,
|
|
3887
|
+
defineValidationSchema,
|
|
3888
|
+
validateModel,
|
|
3889
|
+
type AsyncRule,
|
|
3890
|
+
} from '@reformer/core/validation';
|
|
3510
3891
|
import { required, email } from '@reformer/core/validators';
|
|
3511
|
-
import { Input } from '@reformer/ui-kit';
|
|
3512
3892
|
|
|
3513
|
-
// async
|
|
3514
|
-
|
|
3893
|
+
// async-правило: (value, { signal }) => Promise<ValidationError | null>.
|
|
3894
|
+
// `signal` — AbortSignal устаревшего прогона: прокинь его в fetch, чтобы отменить in-flight.
|
|
3895
|
+
const checkEmailUnique: AsyncRule<string> = async (value, { signal }) => {
|
|
3515
3896
|
if (!value) return null; // пусто = валидно (sync `required` отдельно)
|
|
3516
3897
|
try {
|
|
3517
|
-
const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}
|
|
3898
|
+
const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
|
|
3518
3899
|
const { available } = (await res.json()) as { available: boolean };
|
|
3519
3900
|
return available ? null : { code: 'email-taken', message: 'Email уже зарегистрирован' };
|
|
3520
3901
|
} catch {
|
|
3521
|
-
return
|
|
3902
|
+
return null; // сетевой сбой/отмена НЕ блокирует submit — возвращаем null, а не ошибку
|
|
3522
3903
|
}
|
|
3523
3904
|
};
|
|
3524
3905
|
|
|
3525
3906
|
const model = createModel<{ email: string }>({ email: '' });
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
const
|
|
3907
|
+
|
|
3908
|
+
// Схема — обычная функция над моделью; sync и async — разными операторами на одном сигнале.
|
|
3909
|
+
const schema = defineValidationSchema<{ email: string }>(({ model }) => {
|
|
3910
|
+
validate(model.$.email, [required(), email()]); // sync-фабрики
|
|
3911
|
+
validateAsync(model.$.email, [checkEmailUnique]); // async-правило
|
|
3912
|
+
});
|
|
3913
|
+
|
|
3914
|
+
// Прогон по требованию (submit / шаг):
|
|
3915
|
+
const ok = await validateModel(model, schema); // Promise<boolean>
|
|
3535
3916
|
```
|
|
3536
3917
|
|
|
3537
3918
|
### Как это исполняется
|
|
3538
3919
|
|
|
3539
|
-
1. `
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
(
|
|
3543
|
-
|
|
3920
|
+
1. `validateModel(model, schema)` открывает ambient-окно и **синхронно** прогоняет схему:
|
|
3921
|
+
`validate`/`validateAsync`/`cross` регистрируют правила своих полей.
|
|
3922
|
+
2. Sync-правила выполняются сразу; async-правила из `validateAsync` собираются и после закрытия
|
|
3923
|
+
ambient-окна дожидаются параллельно (`Promise.all`) с прокинутым `AbortSignal`.
|
|
3924
|
+
3. Ошибки роутятся в ноды формы (`getNodeForSignal(sig).setErrors(...)`), UI подсвечивает поле;
|
|
3925
|
+
поля, ставшие валидными, гасятся (`setErrors([])`).
|
|
3926
|
+
4. Возвращает `Promise<boolean>` — `true`, если нет блокирующих ошибок (`severity:'warning'`
|
|
3927
|
+
не блокирует). Устаревший (отменённый) прогон возвращает `false` — ему нельзя доверять для submit.
|
|
3928
|
+
|
|
3929
|
+
Синхронного варианта у нового контракта нет: раннер один — `validateModel`, и он всегда `async`.
|
|
3544
3930
|
|
|
3545
|
-
###
|
|
3931
|
+
### Sync и async — два оператора
|
|
3546
3932
|
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3933
|
+
Разделение sync/async делается не полем схемы, а разными операторами на одном сигнале:
|
|
3934
|
+
`validate(sig, [syncRules])` и `validateAsync(sig, [asyncRules])`. Раннер прогоняет sync-правила
|
|
3935
|
+
инлайн, а async — дожидается. Держи async-правило дешёвым: ранний `if (!value) return null`
|
|
3936
|
+
пропускает сетевой вызов, пока `required()`/формат ещё не пройдены.
|
|
3937
|
+
|
|
3938
|
+
```ts
|
|
3939
|
+
validate(model.$.inn, [required(), pattern(/^\d{12}$/)]); // формат — sync, мгновенно
|
|
3940
|
+
validateAsync(model.$.inn, [checkInnInRegistry]); // обращение к API — отдельным оператором
|
|
3941
|
+
```
|
|
3550
3942
|
|
|
3551
3943
|
### UI integration
|
|
3552
3944
|
|
|
3553
|
-
`
|
|
3554
|
-
|
|
3945
|
+
`validateModel(...)` возвращает `Promise<boolean>` — индикатор проверки держи вокруг этого
|
|
3946
|
+
`await`. Раннер схемы **не** выставляет per-field `pending` на ноде (он роутит только ошибки через
|
|
3947
|
+
`setErrors`), поэтому спиннер async-валидации — это собственный флаг (обычно form-level, как
|
|
3948
|
+
`ui.pending` в submit-флоу, или локальный state компонента):
|
|
3555
3949
|
|
|
3556
3950
|
```tsx
|
|
3557
|
-
const
|
|
3558
|
-
|
|
3951
|
+
const [checking, setChecking] = useState(false);
|
|
3952
|
+
|
|
3953
|
+
const runValidation = async (): Promise<boolean> => {
|
|
3954
|
+
setChecking(true);
|
|
3955
|
+
try {
|
|
3956
|
+
return await validateModel(model, schema);
|
|
3957
|
+
} finally {
|
|
3958
|
+
setChecking(false);
|
|
3959
|
+
}
|
|
3960
|
+
};
|
|
3961
|
+
|
|
3962
|
+
return checking ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
|
|
3559
3963
|
```
|
|
3560
3964
|
|
|
3561
3965
|
### Debounce и отмена
|
|
3562
3966
|
|
|
3563
|
-
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3967
|
+
- **Отмена устаревших — раннером, бесплатно.** Быстрый повторный `validateModel(model, schema)`
|
|
3968
|
+
той же пары `(model, schema)` отменяет предыдущий in-flight прогон через `AbortController`;
|
|
3969
|
+
его `AbortSignal` прокинут в `validateAsync`-правила, поэтому `fetch(url, { signal })` рвётся
|
|
3970
|
+
сам. Ручной debounce для КОРРЕКТНОСТИ не нужен — держи схему стабильным `const`
|
|
3971
|
+
(`defineValidationSchema`), иначе отмена не сматчит прогоны по идентичности.
|
|
3972
|
+
- Валидация запускается on-demand (submit / шаг / через `revalidateWhen`), а не на каждый
|
|
3973
|
+
keystroke — отдельный `debounce` в правиле обычно не нужен.
|
|
3974
|
+
- Если нужно дебаунсить дорогой async-валидатор относительно частых изменений — триггерь прогон
|
|
3975
|
+
из поведения через `onChange`, который даёт debounce из коробки:
|
|
3976
|
+
|
|
3977
|
+
```ts
|
|
3978
|
+
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
|
|
3979
|
+
import { validateModel } from '@reformer/core/validation';
|
|
3980
|
+
|
|
3981
|
+
export const behavior = defineFormBehavior<{ username: string }>(({ model }) => {
|
|
3982
|
+
onChange(model.$.username, () => void validateModel(model, schema), { debounce: 300 });
|
|
3983
|
+
});
|
|
3984
|
+
```
|
|
3985
|
+
|
|
3986
|
+
Либо оборачивай в свой debounce колбэк `revalidateWhen([...], () => void validateModel(...))`.
|
|
3987
|
+
- **Async cross-field** — инлайн `validateAsync`-правило, замыкающее `model` и читающее снапшот
|
|
3988
|
+
соседей `model.get()` до первого `await` (у `AsyncRule` нет `root`, только `value` + `signal`;
|
|
3989
|
+
`cross(...)` — синхронный):
|
|
3990
|
+
|
|
3991
|
+
```ts
|
|
3992
|
+
validateAsync(model.$.email, [
|
|
3993
|
+
async (email, { signal }) => {
|
|
3994
|
+
const { username } = model.get(); // снапшот соседних полей до await
|
|
3995
|
+
const res = await fetch(`/api/check?email=${email}&user=${username}`, { signal });
|
|
3996
|
+
return (await res.json()).ok ? null : { code: 'conflict', message: 'Пара занята' };
|
|
3997
|
+
},
|
|
3998
|
+
]);
|
|
3999
|
+
```
|
|
3568
4000
|
|
|
3569
4001
|
### See also
|
|
3570
4002
|
|
|
3571
|
-
- [27-revalidate-when.md](27-revalidate-when.md) — перезапуск
|
|
4003
|
+
- [27-revalidate-when.md](27-revalidate-when.md) — перезапуск `validateModel` по триггерам
|
|
3572
4004
|
- [29-async-preload.md](29-async-preload.md) — async preload данных при init формы
|
|
3573
|
-
- [
|
|
4005
|
+
- [32-async-options-loading.md](32-async-options-loading.md) — `onChange` с debounce + AbortSignal
|
|
3574
4006
|
|
|
3575
4007
|
## 76. 32. ASYNC OPTIONS LOADING
|
|
3576
4008
|
|
|
@@ -4134,15 +4566,16 @@ export function createForm<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>;
|
|
|
4134
4566
|
```
|
|
4135
4567
|
|
|
4136
4568
|
**Parameters:**
|
|
4137
|
-
- `args` — - Модель данных,
|
|
4569
|
+
- `args` — - Модель данных, layout-схема (component/componentProps) и (опционально) поведение
|
|
4138
4570
|
|
|
4139
4571
|
**Returns:** Типизированная форма с Proxy-доступом к полям
|
|
4140
4572
|
|
|
4141
4573
|
**Examples:**
|
|
4142
4574
|
|
|
4143
|
-
Архитектура M1: `createModel` +
|
|
4575
|
+
Архитектура M1: `createModel` + layout-схема + `createForm({ model, schema })`
|
|
4144
4576
|
```typescript
|
|
4145
|
-
import { createModel, createForm
|
|
4577
|
+
import { createModel, createForm } from '@reformer/core';
|
|
4578
|
+
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
4146
4579
|
import { required, email, minLength } from '@reformer/core/validators';
|
|
4147
4580
|
|
|
4148
4581
|
interface UserForm {
|
|
@@ -4152,28 +4585,23 @@ password: string;
|
|
|
4152
4585
|
|
|
4153
4586
|
const model = createModel<UserForm>({ email: '', password: '' });
|
|
4154
4587
|
|
|
4588
|
+
// Layout-схема НЕ несёт validators — только component/componentProps.
|
|
4155
4589
|
const schema = {
|
|
4156
4590
|
children: [
|
|
4157
|
-
{
|
|
4158
|
-
value: model.$.
|
|
4159
|
-
component: Input,
|
|
4160
|
-
validators: [required(), email()],
|
|
4161
|
-
},
|
|
4162
|
-
{
|
|
4163
|
-
value: model.$.password,
|
|
4164
|
-
component: Input,
|
|
4165
|
-
validators: [required(), minLength(8)],
|
|
4166
|
-
},
|
|
4591
|
+
{ value: model.$.email, component: Input },
|
|
4592
|
+
{ value: model.$.password, component: Input },
|
|
4167
4593
|
],
|
|
4168
4594
|
};
|
|
4169
4595
|
|
|
4170
4596
|
const form = createForm<UserForm>({ model, schema });
|
|
4171
|
-
|
|
4172
|
-
// TypeScript знает о полях (нода привязана к сигналу модели):
|
|
4173
4597
|
form.email.setValue('test@mail.com');
|
|
4174
4598
|
|
|
4175
|
-
// Валидация
|
|
4176
|
-
const
|
|
4599
|
+
// Валидация — ОТДЕЛЬНАЯ схема, прогон внешним раннером (ошибки сами доезжают до нод):
|
|
4600
|
+
const validation = defineValidationSchema<UserForm>(({ model }) => {
|
|
4601
|
+
validate(model.$.email, [required(), email()]);
|
|
4602
|
+
validate(model.$.password, [required(), minLength(8)]);
|
|
4603
|
+
});
|
|
4604
|
+
const ok = await validateModel(model, validation);
|
|
4177
4605
|
```
|
|
4178
4606
|
|
|
4179
4607
|
_Source: src/form/create-form.ts_
|
|
@@ -4342,7 +4770,7 @@ _Source: src/form/behaviors-node.ts_
|
|
|
4342
4770
|
|
|
4343
4771
|
**Signature:**
|
|
4344
4772
|
```typescript
|
|
4345
|
-
export function email<TForm = unknown, TField extends string | undefined = string>(
|
|
4773
|
+
export function email<TForm = unknown, TField extends string | null | undefined = string>(
|
|
4346
4774
|
options?: ValidateOptions
|
|
4347
4775
|
): Validator<TForm, TField>
|
|
4348
4776
|
```
|
|
@@ -4792,6 +5220,70 @@ export type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
|
|
|
4792
5220
|
|
|
4793
5221
|
_Source: src/form/types/contracts.ts_
|
|
4794
5222
|
|
|
5223
|
+
### FileLike
|
|
5224
|
+
|
|
5225
|
+
**Kind:** `interface`
|
|
5226
|
+
|
|
5227
|
+
Минимальный файлоподобный контракт, с которым работают file-валидаторы:
|
|
5228
|
+
нативный `File`, сериализуемый дескриптор загруженного файла и т.п.
|
|
5229
|
+
|
|
5230
|
+
**Signature:**
|
|
5231
|
+
```typescript
|
|
5232
|
+
export interface FileLike {
|
|
5233
|
+
name: string;
|
|
5234
|
+
size?: number;
|
|
5235
|
+
type?: string;
|
|
5236
|
+
}
|
|
5237
|
+
```
|
|
5238
|
+
|
|
5239
|
+
_Source: src/form/validation/validators/file-utils.ts_
|
|
5240
|
+
|
|
5241
|
+
### fileType
|
|
5242
|
+
|
|
5243
|
+
**Kind:** `function`
|
|
5244
|
+
|
|
5245
|
+
Фабрика валидатора типа файла.
|
|
5246
|
+
|
|
5247
|
+
Проверяет каждый файл против accept-строки в синтаксисе нативного атрибута
|
|
5248
|
+
`<input type="file" accept>`: расширения (`.pdf`), точные MIME (`image/png`),
|
|
5249
|
+
wildcard-категории (`image/*`), список через запятую. Матчинг регистронезависимый,
|
|
5250
|
+
по `type` и/или расширению из `name` ({@link matchesFileAccept}).
|
|
5251
|
+
|
|
5252
|
+
Нужен потому, что нативный `accept` — только подсказка пикеру: на drag-and-drop
|
|
5253
|
+
и «All files» он не действует. Пустые значения (`null`/`undefined`/`''`/`[]`)
|
|
5254
|
+
пропускаются (используйте {@link required} для обязательности).
|
|
5255
|
+
|
|
5256
|
+
**Signature:**
|
|
5257
|
+
```typescript
|
|
5258
|
+
export function fileType<TForm = unknown, TField = unknown>(
|
|
5259
|
+
accept: string,
|
|
5260
|
+
options?: ValidateOptions
|
|
5261
|
+
): Validator<TForm, TField>
|
|
5262
|
+
```
|
|
5263
|
+
|
|
5264
|
+
**Parameters:**
|
|
5265
|
+
- `accept` — - Accept-строка допустимых типов, например `'image/*,.pdf'`
|
|
5266
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
5267
|
+
попадают `accept` и `fileName` (первый нарушивший файл).
|
|
5268
|
+
|
|
5269
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
5270
|
+
|
|
5271
|
+
**Examples:**
|
|
5272
|
+
|
|
5273
|
+
Только изображения и PDF
|
|
5274
|
+
```typescript
|
|
5275
|
+
import { fileType } from '@reformer/core/validators';
|
|
5276
|
+
|
|
5277
|
+
// Внутри FieldConfig схемы формы:
|
|
5278
|
+
documents: {
|
|
5279
|
+
value: model.$.documents,
|
|
5280
|
+
component: FileUploadField,
|
|
5281
|
+
validators: [fileType('image/*,.pdf', { message: 'Только изображения или PDF' })],
|
|
5282
|
+
},
|
|
5283
|
+
```
|
|
5284
|
+
|
|
5285
|
+
_Source: src/form/validation/validators/file-type.ts_
|
|
5286
|
+
|
|
4795
5287
|
### FormArrayProxy
|
|
4796
5288
|
|
|
4797
5289
|
**Kind:** `type`
|
|
@@ -5143,8 +5635,8 @@ _Source: src/form/types/deep-schema.ts_
|
|
|
5143
5635
|
|
|
5144
5636
|
**Kind:** `interface`
|
|
5145
5637
|
|
|
5146
|
-
Узел единой схемы M1 —
|
|
5147
|
-
|
|
5638
|
+
Узел единой схемы M1 — layout-дерево, обходимое `createForm({ model, schema })`
|
|
5639
|
+
и рендерерами (schema-валидация живёт отдельно — `@reformer/core/validation`).
|
|
5148
5640
|
|
|
5149
5641
|
Узел совмещает несколько ролей (различаются рантаймом по форме):
|
|
5150
5642
|
- **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
|
|
@@ -5164,8 +5656,12 @@ export interface FormSchemaNode {
|
|
|
5164
5656
|
* `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
|
|
5165
5657
|
*/
|
|
5166
5658
|
value?: unknown;
|
|
5167
|
-
/**
|
|
5168
|
-
|
|
5659
|
+
/**
|
|
5660
|
+
* UI-компонент либо нативный HTML-тег (`'div'`, `'p'`, `'h3'`) для презентационной вёрстки
|
|
5661
|
+
* прямо в схеме. Опционален: core-часть работает без UI (значение/валидация) и `component`
|
|
5662
|
+
* не интерпретирует — он доезжает до рендерера как есть.
|
|
5663
|
+
*/
|
|
5664
|
+
component?: ElementType;
|
|
5169
5665
|
/** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
|
|
5170
5666
|
componentProps?: Record<string, unknown>;
|
|
5171
5667
|
validators?: SchemaValidator[];
|
|
@@ -5301,7 +5797,7 @@ _Source: src/form/types/contracts.ts_
|
|
|
5301
5797
|
```typescript
|
|
5302
5798
|
export function futureDate<
|
|
5303
5799
|
TForm = unknown,
|
|
5304
|
-
TField extends string | Date | undefined = string | Date,
|
|
5800
|
+
TField extends string | Date | null | undefined = string | Date,
|
|
5305
5801
|
>(options?: ValidateOptions): Validator<TForm, TField>
|
|
5306
5802
|
```
|
|
5307
5803
|
|
|
@@ -5394,8 +5890,8 @@ _Source: src/form/type-guards.ts_
|
|
|
5394
5890
|
GroupNode - узел для группы полей
|
|
5395
5891
|
|
|
5396
5892
|
Создаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`
|
|
5397
|
-
(M1: `createForm({ model, schema })`);
|
|
5398
|
-
(`
|
|
5893
|
+
(M1: `createForm({ model, schema })`); schema-валидация/behavior живут на слое модели
|
|
5894
|
+
(`validateModel` из `@reformer/core/validation`, `computeFrom`/`enableWhen`/…), а не на ноде.
|
|
5399
5895
|
|
|
5400
5896
|
**Signature:**
|
|
5401
5897
|
```typescript
|
|
@@ -5519,9 +6015,10 @@ _Source: src/form/type-guards.ts_
|
|
|
5519
6015
|
|
|
5520
6016
|
**Signature:**
|
|
5521
6017
|
```typescript
|
|
5522
|
-
export function isDate<
|
|
5523
|
-
|
|
5524
|
-
|
|
6018
|
+
export function isDate<
|
|
6019
|
+
TForm = unknown,
|
|
6020
|
+
TField extends string | Date | null | undefined = string | Date,
|
|
6021
|
+
>(options?: ValidateOptions): Validator<TForm, TField>
|
|
5525
6022
|
```
|
|
5526
6023
|
|
|
5527
6024
|
**Parameters:**
|
|
@@ -5610,6 +6107,25 @@ if (isFieldNode(node)) {
|
|
|
5610
6107
|
|
|
5611
6108
|
_Source: src/form/type-guards.ts_
|
|
5612
6109
|
|
|
6110
|
+
### isFileLike
|
|
6111
|
+
|
|
6112
|
+
**Kind:** `function`
|
|
6113
|
+
|
|
6114
|
+
Проверяет, что значение файлоподобно: объект со строковым `name` и опциональными
|
|
6115
|
+
числовым `size` / строковым `type`.
|
|
6116
|
+
|
|
6117
|
+
**Signature:**
|
|
6118
|
+
```typescript
|
|
6119
|
+
export function isFileLike(value: unknown): value is FileLike
|
|
6120
|
+
```
|
|
6121
|
+
|
|
6122
|
+
**Parameters:**
|
|
6123
|
+
- `value` — - Проверяемое значение
|
|
6124
|
+
|
|
6125
|
+
**Returns:** `true`, если значение соответствует {@link FileLike}
|
|
6126
|
+
|
|
6127
|
+
_Source: src/form/validation/validators/file-utils.ts_
|
|
6128
|
+
|
|
5613
6129
|
### isFormNode
|
|
5614
6130
|
|
|
5615
6131
|
**Kind:** `function`
|
|
@@ -5742,6 +6258,30 @@ markDerived(model.$.total);
|
|
|
5742
6258
|
|
|
5743
6259
|
_Source: src/state/derived-registry.ts_
|
|
5744
6260
|
|
|
6261
|
+
### matchesFileAccept
|
|
6262
|
+
|
|
6263
|
+
**Kind:** `function`
|
|
6264
|
+
|
|
6265
|
+
Проверяет файл против accept-паттерна в синтаксисе нативного атрибута
|
|
6266
|
+
`<input type="file" accept>`: список через запятую из расширений (`.pdf`),
|
|
6267
|
+
точных MIME (`image/png`) и wildcard-категорий (`image/*`). Регистронезависимо.
|
|
6268
|
+
|
|
6269
|
+
Нужен собственный матчер, потому что `accept` у нативного input — только подсказка
|
|
6270
|
+
пикеру: на drag-and-drop и на программное значение он не действует.
|
|
6271
|
+
|
|
6272
|
+
**Signature:**
|
|
6273
|
+
```typescript
|
|
6274
|
+
export function matchesFileAccept(file: Pick<FileLike, 'name' | 'type'>, accept: string): boolean
|
|
6275
|
+
```
|
|
6276
|
+
|
|
6277
|
+
**Parameters:**
|
|
6278
|
+
- `file` — - Файлоподобное значение (`name` + опциональный `type`)
|
|
6279
|
+
- `accept` — - Accept-строка; пустая/пробельная строка матчит всё
|
|
6280
|
+
|
|
6281
|
+
**Returns:** `true`, если файл подходит хотя бы под один паттерн
|
|
6282
|
+
|
|
6283
|
+
_Source: src/form/validation/validators/file-utils.ts_
|
|
6284
|
+
|
|
5745
6285
|
### max
|
|
5746
6286
|
|
|
5747
6287
|
**Kind:** `function`
|
|
@@ -5862,6 +6402,90 @@ validators: [maxDate(new Date(), { message: 'Дата не может быть
|
|
|
5862
6402
|
|
|
5863
6403
|
_Source: src/form/validation/validators/max-date.ts_
|
|
5864
6404
|
|
|
6405
|
+
### maxFiles
|
|
6406
|
+
|
|
6407
|
+
**Kind:** `function`
|
|
6408
|
+
|
|
6409
|
+
Фабрика валидатора максимального количества файлов.
|
|
6410
|
+
|
|
6411
|
+
Работает с массивом файлов (одиночный файл считается как 1). Пустые значения
|
|
6412
|
+
(`null`/`undefined`/`''`/`[]`) пропускаются (используйте {@link required}
|
|
6413
|
+
для обязательности).
|
|
6414
|
+
|
|
6415
|
+
**Signature:**
|
|
6416
|
+
```typescript
|
|
6417
|
+
export function maxFiles<TForm = unknown, TField = unknown>(
|
|
6418
|
+
max: number,
|
|
6419
|
+
options?: ValidateOptions
|
|
6420
|
+
): Validator<TForm, TField>
|
|
6421
|
+
```
|
|
6422
|
+
|
|
6423
|
+
**Parameters:**
|
|
6424
|
+
- `max` — - Максимально допустимое количество файлов (включительно)
|
|
6425
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6426
|
+
попадают `maxFiles` и `actualCount`.
|
|
6427
|
+
|
|
6428
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6429
|
+
|
|
6430
|
+
**Examples:**
|
|
6431
|
+
|
|
6432
|
+
Не более трёх вложений
|
|
6433
|
+
```typescript
|
|
6434
|
+
import { maxFiles } from '@reformer/core/validators';
|
|
6435
|
+
|
|
6436
|
+
// Внутри FieldConfig схемы формы:
|
|
6437
|
+
documents: {
|
|
6438
|
+
value: model.$.documents,
|
|
6439
|
+
component: FileUploadField,
|
|
6440
|
+
validators: [maxFiles(3, { message: 'Максимум 3 файла' })],
|
|
6441
|
+
},
|
|
6442
|
+
```
|
|
6443
|
+
|
|
6444
|
+
_Source: src/form/validation/validators/max-files.ts_
|
|
6445
|
+
|
|
6446
|
+
### maxFileSize
|
|
6447
|
+
|
|
6448
|
+
**Kind:** `function`
|
|
6449
|
+
|
|
6450
|
+
Фабрика валидатора максимального размера файла.
|
|
6451
|
+
|
|
6452
|
+
Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
|
|
6453
|
+
`name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
|
|
6454
|
+
Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
|
|
6455
|
+
(например, дескриптор загруженного файла без размера) пропускаются
|
|
6456
|
+
(используйте {@link required} для обязательности).
|
|
6457
|
+
|
|
6458
|
+
**Signature:**
|
|
6459
|
+
```typescript
|
|
6460
|
+
export function maxFileSize<TForm = unknown, TField = unknown>(
|
|
6461
|
+
maxSize: number,
|
|
6462
|
+
options?: ValidateOptions
|
|
6463
|
+
): Validator<TForm, TField>
|
|
6464
|
+
```
|
|
6465
|
+
|
|
6466
|
+
**Parameters:**
|
|
6467
|
+
- `maxSize` — - Максимально допустимый размер файла в байтах (включительно)
|
|
6468
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6469
|
+
попадают `maxFileSize`, `fileName` и `actualSize`.
|
|
6470
|
+
|
|
6471
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6472
|
+
|
|
6473
|
+
**Examples:**
|
|
6474
|
+
|
|
6475
|
+
Ограничение размера вложений
|
|
6476
|
+
```typescript
|
|
6477
|
+
import { maxFileSize } from '@reformer/core/validators';
|
|
6478
|
+
|
|
6479
|
+
// Внутри FieldConfig схемы формы:
|
|
6480
|
+
documents: {
|
|
6481
|
+
value: model.$.documents,
|
|
6482
|
+
component: FileUploadField,
|
|
6483
|
+
validators: [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })],
|
|
6484
|
+
},
|
|
6485
|
+
```
|
|
6486
|
+
|
|
6487
|
+
_Source: src/form/validation/validators/max-file-size.ts_
|
|
6488
|
+
|
|
5865
6489
|
### maxLength
|
|
5866
6490
|
|
|
5867
6491
|
**Kind:** `function`
|
|
@@ -5904,6 +6528,47 @@ validators: [maxLength(500, { message: 'Максимум 500 символов' }
|
|
|
5904
6528
|
|
|
5905
6529
|
_Source: src/form/validation/validators/max-length.ts_
|
|
5906
6530
|
|
|
6531
|
+
### maxTotalFileSize
|
|
6532
|
+
|
|
6533
|
+
**Kind:** `function`
|
|
6534
|
+
|
|
6535
|
+
Фабрика валидатора суммарного размера всех файлов.
|
|
6536
|
+
|
|
6537
|
+
Складывает `size` всех файлов значения (элементы без числового `size` в сумму
|
|
6538
|
+
не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
|
|
6539
|
+
(используйте {@link required} для обязательности).
|
|
6540
|
+
|
|
6541
|
+
**Signature:**
|
|
6542
|
+
```typescript
|
|
6543
|
+
export function maxTotalFileSize<TForm = unknown, TField = unknown>(
|
|
6544
|
+
maxTotal: number,
|
|
6545
|
+
options?: ValidateOptions
|
|
6546
|
+
): Validator<TForm, TField>
|
|
6547
|
+
```
|
|
6548
|
+
|
|
6549
|
+
**Parameters:**
|
|
6550
|
+
- `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
|
|
6551
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6552
|
+
попадают `maxTotalFileSize` и `actualTotal`.
|
|
6553
|
+
|
|
6554
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6555
|
+
|
|
6556
|
+
**Examples:**
|
|
6557
|
+
|
|
6558
|
+
Суммарно не более 20 МБ
|
|
6559
|
+
```typescript
|
|
6560
|
+
import { maxTotalFileSize } from '@reformer/core/validators';
|
|
6561
|
+
|
|
6562
|
+
// Внутри FieldConfig схемы формы:
|
|
6563
|
+
documents: {
|
|
6564
|
+
value: model.$.documents,
|
|
6565
|
+
component: FileUploadField,
|
|
6566
|
+
validators: [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })],
|
|
6567
|
+
},
|
|
6568
|
+
```
|
|
6569
|
+
|
|
6570
|
+
_Source: src/form/validation/validators/max-total-file-size.ts_
|
|
6571
|
+
|
|
5907
6572
|
### min
|
|
5908
6573
|
|
|
5909
6574
|
**Kind:** `function`
|
|
@@ -6024,6 +6689,90 @@ validators: [required(), minDate(new Date(), { message: 'Дата не рань
|
|
|
6024
6689
|
|
|
6025
6690
|
_Source: src/form/validation/validators/min-date.ts_
|
|
6026
6691
|
|
|
6692
|
+
### minFiles
|
|
6693
|
+
|
|
6694
|
+
**Kind:** `function`
|
|
6695
|
+
|
|
6696
|
+
Фабрика валидатора минимального количества файлов.
|
|
6697
|
+
|
|
6698
|
+
Работает с массивом файлов (одиночный файл считается как 1). Пустые значения
|
|
6699
|
+
(`null`/`undefined`/`''`/`[]`) пропускаются — «нужен хотя бы один файл» выражается
|
|
6700
|
+
связкой `required()` + `minFiles(n)`, как и у {@link minLength}.
|
|
6701
|
+
|
|
6702
|
+
**Signature:**
|
|
6703
|
+
```typescript
|
|
6704
|
+
export function minFiles<TForm = unknown, TField = unknown>(
|
|
6705
|
+
min: number,
|
|
6706
|
+
options?: ValidateOptions
|
|
6707
|
+
): Validator<TForm, TField>
|
|
6708
|
+
```
|
|
6709
|
+
|
|
6710
|
+
**Parameters:**
|
|
6711
|
+
- `min` — - Минимально допустимое количество файлов (включительно)
|
|
6712
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6713
|
+
попадают `minFiles` и `actualCount`.
|
|
6714
|
+
|
|
6715
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6716
|
+
|
|
6717
|
+
**Examples:**
|
|
6718
|
+
|
|
6719
|
+
Не менее двух документов
|
|
6720
|
+
```typescript
|
|
6721
|
+
import { required, minFiles } from '@reformer/core/validators';
|
|
6722
|
+
|
|
6723
|
+
// Внутри FieldConfig схемы формы:
|
|
6724
|
+
documents: {
|
|
6725
|
+
value: model.$.documents,
|
|
6726
|
+
component: FileUploadField,
|
|
6727
|
+
validators: [required(), minFiles(2, { message: 'Приложите минимум 2 документа' })],
|
|
6728
|
+
},
|
|
6729
|
+
```
|
|
6730
|
+
|
|
6731
|
+
_Source: src/form/validation/validators/min-files.ts_
|
|
6732
|
+
|
|
6733
|
+
### minFileSize
|
|
6734
|
+
|
|
6735
|
+
**Kind:** `function`
|
|
6736
|
+
|
|
6737
|
+
Фабрика валидатора минимального размера файла.
|
|
6738
|
+
|
|
6739
|
+
Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
|
|
6740
|
+
`name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
|
|
6741
|
+
Типовой сценарий — отсев пустых (0-байтовых) файлов: `minFileSize(1)`.
|
|
6742
|
+
Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
|
|
6743
|
+
пропускаются (используйте {@link required} для обязательности).
|
|
6744
|
+
|
|
6745
|
+
**Signature:**
|
|
6746
|
+
```typescript
|
|
6747
|
+
export function minFileSize<TForm = unknown, TField = unknown>(
|
|
6748
|
+
minSize: number,
|
|
6749
|
+
options?: ValidateOptions
|
|
6750
|
+
): Validator<TForm, TField>
|
|
6751
|
+
```
|
|
6752
|
+
|
|
6753
|
+
**Parameters:**
|
|
6754
|
+
- `minSize` — - Минимально допустимый размер файла в байтах (включительно)
|
|
6755
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6756
|
+
попадают `minFileSize`, `fileName` и `actualSize`.
|
|
6757
|
+
|
|
6758
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6759
|
+
|
|
6760
|
+
**Examples:**
|
|
6761
|
+
|
|
6762
|
+
Отсев пустых файлов
|
|
6763
|
+
```typescript
|
|
6764
|
+
import { minFileSize } from '@reformer/core/validators';
|
|
6765
|
+
|
|
6766
|
+
// Внутри FieldConfig схемы формы:
|
|
6767
|
+
documents: {
|
|
6768
|
+
value: model.$.documents,
|
|
6769
|
+
component: FileUploadField,
|
|
6770
|
+
validators: [minFileSize(1, { message: 'Файл пустой' })],
|
|
6771
|
+
},
|
|
6772
|
+
```
|
|
6773
|
+
|
|
6774
|
+
_Source: src/form/validation/validators/min-file-size.ts_
|
|
6775
|
+
|
|
6027
6776
|
### minLength
|
|
6028
6777
|
|
|
6029
6778
|
**Kind:** `function`
|
|
@@ -6256,44 +7005,6 @@ export type ModelSignals<T> = {
|
|
|
6256
7005
|
|
|
6257
7006
|
_Source: src/state/types.ts_
|
|
6258
7007
|
|
|
6259
|
-
### ModelValidationResult
|
|
6260
|
-
|
|
6261
|
-
**Kind:** `interface`
|
|
6262
|
-
|
|
6263
|
-
Результат валидации: ошибки по пути поля.
|
|
6264
|
-
|
|
6265
|
-
**Signature:**
|
|
6266
|
-
```typescript
|
|
6267
|
-
export interface ModelValidationResult {
|
|
6268
|
-
valid: boolean;
|
|
6269
|
-
/** Ключ — путь поля (`'coBorrowers.0.relationship'`). Только поля с ошибками. */
|
|
6270
|
-
errors: Record<string, ValidationError[]>;
|
|
6271
|
-
}
|
|
6272
|
-
```
|
|
6273
|
-
|
|
6274
|
-
_Source: src/form/validate-model-core.ts_
|
|
6275
|
-
|
|
6276
|
-
### ModelValidator
|
|
6277
|
-
|
|
6278
|
-
**Kind:** `type`
|
|
6279
|
-
|
|
6280
|
-
Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
|
|
6281
|
-
массива или корень); `root` — корневая модель.
|
|
6282
|
-
|
|
6283
|
-
Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
|
|
6284
|
-
`FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
|
|
6285
|
-
|
|
6286
|
-
**Signature:**
|
|
6287
|
-
```typescript
|
|
6288
|
-
export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
|
|
6289
|
-
value: TValue,
|
|
6290
|
-
model: TModel,
|
|
6291
|
-
root: TRoot
|
|
6292
|
-
) => ValidationError | null | Promise<ValidationError | null>;
|
|
6293
|
-
```
|
|
6294
|
-
|
|
6295
|
-
_Source: src/form/validate-model-core.ts_
|
|
6296
|
-
|
|
6297
7008
|
### ModelValue
|
|
6298
7009
|
|
|
6299
7010
|
**Kind:** `type`
|
|
@@ -6511,9 +7222,10 @@ _Source: src/form/validation/validators/non-zero.ts_
|
|
|
6511
7222
|
|
|
6512
7223
|
**Signature:**
|
|
6513
7224
|
```typescript
|
|
6514
|
-
export function pastDate<
|
|
6515
|
-
|
|
6516
|
-
|
|
7225
|
+
export function pastDate<
|
|
7226
|
+
TForm = unknown,
|
|
7227
|
+
TField extends string | Date | null | undefined = string | Date,
|
|
7228
|
+
>(options?: ValidateOptions): Validator<TForm, TField>
|
|
6517
7229
|
```
|
|
6518
7230
|
|
|
6519
7231
|
**Parameters:**
|
|
@@ -6565,7 +7277,7 @@ _Source: src/state/types.ts_
|
|
|
6565
7277
|
|
|
6566
7278
|
**Signature:**
|
|
6567
7279
|
```typescript
|
|
6568
|
-
export function pattern<TForm = unknown, TField extends string | undefined = string>(
|
|
7280
|
+
export function pattern<TForm = unknown, TField extends string | null | undefined = string>(
|
|
6569
7281
|
regex: RegExp,
|
|
6570
7282
|
options?: ValidateOptions
|
|
6571
7283
|
): Validator<TForm, TField>
|
|
@@ -6613,7 +7325,7 @@ _Source: src/form/validation/validators/pattern.ts_
|
|
|
6613
7325
|
|
|
6614
7326
|
**Signature:**
|
|
6615
7327
|
```typescript
|
|
6616
|
-
export function phone<TForm = unknown, TField extends string | undefined = string>(
|
|
7328
|
+
export function phone<TForm = unknown, TField extends string | null | undefined = string>(
|
|
6617
7329
|
options?: PhoneValidatorOptions
|
|
6618
7330
|
): Validator<TForm, TField>
|
|
6619
7331
|
```
|
|
@@ -6778,8 +7490,8 @@ _Source: src/form/types/index.ts_
|
|
|
6778
7490
|
|
|
6779
7491
|
**Kind:** `function`
|
|
6780
7492
|
|
|
6781
|
-
Вызывает `revalidate()` при изменении зависимостей (не на инициализации).
|
|
6782
|
-
|
|
7493
|
+
Вызывает `revalidate()` при изменении зависимостей (не на инициализации). Валидация on-demand
|
|
7494
|
+
(`validateModel` из `@reformer/core/validation`), поэтому ревалидация выражается явным колбэком.
|
|
6783
7495
|
|
|
6784
7496
|
**Signature:**
|
|
6785
7497
|
```typescript
|
|
@@ -6792,7 +7504,7 @@ export function revalidateWhen(
|
|
|
6792
7504
|
**Examples:**
|
|
6793
7505
|
|
|
6794
7506
|
```typescript
|
|
6795
|
-
revalidateWhen([model.$.maxAmount], () =>
|
|
7507
|
+
revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
|
|
6796
7508
|
```
|
|
6797
7509
|
|
|
6798
7510
|
_Source: src/state/behaviors-value.ts_
|
|
@@ -6935,10 +7647,10 @@ _Source: src/form/types/schema-node.ts_
|
|
|
6935
7647
|
|
|
6936
7648
|
**Kind:** `type`
|
|
6937
7649
|
|
|
6938
|
-
Валидатор поля
|
|
6939
|
-
`(value, scope, root)`,
|
|
6940
|
-
`
|
|
6941
|
-
|
|
7650
|
+
Валидатор поля схемы (legacy). Дерево-движок, вызывавший `validators` узла как
|
|
7651
|
+
`(value, scope, root)`, удалён — живые правила это `Rule<T>` в `validate(sig, [...])`
|
|
7652
|
+
(`@reformer/core/validation`). Тип остаётся широким по параметрам для совместимости
|
|
7653
|
+
узлов, всё ещё несущих `validators` (гетерогенные контракты `ValidatorFn`/`Validator`).
|
|
6942
7654
|
|
|
6943
7655
|
**Signature:**
|
|
6944
7656
|
```typescript
|
|
@@ -7113,6 +7825,27 @@ syncFields(model.$.field1, model.$.field2);
|
|
|
7113
7825
|
|
|
7114
7826
|
_Source: src/state/behaviors-value.ts_
|
|
7115
7827
|
|
|
7828
|
+
### toFileArray
|
|
7829
|
+
|
|
7830
|
+
**Kind:** `function`
|
|
7831
|
+
|
|
7832
|
+
Нормализует значение file-поля в массив файлоподобных элементов.
|
|
7833
|
+
|
|
7834
|
+
Одиночный {@link FileLike} оборачивается в массив; из массива отбираются только
|
|
7835
|
+
файлоподобные элементы. Для прочих значений — `null` (валидатор пропускает).
|
|
7836
|
+
|
|
7837
|
+
**Signature:**
|
|
7838
|
+
```typescript
|
|
7839
|
+
export function toFileArray(value: unknown): FileLike[] | null
|
|
7840
|
+
```
|
|
7841
|
+
|
|
7842
|
+
**Parameters:**
|
|
7843
|
+
- `value` — - Значение поля (`FileLike`, `FileLike[]` или что-то ещё)
|
|
7844
|
+
|
|
7845
|
+
**Returns:** Массив файлоподобных элементов или `null`
|
|
7846
|
+
|
|
7847
|
+
_Source: src/form/validation/validators/file-utils.ts_
|
|
7848
|
+
|
|
7116
7849
|
### transformValue
|
|
7117
7850
|
|
|
7118
7851
|
**Kind:** `function`
|
|
@@ -7253,7 +7986,7 @@ _Source: src/state/derived-registry.ts_
|
|
|
7253
7986
|
|
|
7254
7987
|
**Signature:**
|
|
7255
7988
|
```typescript
|
|
7256
|
-
export function url<TForm = unknown, TField extends string | undefined = string>(
|
|
7989
|
+
export function url<TForm = unknown, TField extends string | null | undefined = string>(
|
|
7257
7990
|
options?: UrlValidatorOptions
|
|
7258
7991
|
): Validator<TForm, TField>
|
|
7259
7992
|
```
|
|
@@ -7756,94 +8489,6 @@ export interface ValidateAsyncOptions extends ValidateOptions {
|
|
|
7756
8489
|
|
|
7757
8490
|
_Source: src/form/types/validation-schema.ts_
|
|
7758
8491
|
|
|
7759
|
-
### validateFormModel
|
|
7760
|
-
|
|
7761
|
-
**Kind:** `function`
|
|
7762
|
-
|
|
7763
|
-
In-form валидация: прогоняет {@link validateModel} и роутит ошибки в ноды формы
|
|
7764
|
-
(через реестр сигнал→нода): `node.setErrors(errors[path] ?? [])` — заодно очищает прошедшие поля
|
|
7765
|
-
И поля выключенных веток (`{ when, children }` с ложным условием). Дерево обходится ОДИН раз.
|
|
7766
|
-
|
|
7767
|
-
**Signature:**
|
|
7768
|
-
```typescript
|
|
7769
|
-
export async function validateFormModel<T>(
|
|
7770
|
-
model: FormModel<T>,
|
|
7771
|
-
schema: FormSchemaNode
|
|
7772
|
-
): Promise<ModelValidationResult>
|
|
7773
|
-
```
|
|
7774
|
-
|
|
7775
|
-
**Parameters:**
|
|
7776
|
-
- `model` — - Модель данных ({@link FormModel}) — источник значений полей.
|
|
7777
|
-
- `schema` — - Единая схема формы (та же, что передавалась в `createForm`).
|
|
7778
|
-
|
|
7779
|
-
**Returns:**
|
|
7780
|
-
|
|
7781
|
-
**Examples:**
|
|
7782
|
-
|
|
7783
|
-
Валидация перед submit (ошибки показываются в UI автоматически)
|
|
7784
|
-
```typescript
|
|
7785
|
-
const form = createForm({ model, schema });
|
|
7786
|
-
form.touchAll();
|
|
7787
|
-
const res = await validateFormModel(model, schema);
|
|
7788
|
-
if (res.valid) await api.save(model.get());
|
|
7789
|
-
```
|
|
7790
|
-
|
|
7791
|
-
_Source: src/form/validate-model.ts_
|
|
7792
|
-
|
|
7793
|
-
### validateModel
|
|
7794
|
-
|
|
7795
|
-
**Kind:** `function`
|
|
7796
|
-
|
|
7797
|
-
Headless-валидация данных (sync + async). Работает без UI/нод.
|
|
7798
|
-
|
|
7799
|
-
**Signature:**
|
|
7800
|
-
```typescript
|
|
7801
|
-
export async function validateModel<T>(
|
|
7802
|
-
model: FormModel<T>,
|
|
7803
|
-
schema: FormSchemaNode
|
|
7804
|
-
): Promise<ModelValidationResult>
|
|
7805
|
-
```
|
|
7806
|
-
|
|
7807
|
-
**Examples:**
|
|
7808
|
-
|
|
7809
|
-
```typescript
|
|
7810
|
-
const res = await validateModel(model, schema);
|
|
7811
|
-
if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
|
|
7812
|
-
```
|
|
7813
|
-
|
|
7814
|
-
_Source: src/form/validate-model-core.ts_
|
|
7815
|
-
|
|
7816
|
-
### validateModelSync
|
|
7817
|
-
|
|
7818
|
-
**Kind:** `function`
|
|
7819
|
-
|
|
7820
|
-
Синхронная headless-валидация данных. Асинхронные валидаторы пропускаются.
|
|
7821
|
-
|
|
7822
|
-
**Signature:**
|
|
7823
|
-
```typescript
|
|
7824
|
-
export function validateModelSync<T>(
|
|
7825
|
-
model: FormModel<T>,
|
|
7826
|
-
schema: FormSchemaNode
|
|
7827
|
-
): ModelValidationResult
|
|
7828
|
-
```
|
|
7829
|
-
|
|
7830
|
-
**Parameters:**
|
|
7831
|
-
- `model` — - Модель данных ({@link FormModel}), из сигналов которой читаются значения полей.
|
|
7832
|
-
- `schema` — - Единая схема формы (дерево узлов с `value`/`validators`, `{ when, children }`,
|
|
7833
|
-
секциями массивов). Обходится один раз для сбора активных полей.
|
|
7834
|
-
|
|
7835
|
-
**Returns:**
|
|
7836
|
-
|
|
7837
|
-
**Examples:**
|
|
7838
|
-
|
|
7839
|
-
Быстрая синхронная проверка (например, для gate «можно ли перейти на след. шаг»)
|
|
7840
|
-
```typescript
|
|
7841
|
-
const res = validateModelSync(model, schema);
|
|
7842
|
-
if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
|
|
7843
|
-
```
|
|
7844
|
-
|
|
7845
|
-
_Source: src/form/validate-model-core.ts_
|
|
7846
|
-
|
|
7847
8492
|
### ValidateOptions
|
|
7848
8493
|
|
|
7849
8494
|
**Kind:** `interface`
|
|
@@ -7887,20 +8532,7 @@ _Source: src/form/types/contracts.ts_
|
|
|
7887
8532
|
|
|
7888
8533
|
**Kind:** `type`
|
|
7889
8534
|
|
|
7890
|
-
Чистый синхронный валидатор
|
|
7891
|
-
|
|
7892
|
-
Сигнатура зеркалит то, что реально вызывает движок M1
|
|
7893
|
-
(`validateModel`/`validateFormModel`/`validateModelSync`): `validator(value, scope, root)`.
|
|
7894
|
-
- `value` — значение поля (`TField`);
|
|
7895
|
-
- `scope` — ближайшая scope-**модель** (под-модель элемента массива или корень). Из `TForm`/`TField`
|
|
7896
|
-
её тип не выводится, поэтому `unknown` — потребитель сужает сам (для типизированного scope
|
|
7897
|
-
используйте {@link ModelValidator}`<TField, TScope, TForm>`);
|
|
7898
|
-
- `root` — корневая модель формы `FormModel<TForm>` (реактивный value-proxy: `root.field` читает
|
|
7899
|
-
значение). Возвращает `ValidationError` либо `null`. Не знает про реестр валидации.
|
|
7900
|
-
|
|
7901
|
-
Совместим с полем `validators` узла схемы (см. `SchemaValidator`) — там же лежат `ModelValidator`
|
|
7902
|
-
и `ValidatorFn`. Встроенные фабрики (`required()`/`email()`/…) возвращают `(value) => …` и
|
|
7903
|
-
дополнительные аргументы игнорируют.
|
|
8535
|
+
Чистый синхронный валидатор поля (legacy-сигнатура `(value, scope, root)`).
|
|
7904
8536
|
|
|
7905
8537
|
**Signature:**
|
|
7906
8538
|
```typescript
|
|
@@ -7911,23 +8543,12 @@ export type Validator<TForm, TField> = (
|
|
|
7911
8543
|
) => ValidationError | null;
|
|
7912
8544
|
```
|
|
7913
8545
|
|
|
7914
|
-
**
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
// root — FormModel<MyForm>: root.someOtherField читается как значение
|
|
7921
|
-
return null;
|
|
7922
|
-
};
|
|
7923
|
-
|
|
7924
|
-
// Фабрики и кастомные валидаторы кладутся в `validators: [...]` поля:
|
|
7925
|
-
const schema = {
|
|
7926
|
-
children: [
|
|
7927
|
-
{ value: model.$.age, component: Input, validators: [required(), isAdult] },
|
|
7928
|
-
],
|
|
7929
|
-
};
|
|
7930
|
-
```
|
|
8546
|
+
**Deprecated:** Осиротевший остаток удалённого дерево-движка (`validateFormModel`). Живой контракт —
|
|
8547
|
+
`Rule<T> = (value) => ValidationError | null` из `@reformer/core/validation`: правила
|
|
8548
|
+
передаются в `validate(sig, [rules])` внутри `defineValidationSchema` и запускаются
|
|
8549
|
+
`validateModel(model, schema)`. Cross-field — оператор `cross(sig, (f) => …)` над снапшотом
|
|
8550
|
+
`model.get()` (третий аргумент `root` больше не нужен). Тип экспортируется только ради
|
|
8551
|
+
обратной совместимости (`SchemaValidator`-union).
|
|
7931
8552
|
|
|
7932
8553
|
_Source: src/form/types/validation-schema.ts_
|
|
7933
8554
|
|