@reformer/core 9.1.1 → 10.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1198 -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
|
│
|
|
@@ -1720,22 +1948,28 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
|
|
|
1720
1948
|
|
|
1721
1949
|
## 18. 15. NON-EXISTENT API (DO NOT USE)
|
|
1722
1950
|
|
|
1723
|
-
**Следующего API НЕТ в @reformer/core** (
|
|
1724
|
-
удалено при переходе на M1):
|
|
1951
|
+
**Следующего API НЕТ в @reformer/core** (наследие старой path-based архитектуры,
|
|
1952
|
+
удалено при переходе на M1 и при разделении валидации/поведения):
|
|
1953
|
+
|
|
1954
|
+
> ⚠️ Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`
|
|
1955
|
+
> **существуют** — но живут в сабпути `@reformer/core/validation` (ambient-схема
|
|
1956
|
+
> `defineValidationSchema(({ model }) => …)`, раннер `validateModel`), а НЕ в корне и НЕ
|
|
1957
|
+
> в `@reformer/core/validators`. Ниже перечислено то, чего действительно нет.
|
|
1725
1958
|
|
|
1726
1959
|
| Wrong | Correct | Notes |
|
|
1727
1960
|
| -------------------------------- | ------------------------------------------------ | ---------------------------------------------- |
|
|
1728
1961
|
| `useForm` | `createModel` + `createForm` | Хука useForm нет |
|
|
1729
|
-
| `
|
|
1730
|
-
| `
|
|
1731
|
-
| `
|
|
1732
|
-
| `
|
|
1733
|
-
| `ValidationSchemaFn`, `BehaviorSchemaFn` | `
|
|
1962
|
+
| `validateForm`, `validateFormModel` | `validateModel(model, schema)` из `@reformer/core/validation` | Legacy-движок дерева `{ value, validators }` удалён; внешний раннер — `validateModel` |
|
|
1963
|
+
| `validateModelSync` | `await validateModel(model, schema)` | Синхронного раннера нет — прогон асинхронный (`Promise<boolean>`) |
|
|
1964
|
+
| `applyWhen` | `validateWhen(() => cond, () => { … })` | Условная валидация — оператором `validateWhen`, не узлом `{ when, children }` |
|
|
1965
|
+
| `validateItems`, `validateGroup`, `validateTree` | `each(model.arr, (im) => { … })` | Per-item — оператором `each`; под-схема — прямой вызов `sub({ model: model.child })` |
|
|
1966
|
+
| `ValidationSchemaFn`, `BehaviorSchemaFn` | `ValidationSchema<T>` (`defineValidationSchema`) / `defineFormBehavior` | Типы path-схем удалены |
|
|
1967
|
+
| `equalTo`, `custom`, `notEmpty` (validators) | inline `Rule<T>` `(value) => err \| null` в `validate(sig, [...])`; сравнение полей — `cross(sig, fn)` | Таких фабрик нет; кастомное правило — обычная функция значения, cross-field — `cross` |
|
|
1968
|
+
| `form.submit()` / `form.validate()` прогоняют схему | `await validateModel(model, schema)` перед submit/шагом | submit/validate НЕ запускают ambient-схему валидации |
|
|
1734
1969
|
| `FieldPath`, `FieldPathNode` | `model.$.field` (`PathAwareSignal`) | Пути заменены сигналами |
|
|
1735
1970
|
| `ctx.form.x.value.value` | `model.x` / `model.$.x.value` | В behaviors читаем модель напрямую |
|
|
1736
1971
|
| `ctx.setFieldValue(name, value)` | `model.x = value` / `compute(...)` | Не существует |
|
|
1737
1972
|
| `transformers`, `createTransformer` | `transformValue(signal, fn)` | Готового набора трансформеров нет |
|
|
1738
|
-
| `equalTo`, `custom`, `notEmpty`, `date` (validators) | кастомный `ModelValidator` | Таких фабрик нет; пиши функцию `(value, scope, root)` |
|
|
1739
1973
|
| `useHiddenCondition` | `useFormControlValue` + условный рендер в JSX | Хука нет |
|
|
1740
1974
|
| `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
|
|
1741
1975
|
| `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |
|
|
@@ -1743,33 +1977,49 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
|
|
|
1743
1977
|
### Common Import Errors
|
|
1744
1978
|
|
|
1745
1979
|
```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';
|
|
1980
|
+
// WRONG - этих символов / путей НЕ существует
|
|
1981
|
+
import { useForm, validateForm, validateFormModel } from '@reformer/core'; // NO!
|
|
1982
|
+
import { validate, applyWhen, equalTo } from '@reformer/core/validators'; // NO! операторов тут нет
|
|
1983
|
+
import { transformers } from '@reformer/core/behaviors'; // NO!
|
|
1984
|
+
import type { FieldPath, ValidationSchemaFn } from '@reformer/core'; // NO!
|
|
1751
1985
|
|
|
1752
1986
|
// CORRECT
|
|
1753
|
-
import { createModel, createForm,
|
|
1754
|
-
import {
|
|
1755
|
-
|
|
1756
|
-
import
|
|
1987
|
+
import { createModel, createForm, useFormControl } from '@reformer/core';
|
|
1988
|
+
import type { FormModel, ValidationError, FieldConfig } from '@reformer/core';
|
|
1989
|
+
// Операторы валидации + раннер + defineValidationSchema — отдельный сабпуть:
|
|
1990
|
+
import {
|
|
1991
|
+
validate, validateAsync, validateWhen, cross, each, apply,
|
|
1992
|
+
defineValidationSchema, validateModel,
|
|
1993
|
+
type Rule, type AsyncRule, type ValidationSchema,
|
|
1994
|
+
} from '@reformer/core/validation';
|
|
1995
|
+
// Фабрики-валидаторы (value-only, принимают nullable):
|
|
1996
|
+
import { required, email, min, minLength, pattern } from '@reformer/core/validators';
|
|
1997
|
+
// Поведение (отдельный слой, контракт не менялся):
|
|
1998
|
+
import { defineFormBehavior, compute, onChange, revalidateWhen } from '@reformer/core/behaviors';
|
|
1757
1999
|
```
|
|
1758
2000
|
|
|
1759
2001
|
### Schema Common Mistakes
|
|
1760
2002
|
|
|
2003
|
+
Валидация и layout — **разные** контракты. Layout-узел привязывает поле к сигналу и
|
|
2004
|
+
НЕ несёт `validators`; правила живут в отдельной `defineValidationSchema`.
|
|
2005
|
+
|
|
1761
2006
|
```typescript
|
|
1762
|
-
// WRONG -
|
|
2007
|
+
// WRONG - примитив/литерал вместо сигнала, и validators прямо в layout-узле
|
|
1763
2008
|
const schema = {
|
|
1764
|
-
name:
|
|
1765
|
-
email: { value: '',
|
|
2009
|
+
name: '', // нет привязки к сигналу
|
|
2010
|
+
email: { value: '', validators: [required()] }, // value — литерал, а validators в layout больше нет
|
|
1766
2011
|
};
|
|
1767
2012
|
|
|
1768
|
-
// CORRECT -
|
|
1769
|
-
const
|
|
2013
|
+
// CORRECT - layout только связывает поле с сигналом модели...
|
|
2014
|
+
const layout = {
|
|
1770
2015
|
name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
|
|
1771
2016
|
email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
|
|
1772
2017
|
};
|
|
2018
|
+
|
|
2019
|
+
// ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
|
|
2020
|
+
const validation = defineValidationSchema<Form>(({ model }) => {
|
|
2021
|
+
validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);
|
|
2022
|
+
});
|
|
1773
2023
|
```
|
|
1774
2024
|
|
|
1775
2025
|
### Behaviors Common Mistakes
|
|
@@ -1782,6 +2032,14 @@ enableWhen(path.city, (form) => Boolean(form.country));
|
|
|
1782
2032
|
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
|
|
1783
2033
|
```
|
|
1784
2034
|
|
|
2035
|
+
Поведение **не владеет** валидацией. Чтобы поведение инициировало прогон схемы — мост
|
|
2036
|
+
`revalidateWhen` (validate/submit сами схему не запускают):
|
|
2037
|
+
|
|
2038
|
+
```typescript
|
|
2039
|
+
// CORRECT - поведение дёргает внешний раннер валидации при изменении зависимости
|
|
2040
|
+
revalidateWhen([model.$.password], () => void validateModel(model, validation));
|
|
2041
|
+
```
|
|
2042
|
+
|
|
1785
2043
|
## 19. Purpose
|
|
1786
2044
|
|
|
1787
2045
|
«Показать поле X только если Y» под M1 решается тремя независимыми механизмами — выбирай по
|
|
@@ -1789,12 +2047,13 @@ enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true })
|
|
|
1789
2047
|
|
|
1790
2048
|
1. **Видимость / доступность** (поле остаётся в модели, но выключается/включается и не валидируется)
|
|
1791
2049
|
— behaviors: `enableWhen`, а для условной записи значения — `compute` / `copyFrom` с опцией `{ when }`.
|
|
1792
|
-
2. **Условная валидация** (правила применяются только в истинной ветке) —
|
|
1793
|
-
|
|
2050
|
+
2. **Условная валидация** (правила применяются только в истинной ветке) — оператор
|
|
2051
|
+
`validateWhen(() => cond, () => {…})` внутри схемы `defineValidationSchema`, прогоняется раннером `validateModel`.
|
|
1794
2052
|
3. **Скрытие из разметки** (узел вообще не рендерится) — `useFormControlValue` + условный рендер в JSX.
|
|
1795
2053
|
|
|
1796
|
-
>
|
|
1797
|
-
>
|
|
2054
|
+
> Условная **валидация** — оператор `validateWhen` из `@reformer/core/validation`; старого branch-узла
|
|
2055
|
+
> `{ when, children }` и хелпера `applyWhen` больше нет — см.
|
|
2056
|
+
> [Anti-patterns удалённого контракта валидации](#anti-patterns-удалённого-контракта-валидации) и `17-nonexistent-api.md`.
|
|
1798
2057
|
|
|
1799
2058
|
## 20. Видимость и доступность
|
|
1800
2059
|
|
|
@@ -1850,35 +2109,42 @@ export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
|
|
|
1850
2109
|
|
|
1851
2110
|
## 21. Условная валидация
|
|
1852
2111
|
|
|
1853
|
-
Правила, действующие только при выполнении условия,
|
|
1854
|
-
`
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2112
|
+
Правила, действующие только при выполнении условия, оборачиваются оператором
|
|
2113
|
+
`validateWhen(() => cond, () => {…})` внутри схемы `defineValidationSchema<T>(({ model }) => …)`.
|
|
2114
|
+
Условие `cond` читает модель напрямую (`model.field` — снимок на момент прогона): при истинном условии
|
|
2115
|
+
раннер применяет вложенные `validate`/`cross`/…, при ложном — правила ветки **гасятся** (поля,
|
|
2116
|
+
затронутые ранее, получают `setErrors([])`). Прогон — внешним раннером
|
|
2117
|
+
`validateModel(model, schema): Promise<boolean>` (ошибки сами роутятся в ноды формы).
|
|
1858
2118
|
|
|
1859
2119
|
```typescript
|
|
1860
|
-
import {
|
|
2120
|
+
import {
|
|
2121
|
+
validate,
|
|
2122
|
+
validateWhen,
|
|
2123
|
+
defineValidationSchema,
|
|
2124
|
+
validateModel,
|
|
2125
|
+
} from '@reformer/core/validation';
|
|
1861
2126
|
import { required, min } from '@reformer/core/validators';
|
|
1862
2127
|
|
|
1863
|
-
const schema = {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
],
|
|
2128
|
+
const schema = defineValidationSchema<CreditForm>(({ model }) => {
|
|
2129
|
+
validate(model.$.loanType, [required()]);
|
|
2130
|
+
|
|
2131
|
+
// ветка применяется только для ипотеки
|
|
2132
|
+
validateWhen(
|
|
2133
|
+
() => model.loanType === 'mortgage',
|
|
2134
|
+
() => {
|
|
2135
|
+
validate(model.$.propertyValue, [required(), min(1_000_000)]);
|
|
2136
|
+
validate(model.$.initialPayment, [required()]);
|
|
1873
2137
|
},
|
|
1874
|
-
|
|
1875
|
-
};
|
|
2138
|
+
);
|
|
2139
|
+
});
|
|
1876
2140
|
|
|
1877
|
-
const
|
|
2141
|
+
const ok: boolean = await validateModel(model, schema);
|
|
1878
2142
|
```
|
|
1879
2143
|
|
|
1880
2144
|
Переключение `loanType` с `mortgage` на другой тип автоматически снимает ошибки `required` с
|
|
1881
|
-
`propertyValue`/`initialPayment` —
|
|
2145
|
+
`propertyValue`/`initialPayment` — при ложном условии ветка `validateWhen` гасит их (`setErrors([])`).
|
|
2146
|
+
`validateWhen` управляет только **валидацией**; чтобы поля ещё и выпадали из состояния (сброс
|
|
2147
|
+
значения), держи их под `enableWhen` из раздела выше — слои независимы.
|
|
1882
2148
|
|
|
1883
2149
|
## 22. Скрытие в JSX
|
|
1884
2150
|
|
|
@@ -1902,42 +2168,57 @@ function MortgageFields({ form }: { form: CreditForm }) {
|
|
|
1902
2168
|
```
|
|
1903
2169
|
|
|
1904
2170
|
Скрытие в JSX — чисто визуальное: если поле должно ещё и **выпадать из валидации/состояния**,
|
|
1905
|
-
сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или
|
|
2171
|
+
сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или `validateWhen` (валидация).
|
|
2172
|
+
|
|
2173
|
+
## 23. Anti-patterns удалённого контракта валидации
|
|
1906
2174
|
|
|
1907
|
-
|
|
2175
|
+
Условную валидацию раньше собирали иначе; эти формы удалены при переходе на `@reformer/core/validation`.
|
|
2176
|
+
Ниже — **как НЕ надо** (чтобы не регрессировать); рабочий вариант — `validateWhen` из раздела выше.
|
|
1908
2177
|
|
|
1909
|
-
-
|
|
1910
|
-
|
|
1911
|
-
`
|
|
2178
|
+
- **Нет branch-узла `{ when, children }` в дереве схемы.** Старая схема была деревом
|
|
2179
|
+
`{ value, validators: [...] }` с ветками `{ when: (scope, root) => boolean, children: [...] }`,
|
|
2180
|
+
которое обходил `validateFormModel`. Теперь схема — функция `({ model }) => void`, а условие —
|
|
2181
|
+
оператор `validateWhen(() => cond, () => {…})`; отдельного объекта-узла нет.
|
|
1912
2182
|
|
|
1913
2183
|
```typescript
|
|
1914
|
-
//
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2184
|
+
// УДАЛЕНО — так больше нельзя
|
|
2185
|
+
const schema = {
|
|
2186
|
+
children: [
|
|
2187
|
+
{ value: model.$.loanType, validators: [required()] },
|
|
2188
|
+
{ when: (_s, root) => root.loanType === 'mortgage', children: [ /* … */ ] },
|
|
2189
|
+
],
|
|
2190
|
+
};
|
|
2191
|
+
await validateFormModel(model, schema); // символа нет
|
|
1919
2192
|
```
|
|
1920
2193
|
|
|
1921
|
-
|
|
1922
|
-
|
|
2194
|
+
- **`applyWhen` не существует** — ни как экспорт, ни как локальный сахар: он эмитил branch-узел,
|
|
2195
|
+
которого больше нет. Оборачивай условные правила оператором `validateWhen`.
|
|
1923
2196
|
|
|
1924
|
-
- **`ValidationSchemaFn`
|
|
1925
|
-
|
|
2197
|
+
- **`ValidationSchemaFn` удалён** (тип старой path-схемы). Тип схемы — `ValidationSchema<T>`
|
|
2198
|
+
(`(ctx: { model: FormModel<T> }) => void`), фабрика-обёртка — `defineValidationSchema<T>(fn)`.
|
|
2199
|
+
|
|
2200
|
+
- **Cross-field — обычная функция над снапшотом, а не `ModelValidator (value, scope, root)`.**
|
|
2201
|
+
Соседние поля читаются из снимка модели (`f: Root`, эквивалент `model.get()`), а правило навешивается
|
|
2202
|
+
оператором `cross(sig, fn)`. Каноничное использование — `complex-multy-step-form/schemas/validation.ts`.
|
|
1926
2203
|
|
|
1927
2204
|
```typescript
|
|
1928
|
-
import
|
|
2205
|
+
import { cross } from '@reformer/core/validation';
|
|
2206
|
+
import type { ValidationError } from '@reformer/core';
|
|
1929
2207
|
|
|
1930
|
-
// (
|
|
1931
|
-
const passwordsMatch
|
|
1932
|
-
|
|
2208
|
+
// (f: Root) => error — сравнение с соседним полем, без scope/root-параметров
|
|
2209
|
+
const passwordsMatch = (f: { password: string; confirmPassword: string }): ValidationError | null =>
|
|
2210
|
+
f.confirmPassword && f.password && f.confirmPassword !== f.password
|
|
1933
2211
|
? { code: 'mismatch', message: 'Пароли не совпадают' }
|
|
1934
2212
|
: null;
|
|
2213
|
+
|
|
2214
|
+
// внутри defineValidationSchema — fn получает снапшот текущего scope (model.get()):
|
|
2215
|
+
cross(model.$.confirmPassword, passwordsMatch);
|
|
1935
2216
|
```
|
|
1936
2217
|
|
|
1937
2218
|
## 24. See also
|
|
1938
2219
|
|
|
1939
|
-
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom
|
|
1940
|
-
- [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, …)
|
|
2220
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom` (поведение) и `validate`/`validateWhen`/`cross`/`validateModel` (валидация)
|
|
2221
|
+
- [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, `validateFormModel`, …)
|
|
1941
2222
|
- [25-reset-when.md](./25-reset-when.md) — `resetWhen` как альтернатива, когда поле остаётся enabled
|
|
1942
2223
|
- [19-reading-values.md](./19-reading-values.md) — `useFormControlValue` и чтение значений в React
|
|
1943
2224
|
|
|
@@ -2235,17 +2516,23 @@ aggregateInto(model.$.rows, (rows) => {
|
|
|
2235
2516
|
|
|
2236
2517
|
### Array Cross-Validation
|
|
2237
2518
|
|
|
2238
|
-
|
|
2519
|
+
Whole-array правило — оператор `cross(sig, (f) => ...)` из `@reformer/core/validation`:
|
|
2520
|
+
`f` — снапшот `model.get()`, ошибка вешается на скалярное поле-носитель `sig`. Per-item
|
|
2521
|
+
правила — `each(model.<array>, (im) => ...)`.
|
|
2239
2522
|
|
|
2240
2523
|
```typescript
|
|
2241
|
-
import
|
|
2524
|
+
import { cross } from '@reformer/core/validation';
|
|
2525
|
+
import type { ValidationError } from '@reformer/core';
|
|
2242
2526
|
|
|
2243
|
-
const percentagesSumTo100
|
|
2244
|
-
const total =
|
|
2527
|
+
const percentagesSumTo100 = (f: MyForm): ValidationError | null => {
|
|
2528
|
+
const total = f.items.reduce((sum, i) => sum + (i.percentage || 0), 0);
|
|
2245
2529
|
return Math.abs(total - 100) > 0.01
|
|
2246
2530
|
? { code: 'invalid_total', message: 'Percentages must sum to 100%' }
|
|
2247
2531
|
: null;
|
|
2248
2532
|
};
|
|
2533
|
+
|
|
2534
|
+
// внутри defineValidationSchema<MyForm>(({ model }) => { ... })
|
|
2535
|
+
cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
|
|
2249
2536
|
```
|
|
2250
2537
|
|
|
2251
2538
|
## 28. Как устроено под M1
|
|
@@ -2840,12 +3127,14 @@ A: Симптом частых записей. Форматирование с
|
|
|
2840
3127
|
`revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
|
|
2841
3128
|
зависимостей. Применяется, когда правило зависит от значений других полей
|
|
2842
3129
|
(`amount <= maxAmount`, `confirmPassword === password`, `initialPayment >= propertyValue * 0.2`).
|
|
2843
|
-
|
|
2844
|
-
колбэком, а не автоматической
|
|
3130
|
+
Валидация — отдельная функция-схема (`@reformer/core/validation`), прогоняемая по требованию через
|
|
3131
|
+
`validateModel(model, schema)`. Поэтому ревалидация выражается явным колбэком, а не автоматической
|
|
3132
|
+
привязкой к полю: поведение (`revalidateWhen`) лишь _инициирует_ прогон схемы валидации при изменении зависимости.
|
|
2845
3133
|
|
|
2846
3134
|
## 57. API
|
|
2847
3135
|
|
|
2848
|
-
|
|
3136
|
+
`revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core`).
|
|
3137
|
+
Его сигнатура НЕ зависит от контракта валидации:
|
|
2849
3138
|
|
|
2850
3139
|
```typescript
|
|
2851
3140
|
// примитив: возвращает cleanup
|
|
@@ -2856,8 +3145,8 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
|
|
|
2856
3145
|
```
|
|
2857
3146
|
|
|
2858
3147
|
`deps` — массив сигналов-триггеров (`model.$.field`). `revalidate` — колбэк (обычно
|
|
2859
|
-
`() =>
|
|
2860
|
-
(НЕ на инициализации), вне effect-контекста.
|
|
3148
|
+
`() => void validateModel(model, schema)`, где `schema` — `defineValidationSchema<T>(({ model }) => …)`).
|
|
3149
|
+
Вызывается при изменении любой зависимости (НЕ на инициализации), вне effect-контекста.
|
|
2861
3150
|
|
|
2862
3151
|
## 58. Examples
|
|
2863
3152
|
|
|
@@ -2865,27 +3154,29 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
|
|
|
2865
3154
|
|
|
2866
3155
|
```typescript
|
|
2867
3156
|
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
2868
|
-
import {
|
|
3157
|
+
import { validateModel } from '@reformer/core/validation';
|
|
2869
3158
|
|
|
2870
3159
|
export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
|
|
2871
3160
|
revalidateWhen([model.$.maxAmount], () => {
|
|
2872
|
-
void
|
|
3161
|
+
void validateModel(model, paymentSchema);
|
|
2873
3162
|
});
|
|
2874
3163
|
});
|
|
2875
3164
|
```
|
|
2876
3165
|
|
|
2877
|
-
Пример: `revalidateWhen([model.$.maxAmount], () =>
|
|
3166
|
+
Пример: `revalidateWhen([model.$.maxAmount], () => void validateModel(model, paymentSchema))`.
|
|
3167
|
+
Схема `paymentSchema` — стабильный module-level `const` (важно для отмены устаревших прогонов
|
|
3168
|
+
в `validateModel`), где правило amount навешано через `cross(model.$.amount, amountVsMax)`.
|
|
2878
3169
|
|
|
2879
3170
|
### Несколько триггеров
|
|
2880
3171
|
|
|
2881
3172
|
```typescript
|
|
2882
3173
|
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
2883
|
-
import {
|
|
3174
|
+
import { validateModel } from '@reformer/core/validation';
|
|
2884
3175
|
|
|
2885
3176
|
export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) => {
|
|
2886
3177
|
// initialPayment зависит и от propertyValue, и от loanAmount
|
|
2887
3178
|
revalidateWhen([model.$.propertyValue, model.$.loanAmount], () => {
|
|
2888
|
-
void
|
|
3179
|
+
void validateModel(model, mortgageSchema);
|
|
2889
3180
|
});
|
|
2890
3181
|
});
|
|
2891
3182
|
```
|
|
@@ -2894,13 +3185,14 @@ export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) =>
|
|
|
2894
3185
|
|
|
2895
3186
|
```typescript
|
|
2896
3187
|
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
2897
|
-
import {
|
|
3188
|
+
import { validateModel } from '@reformer/core/validation';
|
|
2898
3189
|
|
|
2899
|
-
// правило совпадения — cross-field
|
|
3190
|
+
// правило совпадения — cross-field в схеме: cross(model.$.confirmPassword, passwordsMatch),
|
|
3191
|
+
// где passwordsMatch читает снапшот формы (см. 03-api-signatures.md)
|
|
2900
3192
|
export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ model }) => {
|
|
2901
3193
|
// при смене password перевалидируем схему (confirm перепроверится)
|
|
2902
3194
|
revalidateWhen([model.$.password], () => {
|
|
2903
|
-
void
|
|
3195
|
+
void validateModel(model, registrationSchema);
|
|
2904
3196
|
});
|
|
2905
3197
|
});
|
|
2906
3198
|
```
|
|
@@ -2908,55 +3200,68 @@ export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ mode
|
|
|
2908
3200
|
### Как примитив (вне defineFormBehavior)
|
|
2909
3201
|
|
|
2910
3202
|
```typescript
|
|
2911
|
-
import { revalidateWhen
|
|
2912
|
-
|
|
3203
|
+
import { revalidateWhen } from '@reformer/core';
|
|
3204
|
+
import { validateModel } from '@reformer/core/validation';
|
|
3205
|
+
|
|
3206
|
+
const stop = revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
|
|
2913
3207
|
```
|
|
2914
3208
|
|
|
2915
3209
|
## 59. Anti-patterns
|
|
2916
3210
|
|
|
2917
3211
|
```typescript
|
|
2918
3212
|
// ❌ Триггер == поле, которое и так меняется само
|
|
2919
|
-
revalidateWhen([model.$.amount], () =>
|
|
3213
|
+
revalidateWhen([model.$.amount], () => validateModel(model, schema));
|
|
2920
3214
|
// amount и так валидируется при собственном изменении в общем прогоне
|
|
2921
3215
|
|
|
2922
3216
|
// ✅ Триггеры — ДРУГИЕ поля, от которых зависит правило amount
|
|
2923
|
-
revalidateWhen([model.$.maxAmount, model.$.discount], () =>
|
|
3217
|
+
revalidateWhen([model.$.maxAmount, model.$.discount], () => validateModel(model, schema));
|
|
2924
3218
|
```
|
|
2925
3219
|
|
|
2926
3220
|
```typescript
|
|
2927
3221
|
// ❌ Правило только через revalidateWhen, но в схеме нет зависимости от триггера
|
|
2928
|
-
revalidateWhen([model.$.maxAmount], () =>
|
|
3222
|
+
revalidateWhen([model.$.maxAmount], () => validateModel(model, schema));
|
|
2929
3223
|
// а валидатор amount — статичный max(1000), не читает maxAmount
|
|
2930
3224
|
|
|
2931
|
-
// ✅ Сначала cross-field
|
|
2932
|
-
const amountVsMax
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3225
|
+
// ✅ Сначала cross-field правило (читает снапшот формы) в схеме валидации, потом revalidateWhen
|
|
3226
|
+
const amountVsMax = (f: Form): ValidationError | null =>
|
|
3227
|
+
f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
|
|
3228
|
+
? { code: 'tooBig', message: '...' }
|
|
3229
|
+
: null;
|
|
3230
|
+
|
|
3231
|
+
const schema = defineValidationSchema<Form>(({ model }) => {
|
|
3232
|
+
cross(model.$.amount, amountVsMax); // навешиваем правило на поле amount
|
|
3233
|
+
});
|
|
3234
|
+
revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
|
|
2936
3235
|
```
|
|
2937
3236
|
|
|
2938
3237
|
## 60. Troubleshooting
|
|
2939
3238
|
|
|
2940
3239
|
**Q: Ошибка target не пропадает после изменения триггера.**
|
|
2941
|
-
A: Проверьте, что (1) правило реально читает значение триггера
|
|
2942
|
-
`
|
|
2943
|
-
`
|
|
3240
|
+
A: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, fn)`,
|
|
3241
|
+
где `fn` берёт снапшот формы (`model.get()` / `f`); (2) в `deps` передан именно сигнал (`model.$.trigger`);
|
|
3242
|
+
(3) колбэк вызывает `validateModel` (роутит ошибки в ноды через `getNodeForSignal(sig).setErrors`).
|
|
2944
3243
|
|
|
2945
3244
|
**Q: Как перевалидировать только одно поле, а не всю схему?**
|
|
2946
|
-
A:
|
|
3245
|
+
A: Схема — обычная функция, поэтому опиши стабильную под-схему только для этого поля и прогони её:
|
|
3246
|
+
`validateModel(model, oneFieldSchema)`, где
|
|
3247
|
+
`const oneFieldSchema = defineValidationSchema<Form>(({ model }) => cross(model.$.amount, amountVsMax))`.
|
|
3248
|
+
Раннер выставит/погасит ошибки только затронутых полей (гашение накапливается на пару `(model, schema)`).
|
|
2947
3249
|
|
|
2948
3250
|
## 61. See also
|
|
2949
3251
|
|
|
2950
|
-
- [03-api-signatures.md](./03-api-signatures.md) — cross-field `
|
|
3252
|
+
- [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross(sig, fn)` и раннер `validateModel`
|
|
2951
3253
|
- [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
|
|
2952
3254
|
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения
|
|
2953
3255
|
|
|
2954
3256
|
## 62. Purpose
|
|
2955
3257
|
|
|
2956
|
-
Канонический submit-флоу под M1: «запустить полную валидацию данных (`
|
|
2957
|
-
проверить `
|
|
2958
|
-
Валидация —
|
|
2959
|
-
|
|
3258
|
+
Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateModel`) →
|
|
3259
|
+
проверить `boolean`-результат → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
|
|
3260
|
+
Валидация — **отдельный слой** (`@reformer/core/validation`, схема через `defineValidationSchema`), она
|
|
3261
|
+
НЕ входит в layout-дерево, которое получает `createForm`. Раннер `validateModel(model, schema)` сам роутит
|
|
3262
|
+
ошибки в ноды формы, поэтому UI подсветит проблемные поля, а наружу вернёт лишь `boolean`
|
|
3263
|
+
(false = есть блокирующая ошибка; `severity:'warning'` показывается, но submit не блокирует).
|
|
3264
|
+
`model.reset()` возвращает значения к initial-снимку.
|
|
2960
3265
|
|
|
2961
3266
|
## 63. API
|
|
2962
3267
|
|
|
@@ -2971,13 +3276,23 @@ interface ModelApi<T> {
|
|
|
2971
3276
|
captureInitial(): void; // зафиксировать текущие как новый initial
|
|
2972
3277
|
}
|
|
2973
3278
|
|
|
2974
|
-
// Валидация данных
|
|
2975
|
-
|
|
3279
|
+
// Валидация данных — headless-раннер: находит ноды по сигналам модели, роутит в них ошибки,
|
|
3280
|
+
// отменяет устаревшие прогоны той же (model, schema). Возвращает ТОЛЬКО boolean:
|
|
3281
|
+
validateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean>
|
|
3282
|
+
// true = нет блокирующих ошибок (severity:'warning' не блокирует, но показывается).
|
|
3283
|
+
// false = есть блокирующая ошибка; она уже проставлена в ноду → UI подсветит поле.
|
|
3284
|
+
|
|
3285
|
+
// Схема валидации — отдельный слой над моделью (не смешивается с layout):
|
|
3286
|
+
defineValidationSchema<T>(({ model }) => {
|
|
3287
|
+
validate(model.$.field, [rules]); // синхронные правила поля
|
|
3288
|
+
// validateAsync / validateWhen / cross / each / apply — см. contract-spec
|
|
3289
|
+
}): ValidationSchema<T>
|
|
2976
3290
|
|
|
2977
|
-
// Нода
|
|
2978
|
-
form
|
|
2979
|
-
form
|
|
2980
|
-
form.<field>.
|
|
3291
|
+
// Нода поля/формы (для точечной работы с UI-состоянием):
|
|
3292
|
+
form.markAsTouched(); // тронуть все поля (показать ошибки)
|
|
3293
|
+
form.clearErrors(); // снять ошибки со всех нод
|
|
3294
|
+
form.<field>.setErrors([{ code, message }]); // серверная/бизнес-ошибка в конкретное поле
|
|
3295
|
+
form.<field>.clearErrors();
|
|
2981
3296
|
```
|
|
2982
3297
|
|
|
2983
3298
|
## 64. Examples
|
|
@@ -2986,23 +3301,34 @@ form.<field>.reset();
|
|
|
2986
3301
|
|
|
2987
3302
|
```tsx
|
|
2988
3303
|
import { useMemo } from 'react';
|
|
2989
|
-
import { createModel, createForm
|
|
3304
|
+
import { createModel, createForm } from '@reformer/core';
|
|
3305
|
+
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
3306
|
+
import { required, email, minLength } from '@reformer/core/validators';
|
|
2990
3307
|
|
|
2991
3308
|
type RegistrationFormData = { username: string; email: string; password: string };
|
|
2992
3309
|
|
|
3310
|
+
// Валидация — отдельная схема над моделью. НЕ входит в layout, который получает createForm.
|
|
3311
|
+
// Стабильная module-level `const`-ссылка — чтобы validateModel мог отменять устаревшие прогоны.
|
|
3312
|
+
const registrationSchema = defineValidationSchema<RegistrationFormData>(({ model }) => {
|
|
3313
|
+
validate(model.$.username, [required({ message: 'Имя обязательно' }), minLength(3)]);
|
|
3314
|
+
validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);
|
|
3315
|
+
validate(model.$.password, [required({ message: 'Пароль обязателен' }), minLength(8)]);
|
|
3316
|
+
});
|
|
3317
|
+
|
|
2993
3318
|
function RegistrationForm() {
|
|
2994
|
-
const { model, form
|
|
3319
|
+
const { model, form } = useMemo(() => {
|
|
2995
3320
|
const m = createModel<RegistrationFormData>({ username: '', email: '', password: '' });
|
|
2996
|
-
|
|
2997
|
-
return { model: m, form: createForm({ model: m, schema:
|
|
3321
|
+
// schema здесь — layout-дерево нод (компоненты/раскладка), без валидаторов.
|
|
3322
|
+
return { model: m, form: createForm({ model: m, schema: buildLayout(m) }) };
|
|
2998
3323
|
}, []);
|
|
2999
3324
|
|
|
3000
3325
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
3001
3326
|
e.preventDefault();
|
|
3327
|
+
form.markAsTouched(); // показать ошибки на всех полях
|
|
3002
3328
|
|
|
3003
|
-
// Шаг 1:
|
|
3004
|
-
const
|
|
3005
|
-
if (!
|
|
3329
|
+
// Шаг 1: прогон валидации данных. Ошибки автоматически проставятся в ноды → UI подсветит.
|
|
3330
|
+
const valid = await validateModel(model, registrationSchema);
|
|
3331
|
+
if (!valid) return;
|
|
3006
3332
|
|
|
3007
3333
|
// Шаг 2: достать чистые данные и отправить
|
|
3008
3334
|
const payload = model.get();
|
|
@@ -3032,9 +3358,10 @@ function RegistrationForm() {
|
|
|
3032
3358
|
```tsx
|
|
3033
3359
|
async function handleSubmit(e: React.FormEvent) {
|
|
3034
3360
|
e.preventDefault();
|
|
3361
|
+
form.markAsTouched();
|
|
3035
3362
|
|
|
3036
|
-
const
|
|
3037
|
-
if (!
|
|
3363
|
+
const valid = await validateModel(model, registrationSchema);
|
|
3364
|
+
if (!valid) return;
|
|
3038
3365
|
|
|
3039
3366
|
try {
|
|
3040
3367
|
const response = await api.register(model.get());
|
|
@@ -3078,39 +3405,61 @@ function ActionButtons({ model }: { model: FormModel<RegistrationFormData> }) {
|
|
|
3078
3405
|
|
|
3079
3406
|
### Использование вне React (server action / Node)
|
|
3080
3407
|
|
|
3081
|
-
|
|
3408
|
+
Раннер headless — работает без UI-компонентов, только с моделью и схемой:
|
|
3082
3409
|
|
|
3083
3410
|
```typescript
|
|
3084
|
-
import { createModel
|
|
3411
|
+
import { createModel } from '@reformer/core';
|
|
3412
|
+
import { validateModel } from '@reformer/core/validation';
|
|
3085
3413
|
|
|
3086
3414
|
async function processFormPayload(rawData: Partial<MyForm>) {
|
|
3087
3415
|
const model = createModel<MyForm>(initialValues);
|
|
3088
3416
|
model.patch(rawData); // частичный load данных
|
|
3089
3417
|
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
return { ok:
|
|
3418
|
+
// validateModel находит ноды по сигналам модели и роутит в них ошибки, возвращая boolean.
|
|
3419
|
+
const valid = await validateModel(model, myFormSchema);
|
|
3420
|
+
if (!valid) return { ok: false as const };
|
|
3421
|
+
return { ok: true as const, data: model.get() };
|
|
3093
3422
|
}
|
|
3094
3423
|
```
|
|
3095
3424
|
|
|
3096
3425
|
## 65. Anti-patterns
|
|
3097
3426
|
|
|
3098
3427
|
```typescript
|
|
3099
|
-
// ❌
|
|
3428
|
+
// ❌ Обращение к .valid: раннер возвращает boolean, а не { valid, errors }
|
|
3429
|
+
const result = await validateModel(model, schema);
|
|
3430
|
+
if (!result.valid) return; // result — boolean, .valid === undefined → guard никогда не сработает
|
|
3431
|
+
|
|
3432
|
+
// ✅ Результат — сам boolean
|
|
3433
|
+
const valid = await validateModel(model, schema);
|
|
3434
|
+
if (!valid) return;
|
|
3435
|
+
```
|
|
3436
|
+
|
|
3437
|
+
```typescript
|
|
3438
|
+
// ❌ Чтение результата валидации без await — сабмитим по неполному вердикту
|
|
3100
3439
|
const handleSubmit = (e) => {
|
|
3101
3440
|
e.preventDefault();
|
|
3102
|
-
|
|
3441
|
+
validateModel(model, schema); // Promise проигнорирован, async-валидаторы не дождались
|
|
3103
3442
|
submit(model.get());
|
|
3104
3443
|
};
|
|
3105
3444
|
|
|
3106
|
-
// ✅ Дождаться и проверить
|
|
3445
|
+
// ✅ Дождаться и проверить boolean
|
|
3107
3446
|
const handleSubmit = async (e) => {
|
|
3108
3447
|
e.preventDefault();
|
|
3109
|
-
|
|
3110
|
-
if (result.valid) submit(model.get());
|
|
3448
|
+
if (await validateModel(model, schema)) submit(model.get());
|
|
3111
3449
|
};
|
|
3112
3450
|
```
|
|
3113
3451
|
|
|
3452
|
+
```typescript
|
|
3453
|
+
// ❌ Валидаторы в layout-дереве, отдаваемом в createForm — layout не несёт правил
|
|
3454
|
+
createForm({ model, schema: { username: { value: '', validators: [required()] } } });
|
|
3455
|
+
|
|
3456
|
+
// ✅ Валидация — отдельная схема, прогоняется раннером
|
|
3457
|
+
const schema = defineValidationSchema<T>(({ model }) => {
|
|
3458
|
+
validate(model.$.username, [required()]);
|
|
3459
|
+
});
|
|
3460
|
+
await validateModel(model, schema);
|
|
3461
|
+
```
|
|
3462
|
+
|
|
3114
3463
|
```typescript
|
|
3115
3464
|
// ❌ model.reset() до ответа сервера — потеря данных при ошибке
|
|
3116
3465
|
await api.send(model.get());
|
|
@@ -3137,7 +3486,13 @@ model.reset();
|
|
|
3137
3486
|
|
|
3138
3487
|
**Q: После `reset()` в UI остались старые ошибки.**
|
|
3139
3488
|
A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
|
|
3140
|
-
`
|
|
3489
|
+
`validateModel(model, schema)` после reset (валидные поля погаснут сами), либо очисти напрямую —
|
|
3490
|
+
`form.clearErrors()` / `form.<field>.clearErrors()`.
|
|
3491
|
+
|
|
3492
|
+
**Q: `validateModel` вернул `true`, но в поле висит ошибка.**
|
|
3493
|
+
A: Это правило с `severity: 'warning'` — оно показывается, но submit не блокирует, поэтому раннер
|
|
3494
|
+
и вернул `true`. Блокируют только ошибки без `severity` (default). Чтобы не пускать submit при
|
|
3495
|
+
warning — проверяй его отдельно, вне `validateModel`.
|
|
3141
3496
|
|
|
3142
3497
|
**Q: `reset()` не возвращает данные, загруженные с сервера.**
|
|
3143
3498
|
A: `reset()` возвращает к initial-снимку (значения на момент `createModel`). `set/patch` НЕ
|
|
@@ -3151,8 +3506,9 @@ A: `model.get()` возвращает значения всех полей. Фи
|
|
|
3151
3506
|
## 67. See also
|
|
3152
3507
|
|
|
3153
3508
|
- [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
|
|
3154
|
-
- [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `
|
|
3155
|
-
- [
|
|
3509
|
+
- [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateModel` (`validateStep`/`validateAll`)
|
|
3510
|
+
- [27-revalidate-when.md](./27-revalidate-when.md) — мост «поведение → валидация»: `revalidateWhen([deps], () => void validateModel(model, schema))`
|
|
3511
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateModel`
|
|
3156
3512
|
- [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки
|
|
3157
3513
|
|
|
3158
3514
|
## 68. Purpose
|
|
@@ -3330,7 +3686,7 @@ useEffect(() => {
|
|
|
3330
3686
|
## 72. Troubleshooting
|
|
3331
3687
|
|
|
3332
3688
|
**Q: После `set`/`patch` не показываются ошибки.**
|
|
3333
|
-
A: `set`/`patch` не запускают валидацию. После загрузки вызови `await
|
|
3689
|
+
A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateModel(model, schema)` (внешний раннер из `@reformer/core/validation`).
|
|
3334
3690
|
|
|
3335
3691
|
**Q: Опции в `<Select>` не появляются после `updateComponentProps`.**
|
|
3336
3692
|
A: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что компонент подписан через `useFormControl`;
|
|
@@ -3353,7 +3709,8 @@ A: `reset()` возвращает к initial-снимку из `createModel`, н
|
|
|
3353
3709
|
|
|
3354
3710
|
### Recipe 1 — Imports (root cause prevention)
|
|
3355
3711
|
|
|
3356
|
-
-
|
|
3712
|
+
- Модель/форма/хуки/типы/**примитивы behaviors** — из `@reformer/core`.
|
|
3713
|
+
- Схема валидации (операторы + раннер `validateModel`) — из `@reformer/core/validation`.
|
|
3357
3714
|
- Чистые фабрики валидаторов — из `@reformer/core/validators`.
|
|
3358
3715
|
- Декларативный DSL (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
|
|
3359
3716
|
|
|
@@ -3361,12 +3718,17 @@ A: `reset()` возвращает к initial-снимку из `createModel`, н
|
|
|
3361
3718
|
import {
|
|
3362
3719
|
createModel,
|
|
3363
3720
|
createForm,
|
|
3364
|
-
validateFormModel,
|
|
3365
3721
|
type FormModel,
|
|
3366
3722
|
type FormProxy,
|
|
3367
3723
|
type ModelSignals,
|
|
3368
|
-
type ModelValidator,
|
|
3369
3724
|
} from '@reformer/core';
|
|
3725
|
+
import {
|
|
3726
|
+
defineValidationSchema,
|
|
3727
|
+
validate,
|
|
3728
|
+
cross,
|
|
3729
|
+
validateModel,
|
|
3730
|
+
type Rule,
|
|
3731
|
+
} from '@reformer/core/validation';
|
|
3370
3732
|
import { required, min, max, email } from '@reformer/core/validators';
|
|
3371
3733
|
import { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';
|
|
3372
3734
|
```
|
|
@@ -3396,38 +3758,40 @@ export type CreditApplicationForm = {
|
|
|
3396
3758
|
### Recipe 3 — Схема привязана к сигналам модели
|
|
3397
3759
|
|
|
3398
3760
|
`value` поля — это сигнал модели (`model.$.field`), а не литерал. Тип поля выводится из сигнала.
|
|
3761
|
+
Layout-схема БЕЗ валидаторов — правила живут в отдельной validation-схеме.
|
|
3399
3762
|
|
|
3400
3763
|
```typescript
|
|
3401
3764
|
const model = createModel<CreditApplicationForm>(initial);
|
|
3402
3765
|
|
|
3403
3766
|
const schema = {
|
|
3404
|
-
loanAmount: { value: model.$.loanAmount, component: Input
|
|
3767
|
+
loanAmount: { value: model.$.loanAmount, component: Input },
|
|
3405
3768
|
// вложенная группа — builder, принимающий ModelSignals<Sub>
|
|
3406
3769
|
personalData: personalDataNodes(model.$.personalData),
|
|
3407
3770
|
// массив — { array, item }
|
|
3408
3771
|
properties: { array: model.properties, item: propertyItem },
|
|
3409
3772
|
};
|
|
3773
|
+
|
|
3774
|
+
// правила — отдельно; типы полей выводятся из сигналов
|
|
3775
|
+
const validation = defineValidationSchema<CreditApplicationForm>(({ model }) => {
|
|
3776
|
+
validate(model.$.loanAmount, [required(), min(50000)]);
|
|
3777
|
+
});
|
|
3410
3778
|
```
|
|
3411
3779
|
|
|
3412
|
-
### Recipe 4 — Cross-field валидаторы:
|
|
3780
|
+
### Recipe 4 — Cross-field валидаторы: `cross` над типизированным снапшотом
|
|
3413
3781
|
|
|
3414
|
-
Правило — `
|
|
3415
|
-
читаются без `as`:
|
|
3782
|
+
Правило — обычная функция `(f: Root) => ValidationError | null` над снапшотом `model.get()`.
|
|
3783
|
+
Соседние поля читаются без `as`; ошибка вешается на поле-носитель `sig`:
|
|
3416
3784
|
|
|
3417
3785
|
```typescript
|
|
3418
|
-
import type {
|
|
3786
|
+
import type { ValidationError } from '@reformer/core';
|
|
3419
3787
|
|
|
3420
|
-
const initialPaymentVsProperty
|
|
3421
|
-
|
|
3422
|
-
_scope,
|
|
3423
|
-
root
|
|
3424
|
-
) =>
|
|
3425
|
-
root.initialPayment && root.propertyValue && root.initialPayment > root.propertyValue
|
|
3788
|
+
const initialPaymentVsProperty = (f: CreditApplicationForm): ValidationError | null =>
|
|
3789
|
+
f.initialPayment && f.propertyValue && f.initialPayment > f.propertyValue
|
|
3426
3790
|
? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }
|
|
3427
3791
|
: null;
|
|
3428
3792
|
|
|
3429
|
-
//
|
|
3430
|
-
|
|
3793
|
+
// внутри defineValidationSchema<CreditApplicationForm>(({ model }) => { ... }):
|
|
3794
|
+
cross(model.$.initialPayment, initialPaymentVsProperty);
|
|
3431
3795
|
```
|
|
3432
3796
|
|
|
3433
3797
|
### Recipe 5 — `compute` читает модель напрямую (без аннотаций)
|
|
@@ -3476,18 +3840,19 @@ function MyField<T>({ control }: MyFieldProps<T>) {
|
|
|
3476
3840
|
|
|
3477
3841
|
### Recipe 8 — Вынос правил в именованные функции
|
|
3478
3842
|
|
|
3479
|
-
|
|
3843
|
+
Field-правила — именованные `Rule<T>`, validation-схема остаётся плоской:
|
|
3480
3844
|
|
|
3481
3845
|
```typescript
|
|
3482
|
-
|
|
3846
|
+
import type { Rule } from '@reformer/core/validation';
|
|
3847
|
+
|
|
3848
|
+
const validateAdultAge: Rule<string | null> = (value) => {
|
|
3483
3849
|
if (!value) return null;
|
|
3484
3850
|
const age = new Date().getFullYear() - new Date(value).getFullYear();
|
|
3485
3851
|
return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;
|
|
3486
3852
|
};
|
|
3487
3853
|
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
};
|
|
3854
|
+
// внутри defineValidationSchema<MyForm>(({ model }) => { ... }):
|
|
3855
|
+
validate(model.$.birthDate, [validateAdultAge]);
|
|
3491
3856
|
```
|
|
3492
3857
|
|
|
3493
3858
|
### Anti-patterns to avoid
|
|
@@ -3501,76 +3866,138 @@ const schema = {
|
|
|
3501
3866
|
## 75. 31. ASYNC VALIDATOR
|
|
3502
3867
|
|
|
3503
3868
|
Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
|
|
3504
|
-
async
|
|
3505
|
-
|
|
3506
|
-
|
|
3869
|
+
async-правило это `AsyncRule<T>` = `(value, { signal }) => Promise<ValidationError | null>`.
|
|
3870
|
+
Оно регистрируется оператором `validateAsync(sig, [asyncRules])` внутри схемы валидации
|
|
3871
|
+
(`@reformer/core/validation`) и исполняется внешним раннером `validateModel` (async-правила
|
|
3872
|
+
прогоняются параллельно через `Promise.all`, раннер их дожидается).
|
|
3873
|
+
|
|
3874
|
+
Слои разделены: layout (`createForm`-схема / JSON) НЕ несёт валидаторов — правила живут в
|
|
3875
|
+
отдельной функции-схеме `defineValidationSchema<T>(({ model }) => …)`.
|
|
3507
3876
|
|
|
3508
3877
|
```ts
|
|
3509
|
-
import { createModel
|
|
3878
|
+
import { createModel } from '@reformer/core';
|
|
3879
|
+
import {
|
|
3880
|
+
validate,
|
|
3881
|
+
validateAsync,
|
|
3882
|
+
defineValidationSchema,
|
|
3883
|
+
validateModel,
|
|
3884
|
+
type AsyncRule,
|
|
3885
|
+
} from '@reformer/core/validation';
|
|
3510
3886
|
import { required, email } from '@reformer/core/validators';
|
|
3511
|
-
import { Input } from '@reformer/ui-kit';
|
|
3512
3887
|
|
|
3513
|
-
// async
|
|
3514
|
-
|
|
3888
|
+
// async-правило: (value, { signal }) => Promise<ValidationError | null>.
|
|
3889
|
+
// `signal` — AbortSignal устаревшего прогона: прокинь его в fetch, чтобы отменить in-flight.
|
|
3890
|
+
const checkEmailUnique: AsyncRule<string> = async (value, { signal }) => {
|
|
3515
3891
|
if (!value) return null; // пусто = валидно (sync `required` отдельно)
|
|
3516
3892
|
try {
|
|
3517
|
-
const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}
|
|
3893
|
+
const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
|
|
3518
3894
|
const { available } = (await res.json()) as { available: boolean };
|
|
3519
3895
|
return available ? null : { code: 'email-taken', message: 'Email уже зарегистрирован' };
|
|
3520
3896
|
} catch {
|
|
3521
|
-
return
|
|
3897
|
+
return null; // сетевой сбой/отмена НЕ блокирует submit — возвращаем null, а не ошибку
|
|
3522
3898
|
}
|
|
3523
3899
|
};
|
|
3524
3900
|
|
|
3525
3901
|
const model = createModel<{ email: string }>({ email: '' });
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
const
|
|
3902
|
+
|
|
3903
|
+
// Схема — обычная функция над моделью; sync и async — разными операторами на одном сигнале.
|
|
3904
|
+
const schema = defineValidationSchema<{ email: string }>(({ model }) => {
|
|
3905
|
+
validate(model.$.email, [required(), email()]); // sync-фабрики
|
|
3906
|
+
validateAsync(model.$.email, [checkEmailUnique]); // async-правило
|
|
3907
|
+
});
|
|
3908
|
+
|
|
3909
|
+
// Прогон по требованию (submit / шаг):
|
|
3910
|
+
const ok = await validateModel(model, schema); // Promise<boolean>
|
|
3535
3911
|
```
|
|
3536
3912
|
|
|
3537
3913
|
### Как это исполняется
|
|
3538
3914
|
|
|
3539
|
-
1. `
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
(
|
|
3543
|
-
|
|
3915
|
+
1. `validateModel(model, schema)` открывает ambient-окно и **синхронно** прогоняет схему:
|
|
3916
|
+
`validate`/`validateAsync`/`cross` регистрируют правила своих полей.
|
|
3917
|
+
2. Sync-правила выполняются сразу; async-правила из `validateAsync` собираются и после закрытия
|
|
3918
|
+
ambient-окна дожидаются параллельно (`Promise.all`) с прокинутым `AbortSignal`.
|
|
3919
|
+
3. Ошибки роутятся в ноды формы (`getNodeForSignal(sig).setErrors(...)`), UI подсвечивает поле;
|
|
3920
|
+
поля, ставшие валидными, гасятся (`setErrors([])`).
|
|
3921
|
+
4. Возвращает `Promise<boolean>` — `true`, если нет блокирующих ошибок (`severity:'warning'`
|
|
3922
|
+
не блокирует). Устаревший (отменённый) прогон возвращает `false` — ему нельзя доверять для submit.
|
|
3923
|
+
|
|
3924
|
+
Синхронного варианта у нового контракта нет: раннер один — `validateModel`, и он всегда `async`.
|
|
3925
|
+
|
|
3926
|
+
### Sync и async — два оператора
|
|
3544
3927
|
|
|
3545
|
-
|
|
3928
|
+
Разделение sync/async делается не полем схемы, а разными операторами на одном сигнале:
|
|
3929
|
+
`validate(sig, [syncRules])` и `validateAsync(sig, [asyncRules])`. Раннер прогоняет sync-правила
|
|
3930
|
+
инлайн, а async — дожидается. Держи async-правило дешёвым: ранний `if (!value) return null`
|
|
3931
|
+
пропускает сетевой вызов, пока `required()`/формат ещё не пройдены.
|
|
3546
3932
|
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3933
|
+
```ts
|
|
3934
|
+
validate(model.$.inn, [required(), pattern(/^\d{12}$/)]); // формат — sync, мгновенно
|
|
3935
|
+
validateAsync(model.$.inn, [checkInnInRegistry]); // обращение к API — отдельным оператором
|
|
3936
|
+
```
|
|
3550
3937
|
|
|
3551
3938
|
### UI integration
|
|
3552
3939
|
|
|
3553
|
-
`
|
|
3554
|
-
|
|
3940
|
+
`validateModel(...)` возвращает `Promise<boolean>` — индикатор проверки держи вокруг этого
|
|
3941
|
+
`await`. Раннер схемы **не** выставляет per-field `pending` на ноде (он роутит только ошибки через
|
|
3942
|
+
`setErrors`), поэтому спиннер async-валидации — это собственный флаг (обычно form-level, как
|
|
3943
|
+
`ui.pending` в submit-флоу, или локальный state компонента):
|
|
3555
3944
|
|
|
3556
3945
|
```tsx
|
|
3557
|
-
const
|
|
3558
|
-
|
|
3946
|
+
const [checking, setChecking] = useState(false);
|
|
3947
|
+
|
|
3948
|
+
const runValidation = async (): Promise<boolean> => {
|
|
3949
|
+
setChecking(true);
|
|
3950
|
+
try {
|
|
3951
|
+
return await validateModel(model, schema);
|
|
3952
|
+
} finally {
|
|
3953
|
+
setChecking(false);
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
|
|
3957
|
+
return checking ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
|
|
3559
3958
|
```
|
|
3560
3959
|
|
|
3561
3960
|
### Debounce и отмена
|
|
3562
3961
|
|
|
3563
|
-
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3962
|
+
- **Отмена устаревших — раннером, бесплатно.** Быстрый повторный `validateModel(model, schema)`
|
|
3963
|
+
той же пары `(model, schema)` отменяет предыдущий in-flight прогон через `AbortController`;
|
|
3964
|
+
его `AbortSignal` прокинут в `validateAsync`-правила, поэтому `fetch(url, { signal })` рвётся
|
|
3965
|
+
сам. Ручной debounce для КОРРЕКТНОСТИ не нужен — держи схему стабильным `const`
|
|
3966
|
+
(`defineValidationSchema`), иначе отмена не сматчит прогоны по идентичности.
|
|
3967
|
+
- Валидация запускается on-demand (submit / шаг / через `revalidateWhen`), а не на каждый
|
|
3968
|
+
keystroke — отдельный `debounce` в правиле обычно не нужен.
|
|
3969
|
+
- Если нужно дебаунсить дорогой async-валидатор относительно частых изменений — триггерь прогон
|
|
3970
|
+
из поведения через `onChange`, который даёт debounce из коробки:
|
|
3971
|
+
|
|
3972
|
+
```ts
|
|
3973
|
+
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
|
|
3974
|
+
import { validateModel } from '@reformer/core/validation';
|
|
3975
|
+
|
|
3976
|
+
export const behavior = defineFormBehavior<{ username: string }>(({ model }) => {
|
|
3977
|
+
onChange(model.$.username, () => void validateModel(model, schema), { debounce: 300 });
|
|
3978
|
+
});
|
|
3979
|
+
```
|
|
3980
|
+
|
|
3981
|
+
Либо оборачивай в свой debounce колбэк `revalidateWhen([...], () => void validateModel(...))`.
|
|
3982
|
+
- **Async cross-field** — инлайн `validateAsync`-правило, замыкающее `model` и читающее снапшот
|
|
3983
|
+
соседей `model.get()` до первого `await` (у `AsyncRule` нет `root`, только `value` + `signal`;
|
|
3984
|
+
`cross(...)` — синхронный):
|
|
3985
|
+
|
|
3986
|
+
```ts
|
|
3987
|
+
validateAsync(model.$.email, [
|
|
3988
|
+
async (email, { signal }) => {
|
|
3989
|
+
const { username } = model.get(); // снапшот соседних полей до await
|
|
3990
|
+
const res = await fetch(`/api/check?email=${email}&user=${username}`, { signal });
|
|
3991
|
+
return (await res.json()).ok ? null : { code: 'conflict', message: 'Пара занята' };
|
|
3992
|
+
},
|
|
3993
|
+
]);
|
|
3994
|
+
```
|
|
3568
3995
|
|
|
3569
3996
|
### See also
|
|
3570
3997
|
|
|
3571
|
-
- [27-revalidate-when.md](27-revalidate-when.md) — перезапуск
|
|
3998
|
+
- [27-revalidate-when.md](27-revalidate-when.md) — перезапуск `validateModel` по триггерам
|
|
3572
3999
|
- [29-async-preload.md](29-async-preload.md) — async preload данных при init формы
|
|
3573
|
-
- [
|
|
4000
|
+
- [32-async-options-loading.md](32-async-options-loading.md) — `onChange` с debounce + AbortSignal
|
|
3574
4001
|
|
|
3575
4002
|
## 76. 32. ASYNC OPTIONS LOADING
|
|
3576
4003
|
|
|
@@ -4134,15 +4561,16 @@ export function createForm<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>;
|
|
|
4134
4561
|
```
|
|
4135
4562
|
|
|
4136
4563
|
**Parameters:**
|
|
4137
|
-
- `args` — - Модель данных,
|
|
4564
|
+
- `args` — - Модель данных, layout-схема (component/componentProps) и (опционально) поведение
|
|
4138
4565
|
|
|
4139
4566
|
**Returns:** Типизированная форма с Proxy-доступом к полям
|
|
4140
4567
|
|
|
4141
4568
|
**Examples:**
|
|
4142
4569
|
|
|
4143
|
-
Архитектура M1: `createModel` +
|
|
4570
|
+
Архитектура M1: `createModel` + layout-схема + `createForm({ model, schema })`
|
|
4144
4571
|
```typescript
|
|
4145
|
-
import { createModel, createForm
|
|
4572
|
+
import { createModel, createForm } from '@reformer/core';
|
|
4573
|
+
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
4146
4574
|
import { required, email, minLength } from '@reformer/core/validators';
|
|
4147
4575
|
|
|
4148
4576
|
interface UserForm {
|
|
@@ -4152,28 +4580,23 @@ password: string;
|
|
|
4152
4580
|
|
|
4153
4581
|
const model = createModel<UserForm>({ email: '', password: '' });
|
|
4154
4582
|
|
|
4583
|
+
// Layout-схема НЕ несёт validators — только component/componentProps.
|
|
4155
4584
|
const schema = {
|
|
4156
4585
|
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
|
-
},
|
|
4586
|
+
{ value: model.$.email, component: Input },
|
|
4587
|
+
{ value: model.$.password, component: Input },
|
|
4167
4588
|
],
|
|
4168
4589
|
};
|
|
4169
4590
|
|
|
4170
4591
|
const form = createForm<UserForm>({ model, schema });
|
|
4171
|
-
|
|
4172
|
-
// TypeScript знает о полях (нода привязана к сигналу модели):
|
|
4173
4592
|
form.email.setValue('test@mail.com');
|
|
4174
4593
|
|
|
4175
|
-
// Валидация
|
|
4176
|
-
const
|
|
4594
|
+
// Валидация — ОТДЕЛЬНАЯ схема, прогон внешним раннером (ошибки сами доезжают до нод):
|
|
4595
|
+
const validation = defineValidationSchema<UserForm>(({ model }) => {
|
|
4596
|
+
validate(model.$.email, [required(), email()]);
|
|
4597
|
+
validate(model.$.password, [required(), minLength(8)]);
|
|
4598
|
+
});
|
|
4599
|
+
const ok = await validateModel(model, validation);
|
|
4177
4600
|
```
|
|
4178
4601
|
|
|
4179
4602
|
_Source: src/form/create-form.ts_
|
|
@@ -4342,7 +4765,7 @@ _Source: src/form/behaviors-node.ts_
|
|
|
4342
4765
|
|
|
4343
4766
|
**Signature:**
|
|
4344
4767
|
```typescript
|
|
4345
|
-
export function email<TForm = unknown, TField extends string | undefined = string>(
|
|
4768
|
+
export function email<TForm = unknown, TField extends string | null | undefined = string>(
|
|
4346
4769
|
options?: ValidateOptions
|
|
4347
4770
|
): Validator<TForm, TField>
|
|
4348
4771
|
```
|
|
@@ -4792,6 +5215,70 @@ export type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
|
|
|
4792
5215
|
|
|
4793
5216
|
_Source: src/form/types/contracts.ts_
|
|
4794
5217
|
|
|
5218
|
+
### FileLike
|
|
5219
|
+
|
|
5220
|
+
**Kind:** `interface`
|
|
5221
|
+
|
|
5222
|
+
Минимальный файлоподобный контракт, с которым работают file-валидаторы:
|
|
5223
|
+
нативный `File`, сериализуемый дескриптор загруженного файла и т.п.
|
|
5224
|
+
|
|
5225
|
+
**Signature:**
|
|
5226
|
+
```typescript
|
|
5227
|
+
export interface FileLike {
|
|
5228
|
+
name: string;
|
|
5229
|
+
size?: number;
|
|
5230
|
+
type?: string;
|
|
5231
|
+
}
|
|
5232
|
+
```
|
|
5233
|
+
|
|
5234
|
+
_Source: src/form/validation/validators/file-utils.ts_
|
|
5235
|
+
|
|
5236
|
+
### fileType
|
|
5237
|
+
|
|
5238
|
+
**Kind:** `function`
|
|
5239
|
+
|
|
5240
|
+
Фабрика валидатора типа файла.
|
|
5241
|
+
|
|
5242
|
+
Проверяет каждый файл против accept-строки в синтаксисе нативного атрибута
|
|
5243
|
+
`<input type="file" accept>`: расширения (`.pdf`), точные MIME (`image/png`),
|
|
5244
|
+
wildcard-категории (`image/*`), список через запятую. Матчинг регистронезависимый,
|
|
5245
|
+
по `type` и/или расширению из `name` ({@link matchesFileAccept}).
|
|
5246
|
+
|
|
5247
|
+
Нужен потому, что нативный `accept` — только подсказка пикеру: на drag-and-drop
|
|
5248
|
+
и «All files» он не действует. Пустые значения (`null`/`undefined`/`''`/`[]`)
|
|
5249
|
+
пропускаются (используйте {@link required} для обязательности).
|
|
5250
|
+
|
|
5251
|
+
**Signature:**
|
|
5252
|
+
```typescript
|
|
5253
|
+
export function fileType<TForm = unknown, TField = unknown>(
|
|
5254
|
+
accept: string,
|
|
5255
|
+
options?: ValidateOptions
|
|
5256
|
+
): Validator<TForm, TField>
|
|
5257
|
+
```
|
|
5258
|
+
|
|
5259
|
+
**Parameters:**
|
|
5260
|
+
- `accept` — - Accept-строка допустимых типов, например `'image/*,.pdf'`
|
|
5261
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
5262
|
+
попадают `accept` и `fileName` (первый нарушивший файл).
|
|
5263
|
+
|
|
5264
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
5265
|
+
|
|
5266
|
+
**Examples:**
|
|
5267
|
+
|
|
5268
|
+
Только изображения и PDF
|
|
5269
|
+
```typescript
|
|
5270
|
+
import { fileType } from '@reformer/core/validators';
|
|
5271
|
+
|
|
5272
|
+
// Внутри FieldConfig схемы формы:
|
|
5273
|
+
documents: {
|
|
5274
|
+
value: model.$.documents,
|
|
5275
|
+
component: FileUploadField,
|
|
5276
|
+
validators: [fileType('image/*,.pdf', { message: 'Только изображения или PDF' })],
|
|
5277
|
+
},
|
|
5278
|
+
```
|
|
5279
|
+
|
|
5280
|
+
_Source: src/form/validation/validators/file-type.ts_
|
|
5281
|
+
|
|
4795
5282
|
### FormArrayProxy
|
|
4796
5283
|
|
|
4797
5284
|
**Kind:** `type`
|
|
@@ -5143,8 +5630,8 @@ _Source: src/form/types/deep-schema.ts_
|
|
|
5143
5630
|
|
|
5144
5631
|
**Kind:** `interface`
|
|
5145
5632
|
|
|
5146
|
-
Узел единой схемы M1 —
|
|
5147
|
-
|
|
5633
|
+
Узел единой схемы M1 — layout-дерево, обходимое `createForm({ model, schema })`
|
|
5634
|
+
и рендерерами (schema-валидация живёт отдельно — `@reformer/core/validation`).
|
|
5148
5635
|
|
|
5149
5636
|
Узел совмещает несколько ролей (различаются рантаймом по форме):
|
|
5150
5637
|
- **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
|
|
@@ -5164,8 +5651,12 @@ export interface FormSchemaNode {
|
|
|
5164
5651
|
* `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
|
|
5165
5652
|
*/
|
|
5166
5653
|
value?: unknown;
|
|
5167
|
-
/**
|
|
5168
|
-
|
|
5654
|
+
/**
|
|
5655
|
+
* UI-компонент либо нативный HTML-тег (`'div'`, `'p'`, `'h3'`) для презентационной вёрстки
|
|
5656
|
+
* прямо в схеме. Опционален: core-часть работает без UI (значение/валидация) и `component`
|
|
5657
|
+
* не интерпретирует — он доезжает до рендерера как есть.
|
|
5658
|
+
*/
|
|
5659
|
+
component?: ElementType;
|
|
5169
5660
|
/** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
|
|
5170
5661
|
componentProps?: Record<string, unknown>;
|
|
5171
5662
|
validators?: SchemaValidator[];
|
|
@@ -5301,7 +5792,7 @@ _Source: src/form/types/contracts.ts_
|
|
|
5301
5792
|
```typescript
|
|
5302
5793
|
export function futureDate<
|
|
5303
5794
|
TForm = unknown,
|
|
5304
|
-
TField extends string | Date | undefined = string | Date,
|
|
5795
|
+
TField extends string | Date | null | undefined = string | Date,
|
|
5305
5796
|
>(options?: ValidateOptions): Validator<TForm, TField>
|
|
5306
5797
|
```
|
|
5307
5798
|
|
|
@@ -5394,8 +5885,8 @@ _Source: src/form/type-guards.ts_
|
|
|
5394
5885
|
GroupNode - узел для группы полей
|
|
5395
5886
|
|
|
5396
5887
|
Создаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`
|
|
5397
|
-
(M1: `createForm({ model, schema })`);
|
|
5398
|
-
(`
|
|
5888
|
+
(M1: `createForm({ model, schema })`); schema-валидация/behavior живут на слое модели
|
|
5889
|
+
(`validateModel` из `@reformer/core/validation`, `computeFrom`/`enableWhen`/…), а не на ноде.
|
|
5399
5890
|
|
|
5400
5891
|
**Signature:**
|
|
5401
5892
|
```typescript
|
|
@@ -5519,9 +6010,10 @@ _Source: src/form/type-guards.ts_
|
|
|
5519
6010
|
|
|
5520
6011
|
**Signature:**
|
|
5521
6012
|
```typescript
|
|
5522
|
-
export function isDate<
|
|
5523
|
-
|
|
5524
|
-
|
|
6013
|
+
export function isDate<
|
|
6014
|
+
TForm = unknown,
|
|
6015
|
+
TField extends string | Date | null | undefined = string | Date,
|
|
6016
|
+
>(options?: ValidateOptions): Validator<TForm, TField>
|
|
5525
6017
|
```
|
|
5526
6018
|
|
|
5527
6019
|
**Parameters:**
|
|
@@ -5610,6 +6102,25 @@ if (isFieldNode(node)) {
|
|
|
5610
6102
|
|
|
5611
6103
|
_Source: src/form/type-guards.ts_
|
|
5612
6104
|
|
|
6105
|
+
### isFileLike
|
|
6106
|
+
|
|
6107
|
+
**Kind:** `function`
|
|
6108
|
+
|
|
6109
|
+
Проверяет, что значение файлоподобно: объект со строковым `name` и опциональными
|
|
6110
|
+
числовым `size` / строковым `type`.
|
|
6111
|
+
|
|
6112
|
+
**Signature:**
|
|
6113
|
+
```typescript
|
|
6114
|
+
export function isFileLike(value: unknown): value is FileLike
|
|
6115
|
+
```
|
|
6116
|
+
|
|
6117
|
+
**Parameters:**
|
|
6118
|
+
- `value` — - Проверяемое значение
|
|
6119
|
+
|
|
6120
|
+
**Returns:** `true`, если значение соответствует {@link FileLike}
|
|
6121
|
+
|
|
6122
|
+
_Source: src/form/validation/validators/file-utils.ts_
|
|
6123
|
+
|
|
5613
6124
|
### isFormNode
|
|
5614
6125
|
|
|
5615
6126
|
**Kind:** `function`
|
|
@@ -5742,6 +6253,30 @@ markDerived(model.$.total);
|
|
|
5742
6253
|
|
|
5743
6254
|
_Source: src/state/derived-registry.ts_
|
|
5744
6255
|
|
|
6256
|
+
### matchesFileAccept
|
|
6257
|
+
|
|
6258
|
+
**Kind:** `function`
|
|
6259
|
+
|
|
6260
|
+
Проверяет файл против accept-паттерна в синтаксисе нативного атрибута
|
|
6261
|
+
`<input type="file" accept>`: список через запятую из расширений (`.pdf`),
|
|
6262
|
+
точных MIME (`image/png`) и wildcard-категорий (`image/*`). Регистронезависимо.
|
|
6263
|
+
|
|
6264
|
+
Нужен собственный матчер, потому что `accept` у нативного input — только подсказка
|
|
6265
|
+
пикеру: на drag-and-drop и на программное значение он не действует.
|
|
6266
|
+
|
|
6267
|
+
**Signature:**
|
|
6268
|
+
```typescript
|
|
6269
|
+
export function matchesFileAccept(file: Pick<FileLike, 'name' | 'type'>, accept: string): boolean
|
|
6270
|
+
```
|
|
6271
|
+
|
|
6272
|
+
**Parameters:**
|
|
6273
|
+
- `file` — - Файлоподобное значение (`name` + опциональный `type`)
|
|
6274
|
+
- `accept` — - Accept-строка; пустая/пробельная строка матчит всё
|
|
6275
|
+
|
|
6276
|
+
**Returns:** `true`, если файл подходит хотя бы под один паттерн
|
|
6277
|
+
|
|
6278
|
+
_Source: src/form/validation/validators/file-utils.ts_
|
|
6279
|
+
|
|
5745
6280
|
### max
|
|
5746
6281
|
|
|
5747
6282
|
**Kind:** `function`
|
|
@@ -5862,6 +6397,90 @@ validators: [maxDate(new Date(), { message: 'Дата не может быть
|
|
|
5862
6397
|
|
|
5863
6398
|
_Source: src/form/validation/validators/max-date.ts_
|
|
5864
6399
|
|
|
6400
|
+
### maxFiles
|
|
6401
|
+
|
|
6402
|
+
**Kind:** `function`
|
|
6403
|
+
|
|
6404
|
+
Фабрика валидатора максимального количества файлов.
|
|
6405
|
+
|
|
6406
|
+
Работает с массивом файлов (одиночный файл считается как 1). Пустые значения
|
|
6407
|
+
(`null`/`undefined`/`''`/`[]`) пропускаются (используйте {@link required}
|
|
6408
|
+
для обязательности).
|
|
6409
|
+
|
|
6410
|
+
**Signature:**
|
|
6411
|
+
```typescript
|
|
6412
|
+
export function maxFiles<TForm = unknown, TField = unknown>(
|
|
6413
|
+
max: number,
|
|
6414
|
+
options?: ValidateOptions
|
|
6415
|
+
): Validator<TForm, TField>
|
|
6416
|
+
```
|
|
6417
|
+
|
|
6418
|
+
**Parameters:**
|
|
6419
|
+
- `max` — - Максимально допустимое количество файлов (включительно)
|
|
6420
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6421
|
+
попадают `maxFiles` и `actualCount`.
|
|
6422
|
+
|
|
6423
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6424
|
+
|
|
6425
|
+
**Examples:**
|
|
6426
|
+
|
|
6427
|
+
Не более трёх вложений
|
|
6428
|
+
```typescript
|
|
6429
|
+
import { maxFiles } from '@reformer/core/validators';
|
|
6430
|
+
|
|
6431
|
+
// Внутри FieldConfig схемы формы:
|
|
6432
|
+
documents: {
|
|
6433
|
+
value: model.$.documents,
|
|
6434
|
+
component: FileUploadField,
|
|
6435
|
+
validators: [maxFiles(3, { message: 'Максимум 3 файла' })],
|
|
6436
|
+
},
|
|
6437
|
+
```
|
|
6438
|
+
|
|
6439
|
+
_Source: src/form/validation/validators/max-files.ts_
|
|
6440
|
+
|
|
6441
|
+
### maxFileSize
|
|
6442
|
+
|
|
6443
|
+
**Kind:** `function`
|
|
6444
|
+
|
|
6445
|
+
Фабрика валидатора максимального размера файла.
|
|
6446
|
+
|
|
6447
|
+
Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
|
|
6448
|
+
`name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
|
|
6449
|
+
Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
|
|
6450
|
+
(например, дескриптор загруженного файла без размера) пропускаются
|
|
6451
|
+
(используйте {@link required} для обязательности).
|
|
6452
|
+
|
|
6453
|
+
**Signature:**
|
|
6454
|
+
```typescript
|
|
6455
|
+
export function maxFileSize<TForm = unknown, TField = unknown>(
|
|
6456
|
+
maxSize: number,
|
|
6457
|
+
options?: ValidateOptions
|
|
6458
|
+
): Validator<TForm, TField>
|
|
6459
|
+
```
|
|
6460
|
+
|
|
6461
|
+
**Parameters:**
|
|
6462
|
+
- `maxSize` — - Максимально допустимый размер файла в байтах (включительно)
|
|
6463
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6464
|
+
попадают `maxFileSize`, `fileName` и `actualSize`.
|
|
6465
|
+
|
|
6466
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6467
|
+
|
|
6468
|
+
**Examples:**
|
|
6469
|
+
|
|
6470
|
+
Ограничение размера вложений
|
|
6471
|
+
```typescript
|
|
6472
|
+
import { maxFileSize } from '@reformer/core/validators';
|
|
6473
|
+
|
|
6474
|
+
// Внутри FieldConfig схемы формы:
|
|
6475
|
+
documents: {
|
|
6476
|
+
value: model.$.documents,
|
|
6477
|
+
component: FileUploadField,
|
|
6478
|
+
validators: [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })],
|
|
6479
|
+
},
|
|
6480
|
+
```
|
|
6481
|
+
|
|
6482
|
+
_Source: src/form/validation/validators/max-file-size.ts_
|
|
6483
|
+
|
|
5865
6484
|
### maxLength
|
|
5866
6485
|
|
|
5867
6486
|
**Kind:** `function`
|
|
@@ -5904,6 +6523,47 @@ validators: [maxLength(500, { message: 'Максимум 500 символов' }
|
|
|
5904
6523
|
|
|
5905
6524
|
_Source: src/form/validation/validators/max-length.ts_
|
|
5906
6525
|
|
|
6526
|
+
### maxTotalFileSize
|
|
6527
|
+
|
|
6528
|
+
**Kind:** `function`
|
|
6529
|
+
|
|
6530
|
+
Фабрика валидатора суммарного размера всех файлов.
|
|
6531
|
+
|
|
6532
|
+
Складывает `size` всех файлов значения (элементы без числового `size` в сумму
|
|
6533
|
+
не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
|
|
6534
|
+
(используйте {@link required} для обязательности).
|
|
6535
|
+
|
|
6536
|
+
**Signature:**
|
|
6537
|
+
```typescript
|
|
6538
|
+
export function maxTotalFileSize<TForm = unknown, TField = unknown>(
|
|
6539
|
+
maxTotal: number,
|
|
6540
|
+
options?: ValidateOptions
|
|
6541
|
+
): Validator<TForm, TField>
|
|
6542
|
+
```
|
|
6543
|
+
|
|
6544
|
+
**Parameters:**
|
|
6545
|
+
- `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
|
|
6546
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6547
|
+
попадают `maxTotalFileSize` и `actualTotal`.
|
|
6548
|
+
|
|
6549
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6550
|
+
|
|
6551
|
+
**Examples:**
|
|
6552
|
+
|
|
6553
|
+
Суммарно не более 20 МБ
|
|
6554
|
+
```typescript
|
|
6555
|
+
import { maxTotalFileSize } from '@reformer/core/validators';
|
|
6556
|
+
|
|
6557
|
+
// Внутри FieldConfig схемы формы:
|
|
6558
|
+
documents: {
|
|
6559
|
+
value: model.$.documents,
|
|
6560
|
+
component: FileUploadField,
|
|
6561
|
+
validators: [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })],
|
|
6562
|
+
},
|
|
6563
|
+
```
|
|
6564
|
+
|
|
6565
|
+
_Source: src/form/validation/validators/max-total-file-size.ts_
|
|
6566
|
+
|
|
5907
6567
|
### min
|
|
5908
6568
|
|
|
5909
6569
|
**Kind:** `function`
|
|
@@ -6024,6 +6684,90 @@ validators: [required(), minDate(new Date(), { message: 'Дата не рань
|
|
|
6024
6684
|
|
|
6025
6685
|
_Source: src/form/validation/validators/min-date.ts_
|
|
6026
6686
|
|
|
6687
|
+
### minFiles
|
|
6688
|
+
|
|
6689
|
+
**Kind:** `function`
|
|
6690
|
+
|
|
6691
|
+
Фабрика валидатора минимального количества файлов.
|
|
6692
|
+
|
|
6693
|
+
Работает с массивом файлов (одиночный файл считается как 1). Пустые значения
|
|
6694
|
+
(`null`/`undefined`/`''`/`[]`) пропускаются — «нужен хотя бы один файл» выражается
|
|
6695
|
+
связкой `required()` + `minFiles(n)`, как и у {@link minLength}.
|
|
6696
|
+
|
|
6697
|
+
**Signature:**
|
|
6698
|
+
```typescript
|
|
6699
|
+
export function minFiles<TForm = unknown, TField = unknown>(
|
|
6700
|
+
min: number,
|
|
6701
|
+
options?: ValidateOptions
|
|
6702
|
+
): Validator<TForm, TField>
|
|
6703
|
+
```
|
|
6704
|
+
|
|
6705
|
+
**Parameters:**
|
|
6706
|
+
- `min` — - Минимально допустимое количество файлов (включительно)
|
|
6707
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6708
|
+
попадают `minFiles` и `actualCount`.
|
|
6709
|
+
|
|
6710
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6711
|
+
|
|
6712
|
+
**Examples:**
|
|
6713
|
+
|
|
6714
|
+
Не менее двух документов
|
|
6715
|
+
```typescript
|
|
6716
|
+
import { required, minFiles } from '@reformer/core/validators';
|
|
6717
|
+
|
|
6718
|
+
// Внутри FieldConfig схемы формы:
|
|
6719
|
+
documents: {
|
|
6720
|
+
value: model.$.documents,
|
|
6721
|
+
component: FileUploadField,
|
|
6722
|
+
validators: [required(), minFiles(2, { message: 'Приложите минимум 2 документа' })],
|
|
6723
|
+
},
|
|
6724
|
+
```
|
|
6725
|
+
|
|
6726
|
+
_Source: src/form/validation/validators/min-files.ts_
|
|
6727
|
+
|
|
6728
|
+
### minFileSize
|
|
6729
|
+
|
|
6730
|
+
**Kind:** `function`
|
|
6731
|
+
|
|
6732
|
+
Фабрика валидатора минимального размера файла.
|
|
6733
|
+
|
|
6734
|
+
Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
|
|
6735
|
+
`name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
|
|
6736
|
+
Типовой сценарий — отсев пустых (0-байтовых) файлов: `minFileSize(1)`.
|
|
6737
|
+
Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
|
|
6738
|
+
пропускаются (используйте {@link required} для обязательности).
|
|
6739
|
+
|
|
6740
|
+
**Signature:**
|
|
6741
|
+
```typescript
|
|
6742
|
+
export function minFileSize<TForm = unknown, TField = unknown>(
|
|
6743
|
+
minSize: number,
|
|
6744
|
+
options?: ValidateOptions
|
|
6745
|
+
): Validator<TForm, TField>
|
|
6746
|
+
```
|
|
6747
|
+
|
|
6748
|
+
**Parameters:**
|
|
6749
|
+
- `minSize` — - Минимально допустимый размер файла в байтах (включительно)
|
|
6750
|
+
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
|
|
6751
|
+
попадают `minFileSize`, `fileName` и `actualSize`.
|
|
6752
|
+
|
|
6753
|
+
**Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
|
|
6754
|
+
|
|
6755
|
+
**Examples:**
|
|
6756
|
+
|
|
6757
|
+
Отсев пустых файлов
|
|
6758
|
+
```typescript
|
|
6759
|
+
import { minFileSize } from '@reformer/core/validators';
|
|
6760
|
+
|
|
6761
|
+
// Внутри FieldConfig схемы формы:
|
|
6762
|
+
documents: {
|
|
6763
|
+
value: model.$.documents,
|
|
6764
|
+
component: FileUploadField,
|
|
6765
|
+
validators: [minFileSize(1, { message: 'Файл пустой' })],
|
|
6766
|
+
},
|
|
6767
|
+
```
|
|
6768
|
+
|
|
6769
|
+
_Source: src/form/validation/validators/min-file-size.ts_
|
|
6770
|
+
|
|
6027
6771
|
### minLength
|
|
6028
6772
|
|
|
6029
6773
|
**Kind:** `function`
|
|
@@ -6256,44 +7000,6 @@ export type ModelSignals<T> = {
|
|
|
6256
7000
|
|
|
6257
7001
|
_Source: src/state/types.ts_
|
|
6258
7002
|
|
|
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
7003
|
### ModelValue
|
|
6298
7004
|
|
|
6299
7005
|
**Kind:** `type`
|
|
@@ -6511,9 +7217,10 @@ _Source: src/form/validation/validators/non-zero.ts_
|
|
|
6511
7217
|
|
|
6512
7218
|
**Signature:**
|
|
6513
7219
|
```typescript
|
|
6514
|
-
export function pastDate<
|
|
6515
|
-
|
|
6516
|
-
|
|
7220
|
+
export function pastDate<
|
|
7221
|
+
TForm = unknown,
|
|
7222
|
+
TField extends string | Date | null | undefined = string | Date,
|
|
7223
|
+
>(options?: ValidateOptions): Validator<TForm, TField>
|
|
6517
7224
|
```
|
|
6518
7225
|
|
|
6519
7226
|
**Parameters:**
|
|
@@ -6565,7 +7272,7 @@ _Source: src/state/types.ts_
|
|
|
6565
7272
|
|
|
6566
7273
|
**Signature:**
|
|
6567
7274
|
```typescript
|
|
6568
|
-
export function pattern<TForm = unknown, TField extends string | undefined = string>(
|
|
7275
|
+
export function pattern<TForm = unknown, TField extends string | null | undefined = string>(
|
|
6569
7276
|
regex: RegExp,
|
|
6570
7277
|
options?: ValidateOptions
|
|
6571
7278
|
): Validator<TForm, TField>
|
|
@@ -6613,7 +7320,7 @@ _Source: src/form/validation/validators/pattern.ts_
|
|
|
6613
7320
|
|
|
6614
7321
|
**Signature:**
|
|
6615
7322
|
```typescript
|
|
6616
|
-
export function phone<TForm = unknown, TField extends string | undefined = string>(
|
|
7323
|
+
export function phone<TForm = unknown, TField extends string | null | undefined = string>(
|
|
6617
7324
|
options?: PhoneValidatorOptions
|
|
6618
7325
|
): Validator<TForm, TField>
|
|
6619
7326
|
```
|
|
@@ -6778,8 +7485,8 @@ _Source: src/form/types/index.ts_
|
|
|
6778
7485
|
|
|
6779
7486
|
**Kind:** `function`
|
|
6780
7487
|
|
|
6781
|
-
Вызывает `revalidate()` при изменении зависимостей (не на инициализации).
|
|
6782
|
-
|
|
7488
|
+
Вызывает `revalidate()` при изменении зависимостей (не на инициализации). Валидация on-demand
|
|
7489
|
+
(`validateModel` из `@reformer/core/validation`), поэтому ревалидация выражается явным колбэком.
|
|
6783
7490
|
|
|
6784
7491
|
**Signature:**
|
|
6785
7492
|
```typescript
|
|
@@ -6792,7 +7499,7 @@ export function revalidateWhen(
|
|
|
6792
7499
|
**Examples:**
|
|
6793
7500
|
|
|
6794
7501
|
```typescript
|
|
6795
|
-
revalidateWhen([model.$.maxAmount], () =>
|
|
7502
|
+
revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
|
|
6796
7503
|
```
|
|
6797
7504
|
|
|
6798
7505
|
_Source: src/state/behaviors-value.ts_
|
|
@@ -6935,10 +7642,10 @@ _Source: src/form/types/schema-node.ts_
|
|
|
6935
7642
|
|
|
6936
7643
|
**Kind:** `type`
|
|
6937
7644
|
|
|
6938
|
-
Валидатор поля
|
|
6939
|
-
`(value, scope, root)`,
|
|
6940
|
-
`
|
|
6941
|
-
|
|
7645
|
+
Валидатор поля схемы (legacy). Дерево-движок, вызывавший `validators` узла как
|
|
7646
|
+
`(value, scope, root)`, удалён — живые правила это `Rule<T>` в `validate(sig, [...])`
|
|
7647
|
+
(`@reformer/core/validation`). Тип остаётся широким по параметрам для совместимости
|
|
7648
|
+
узлов, всё ещё несущих `validators` (гетерогенные контракты `ValidatorFn`/`Validator`).
|
|
6942
7649
|
|
|
6943
7650
|
**Signature:**
|
|
6944
7651
|
```typescript
|
|
@@ -7113,6 +7820,27 @@ syncFields(model.$.field1, model.$.field2);
|
|
|
7113
7820
|
|
|
7114
7821
|
_Source: src/state/behaviors-value.ts_
|
|
7115
7822
|
|
|
7823
|
+
### toFileArray
|
|
7824
|
+
|
|
7825
|
+
**Kind:** `function`
|
|
7826
|
+
|
|
7827
|
+
Нормализует значение file-поля в массив файлоподобных элементов.
|
|
7828
|
+
|
|
7829
|
+
Одиночный {@link FileLike} оборачивается в массив; из массива отбираются только
|
|
7830
|
+
файлоподобные элементы. Для прочих значений — `null` (валидатор пропускает).
|
|
7831
|
+
|
|
7832
|
+
**Signature:**
|
|
7833
|
+
```typescript
|
|
7834
|
+
export function toFileArray(value: unknown): FileLike[] | null
|
|
7835
|
+
```
|
|
7836
|
+
|
|
7837
|
+
**Parameters:**
|
|
7838
|
+
- `value` — - Значение поля (`FileLike`, `FileLike[]` или что-то ещё)
|
|
7839
|
+
|
|
7840
|
+
**Returns:** Массив файлоподобных элементов или `null`
|
|
7841
|
+
|
|
7842
|
+
_Source: src/form/validation/validators/file-utils.ts_
|
|
7843
|
+
|
|
7116
7844
|
### transformValue
|
|
7117
7845
|
|
|
7118
7846
|
**Kind:** `function`
|
|
@@ -7253,7 +7981,7 @@ _Source: src/state/derived-registry.ts_
|
|
|
7253
7981
|
|
|
7254
7982
|
**Signature:**
|
|
7255
7983
|
```typescript
|
|
7256
|
-
export function url<TForm = unknown, TField extends string | undefined = string>(
|
|
7984
|
+
export function url<TForm = unknown, TField extends string | null | undefined = string>(
|
|
7257
7985
|
options?: UrlValidatorOptions
|
|
7258
7986
|
): Validator<TForm, TField>
|
|
7259
7987
|
```
|
|
@@ -7756,94 +8484,6 @@ export interface ValidateAsyncOptions extends ValidateOptions {
|
|
|
7756
8484
|
|
|
7757
8485
|
_Source: src/form/types/validation-schema.ts_
|
|
7758
8486
|
|
|
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
8487
|
### ValidateOptions
|
|
7848
8488
|
|
|
7849
8489
|
**Kind:** `interface`
|
|
@@ -7887,20 +8527,7 @@ _Source: src/form/types/contracts.ts_
|
|
|
7887
8527
|
|
|
7888
8528
|
**Kind:** `type`
|
|
7889
8529
|
|
|
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
|
-
дополнительные аргументы игнорируют.
|
|
8530
|
+
Чистый синхронный валидатор поля (legacy-сигнатура `(value, scope, root)`).
|
|
7904
8531
|
|
|
7905
8532
|
**Signature:**
|
|
7906
8533
|
```typescript
|
|
@@ -7911,23 +8538,12 @@ export type Validator<TForm, TField> = (
|
|
|
7911
8538
|
) => ValidationError | null;
|
|
7912
8539
|
```
|
|
7913
8540
|
|
|
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
|
-
```
|
|
8541
|
+
**Deprecated:** Осиротевший остаток удалённого дерево-движка (`validateFormModel`). Живой контракт —
|
|
8542
|
+
`Rule<T> = (value) => ValidationError | null` из `@reformer/core/validation`: правила
|
|
8543
|
+
передаются в `validate(sig, [rules])` внутри `defineValidationSchema` и запускаются
|
|
8544
|
+
`validateModel(model, schema)`. Cross-field — оператор `cross(sig, (f) => …)` над снапшотом
|
|
8545
|
+
`model.get()` (третий аргумент `root` больше не нужен). Тип экспортируется только ради
|
|
8546
|
+
обратной совместимости (`SchemaValidator`-union).
|
|
7931
8547
|
|
|
7932
8548
|
_Source: src/form/types/validation-schema.ts_
|
|
7933
8549
|
|