@reformer/core 10.0.0-beta.2 → 10.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{behaviors-node-DUrqAqp1.js → behaviors-node-DGEFMTQu.js} +1 -1
- package/dist/{behaviors-value-lCpqicfa.js → behaviors-value-C7dSEhCc.js} +43 -57
- package/dist/behaviors.js +108 -107
- package/dist/derived-registry-CluZCQ7U.js +16 -0
- package/dist/form/hooks/use-form-validation.d.ts +48 -0
- package/dist/form/index.d.ts +2 -0
- package/dist/form/validation-schema.d.ts +16 -3
- package/dist/form/validation-strategy.d.ts +55 -0
- package/dist/form-model-DYCzGGKF.js +286 -0
- package/dist/index.js +198 -181
- package/dist/state/form-model.d.ts +12 -1
- package/dist/state.js +17 -15
- package/dist/subscription-manager-705bls97.js +171 -0
- package/dist/validation.js +133 -81
- package/llms.txt +269 -1
- package/package.json +1 -1
- package/dist/form-model-C8Fi83k4.js +0 -439
package/llms.txt
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
- 30-type-safety-recipes.md — type-safety-recipes
|
|
38
38
|
- 31-async-validator-debounce.md — async-validator-debounce
|
|
39
39
|
- 32-async-options-loading.md — async-options-loading
|
|
40
|
+
- 33-validation-strategy.md — useFormValidation — Единый выбор стратегии валидации
|
|
40
41
|
- API Reference (auto-generated from JSDoc)
|
|
41
42
|
|
|
42
43
|
## 1. 1. API Reference
|
|
@@ -4113,7 +4114,199 @@ loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: tr
|
|
|
4113
4114
|
- [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
|
|
4114
4115
|
- [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)
|
|
4115
4116
|
|
|
4116
|
-
## 77.
|
|
4117
|
+
## 77. Purpose
|
|
4118
|
+
|
|
4119
|
+
Одна декларативная точка выбора «когда прогонять схему валидации» вместо ручной разводки
|
|
4120
|
+
`markAsTouched` + `validateModel` + `revalidateWhen` + `useState(pending)`. Аддитивный слой над
|
|
4121
|
+
функциональной схемой (`@reformer/core/validation`): переиспользует `validateModel` как
|
|
4122
|
+
ЕДИНСТВЕННЫЙ движок (роутинг ошибок в ноды, отмена устаревших прогонов, дедуп по `(model, schema)`),
|
|
4123
|
+
второго движка не заводит. Node-level `updateOn` / `debounce` на самой ноде поля (legacy-триггеры)
|
|
4124
|
+
этот API НЕ трогает.
|
|
4125
|
+
|
|
4126
|
+
Стратегия определяет только момент запуска и раскрытие ошибок — сами правила остаются в отдельной
|
|
4127
|
+
`defineValidationSchema<T>(({ model }) => …)`.
|
|
4128
|
+
|
|
4129
|
+
## 78. API
|
|
4130
|
+
|
|
4131
|
+
Основной способ — React-хук `useFormValidation` из `@reformer/core` (рядом с `useFormControl`).
|
|
4132
|
+
Под ним — headless-фабрика `createFormValidation` из `@reformer/core/validation` (для SSR / тестов /
|
|
4133
|
+
не-React потребителей).
|
|
4134
|
+
|
|
4135
|
+
```typescript
|
|
4136
|
+
// React (@reformer/core)
|
|
4137
|
+
function useFormValidation<T>(args: {
|
|
4138
|
+
model: FormModel<T>;
|
|
4139
|
+
schema: ValidationSchema<T>; // СТАБИЛЬНАЯ ссылка (module-level const / useMemo)
|
|
4140
|
+
strategy?: ValidationStrategyKind; // default 'submit'
|
|
4141
|
+
debounce?: number; // мс, для live-фаз (change / live-часть afterFirstSubmit)
|
|
4142
|
+
liveAfterSubmit?: 'change' | 'blur'; // default 'change'
|
|
4143
|
+
}): { submit: () => Promise<boolean>; validate: () => Promise<boolean>; isValidating: boolean };
|
|
4144
|
+
|
|
4145
|
+
// headless (@reformer/core/validation)
|
|
4146
|
+
type ValidationStrategyKind = 'submit' | 'blur' | 'change' | 'afterFirstSubmit';
|
|
4147
|
+
interface ValidationStrategyOptions {
|
|
4148
|
+
strategy?: ValidationStrategyKind;
|
|
4149
|
+
debounce?: number;
|
|
4150
|
+
liveAfterSubmit?: 'change' | 'blur';
|
|
4151
|
+
}
|
|
4152
|
+
interface FormValidationController {
|
|
4153
|
+
validate(): Promise<boolean>; // полный прогон, touch:true (раскрыть ВСЕ ошибки); переводит afterFirstSubmit в live-фазу
|
|
4154
|
+
start(): () => void; // армит реактивные подписки (ТОЛЬКО на клиенте) → dispose
|
|
4155
|
+
dispose(): void;
|
|
4156
|
+
readonly isValidating: boolean;
|
|
4157
|
+
readonly validating: ReadonlySignal<boolean>;
|
|
4158
|
+
}
|
|
4159
|
+
function createFormValidation<T>(
|
|
4160
|
+
model: FormModel<T>,
|
|
4161
|
+
schema: ValidationSchema<T>,
|
|
4162
|
+
options?: ValidationStrategyOptions,
|
|
4163
|
+
): FormValidationController;
|
|
4164
|
+
```
|
|
4165
|
+
|
|
4166
|
+
- `useFormValidation` мемоизирует контроллер по `[model, schema, strategy, debounce, liveAfterSubmit]`,
|
|
4167
|
+
армит стратегию в `useEffect` (SSR-safe), а `submit()` = `controller.validate()`.
|
|
4168
|
+
- `createFormValidation` ЧИСТА до `start()` (никаких подписок) → SSR-safe; `start()` возвращает
|
|
4169
|
+
`dispose` и арминует триггеры только на клиенте.
|
|
4170
|
+
|
|
4171
|
+
### Стратегии
|
|
4172
|
+
|
|
4173
|
+
| strategy | когда прогон | touch | раскрытие ошибок |
|
|
4174
|
+
| --- | --- | --- | --- |
|
|
4175
|
+
| `submit` (default) | только `validate()` / `submit()` | `true` | всё на submit |
|
|
4176
|
+
| `blur` | смена `touched` поля (потеря фокуса) | `false` | только сблюренные (по `touched`) |
|
|
4177
|
+
| `change` | на каждый ввод (+ `debounce`) | `false` | только редактированные (по `dirty`) |
|
|
4178
|
+
| `afterFirstSubmit` | тихо до 1-го submit, затем live (`liveAfterSubmit: 'change'` \| `'blur'`, default `'change'`) | `true` один раз на 1-м submit, дальше `false` | тихо → раскрыть всё на submit → далее live |
|
|
4179
|
+
|
|
4180
|
+
## 79. Examples
|
|
4181
|
+
|
|
4182
|
+
### Простая форма — afterFirstSubmit + debounce
|
|
4183
|
+
|
|
4184
|
+
Тихо до первой отправки, `submit()` раскрывает все ошибки и переводит форму в живую фазу
|
|
4185
|
+
(дальше проверка на ввод с задержкой 400 мс). `submit()` сам метит `touched` — ручной
|
|
4186
|
+
`markAsTouched` не нужен.
|
|
4187
|
+
|
|
4188
|
+
```tsx
|
|
4189
|
+
import { createModel, createForm, useFormValidation } from '@reformer/core';
|
|
4190
|
+
import { defineValidationSchema, validate } from '@reformer/core/validation';
|
|
4191
|
+
import { required, email, minLength } from '@reformer/core/validators';
|
|
4192
|
+
|
|
4193
|
+
type RegistrationData = { username: string; email: string; password: string };
|
|
4194
|
+
|
|
4195
|
+
// Схема — стабильный module-level const (иначе ломается дедуп раннера).
|
|
4196
|
+
const registrationValidation = defineValidationSchema<RegistrationData>(({ model }) => {
|
|
4197
|
+
validate(model.$.username, [required(), minLength(3)]);
|
|
4198
|
+
validate(model.$.email, [required(), email()]);
|
|
4199
|
+
validate(model.$.password, [required(), minLength(8)]);
|
|
4200
|
+
});
|
|
4201
|
+
|
|
4202
|
+
function RegistrationForm() {
|
|
4203
|
+
const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);
|
|
4204
|
+
const form = useMemo(() => createForm({ model }), [model]);
|
|
4205
|
+
|
|
4206
|
+
const { submit, isValidating } = useFormValidation({
|
|
4207
|
+
model,
|
|
4208
|
+
schema: registrationValidation,
|
|
4209
|
+
strategy: 'afterFirstSubmit',
|
|
4210
|
+
debounce: 400,
|
|
4211
|
+
});
|
|
4212
|
+
|
|
4213
|
+
const onSubmit = async (e: React.FormEvent) => {
|
|
4214
|
+
e.preventDefault();
|
|
4215
|
+
if (await submit()) await api.save(model.get()); // true = нет блокирующих ошибок
|
|
4216
|
+
};
|
|
4217
|
+
|
|
4218
|
+
return (
|
|
4219
|
+
<form onSubmit={onSubmit}>
|
|
4220
|
+
<FormField control={form.username} />
|
|
4221
|
+
<FormField control={form.email} />
|
|
4222
|
+
<FormField control={form.password} />
|
|
4223
|
+
<button type="submit" disabled={isValidating}>Отправить</button>
|
|
4224
|
+
</form>
|
|
4225
|
+
);
|
|
4226
|
+
}
|
|
4227
|
+
```
|
|
4228
|
+
|
|
4229
|
+
### Wizard — live-стратегия внутри активного шага
|
|
4230
|
+
|
|
4231
|
+
`defineSteps(..., { strategy })` задаёт ЖИВУЮ стратегию ВНУТРИ шага; `useWizardStepValidation`
|
|
4232
|
+
арминует её под текущий шаг (`currentStep` из `FormWizardContext`) и снимает при смене шага / unmount.
|
|
4233
|
+
Per-step gate (`validateStep` на «Далее») и `validateAll` (submit) при этом НЕ меняются — стратегия
|
|
4234
|
+
добавляет живой слой ПОВЕРХ них. No-op, если `strategy` не задана / `'submit'` / у шага нет правил.
|
|
4235
|
+
|
|
4236
|
+
```tsx
|
|
4237
|
+
import { defineSteps, useWizardStepValidation, FormWizard } from '@reformer/cdk/form-wizard';
|
|
4238
|
+
|
|
4239
|
+
const config = defineSteps<'loan' | 'applicant' | 'confirm', CreditForm>(model, {
|
|
4240
|
+
steps: { loan: loanStep, applicant: applicantStep, confirm: null }, // confirm — без правил, ЯВНО
|
|
4241
|
+
extras: crossFieldRules, // применяются только в validateAll (submit)
|
|
4242
|
+
strategy: 'blur', // живая валидация полей текущего шага при потере фокуса
|
|
4243
|
+
});
|
|
4244
|
+
|
|
4245
|
+
function LoanStepBody() {
|
|
4246
|
+
useWizardStepValidation(config); // армит 'blur' под активный шаг, снимает при переходе
|
|
4247
|
+
return <>{/* поля шага */}</>;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
<FormWizard form={form} config={config}>
|
|
4251
|
+
<LoanStepBody />
|
|
4252
|
+
</FormWizard>;
|
|
4253
|
+
```
|
|
4254
|
+
|
|
4255
|
+
### Headless / SSR / тесты — createFormValidation
|
|
4256
|
+
|
|
4257
|
+
```typescript
|
|
4258
|
+
import { createFormValidation } from '@reformer/core/validation';
|
|
4259
|
+
|
|
4260
|
+
const ctrl = createFormValidation(model, registrationValidation, {
|
|
4261
|
+
strategy: 'afterFirstSubmit',
|
|
4262
|
+
debounce: 300,
|
|
4263
|
+
});
|
|
4264
|
+
const dispose = ctrl.start(); // ТОЛЬКО на клиенте (в React — из useEffect)
|
|
4265
|
+
const ok = await ctrl.validate(); // submit → раскрыть все ошибки
|
|
4266
|
+
dispose();
|
|
4267
|
+
```
|
|
4268
|
+
|
|
4269
|
+
## 80. Anti-patterns
|
|
4270
|
+
|
|
4271
|
+
```typescript
|
|
4272
|
+
// ❌ schema пересоздаётся каждый рендер → контроллер и дедуп раннера ломаются
|
|
4273
|
+
function MyForm() {
|
|
4274
|
+
const schema = defineValidationSchema<Form>(({ model }) => { … }); // новая ссылка на каждый рендер
|
|
4275
|
+
useFormValidation({ model, schema, strategy: 'change' });
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
// ✅ Стабильная ссылка: module-level const (или useMemo с пустыми deps)
|
|
4279
|
+
const formValidation = defineValidationSchema<Form>(({ model }) => { … });
|
|
4280
|
+
function MyForm() {
|
|
4281
|
+
useFormValidation({ model, schema: formValidation, strategy: 'change' });
|
|
4282
|
+
}
|
|
4283
|
+
```
|
|
4284
|
+
|
|
4285
|
+
```typescript
|
|
4286
|
+
// ❌ Node-level updateOn (Слой B) И активная schema-стратегия на ОДНОМ поле → оба пишут ошибки в ноду → мерцание
|
|
4287
|
+
// узел поля: { updateOn: 'blur', … } + useFormValidation({ strategy: 'blur', schema: включает это же поле })
|
|
4288
|
+
|
|
4289
|
+
// ✅ Одно поле обслуживает ОДИН слой: либо node-level updateOn, либо schema-стратегия — не оба сразу
|
|
4290
|
+
```
|
|
4291
|
+
|
|
4292
|
+
```typescript
|
|
4293
|
+
// ❌ start() на сервере — арминует реактивные подписки при SSR
|
|
4294
|
+
const ctrl = createFormValidation(model, schema, { strategy: 'change' });
|
|
4295
|
+
ctrl.start(); // на сервере подписок быть не должно
|
|
4296
|
+
|
|
4297
|
+
// ✅ Фабрика чиста до start(); армить только на клиенте (useFormValidation делает это в useEffect)
|
|
4298
|
+
const ctrl = createFormValidation(model, schema, { strategy: 'change' });
|
|
4299
|
+
if (typeof window !== 'undefined') ctrl.start();
|
|
4300
|
+
```
|
|
4301
|
+
|
|
4302
|
+
## 81. See also
|
|
4303
|
+
|
|
4304
|
+
- [28-submit-and-reset.md](./28-submit-and-reset.md) — submit-флоу и `validateModel` (движок под стратегией)
|
|
4305
|
+
- [27-revalidate-when.md](./27-revalidate-when.md) — ручные триггеры перевалидации (низкоуровневый примитив)
|
|
4306
|
+
- [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов
|
|
4307
|
+
- [13-multi-step.md](./13-multi-step.md) — per-step / полная валидация wizard (`validateStep` / `validateAll`)
|
|
4308
|
+
|
|
4309
|
+
## 82. API Reference
|
|
4117
4310
|
|
|
4118
4311
|
_Auto-generated from JSDoc on public exports._
|
|
4119
4312
|
|
|
@@ -8471,6 +8664,81 @@ return (
|
|
|
8471
8664
|
|
|
8472
8665
|
_Source: src/form/hooks/useFormControlValue.ts_
|
|
8473
8666
|
|
|
8667
|
+
### useFormValidation
|
|
8668
|
+
|
|
8669
|
+
**Kind:** `function`
|
|
8670
|
+
|
|
8671
|
+
Декларативный выбор стратегии валидации формы: одна точка вместо ручной разводки
|
|
8672
|
+
`validateModel`/`revalidateWhen`. Собирает стабильный {@link createFormValidation}-контроллер,
|
|
8673
|
+
армит реактивную стратегию на клиенте (`useEffect` → SSR-safe) и отдаёт `submit`/`isValidating`.
|
|
8674
|
+
|
|
8675
|
+
Схема-валидации — **стабильная ссылка** (обязанность вызывающего): контроллер мемоизируется по
|
|
8676
|
+
`[model, schema, strategy, debounce, liveAfterSubmit]`.
|
|
8677
|
+
|
|
8678
|
+
**Signature:**
|
|
8679
|
+
```typescript
|
|
8680
|
+
export function useFormValidation<T>(args: UseFormValidationArgs<T>): UseFormValidationResult
|
|
8681
|
+
```
|
|
8682
|
+
|
|
8683
|
+
**Examples:**
|
|
8684
|
+
|
|
8685
|
+
```tsx
|
|
8686
|
+
const validation = defineValidationSchema<Form>(({ model }) => { ... }); // module-level
|
|
8687
|
+
|
|
8688
|
+
function MyForm() {
|
|
8689
|
+
const { submit, isValidating } = useFormValidation({
|
|
8690
|
+
model, schema: validation, strategy: 'afterFirstSubmit', debounce: 300,
|
|
8691
|
+
});
|
|
8692
|
+
const onSubmit = async (e: React.FormEvent) => {
|
|
8693
|
+
e.preventDefault();
|
|
8694
|
+
if (await submit()) await api.save(model.get());
|
|
8695
|
+
};
|
|
8696
|
+
return <form onSubmit={onSubmit}>{ ... }<button disabled={isValidating}>Отправить</button></form>;
|
|
8697
|
+
}
|
|
8698
|
+
```
|
|
8699
|
+
|
|
8700
|
+
_Source: src/form/hooks/use-form-validation.ts_
|
|
8701
|
+
|
|
8702
|
+
### UseFormValidationArgs
|
|
8703
|
+
|
|
8704
|
+
**Kind:** `interface`
|
|
8705
|
+
|
|
8706
|
+
**Signature:**
|
|
8707
|
+
```typescript
|
|
8708
|
+
export interface UseFormValidationArgs<T> {
|
|
8709
|
+
/** Модель данных формы. */
|
|
8710
|
+
model: FormModel<T>;
|
|
8711
|
+
/** Схема валидации. **Стабильная ссылка** (module-level `const` / `useMemo`) — иначе ломается дедуп раннера. */
|
|
8712
|
+
schema: ValidationSchema<T>;
|
|
8713
|
+
/** Стратегия запуска. Default `'submit'`. */
|
|
8714
|
+
strategy?: ValidationStrategyKind;
|
|
8715
|
+
/** Debounce (мс) для live-фаз (`change` / live-часть `afterFirstSubmit`). */
|
|
8716
|
+
debounce?: number;
|
|
8717
|
+
/** Режим live-фазы для `afterFirstSubmit`. Default `'change'`. */
|
|
8718
|
+
liveAfterSubmit?: 'change' | 'blur';
|
|
8719
|
+
}
|
|
8720
|
+
```
|
|
8721
|
+
|
|
8722
|
+
_Source: src/form/hooks/use-form-validation.ts_
|
|
8723
|
+
|
|
8724
|
+
### UseFormValidationResult
|
|
8725
|
+
|
|
8726
|
+
**Kind:** `interface`
|
|
8727
|
+
|
|
8728
|
+
**Signature:**
|
|
8729
|
+
```typescript
|
|
8730
|
+
export interface UseFormValidationResult {
|
|
8731
|
+
/** Полный прогон + раскрытие ошибок (`touch:true`); для `onSubmit`. Стабильная ссылка. */
|
|
8732
|
+
submit: () => Promise<boolean>;
|
|
8733
|
+
/** Алиас `submit` для не-submit сценариев. */
|
|
8734
|
+
validate: () => Promise<boolean>;
|
|
8735
|
+
/** Идёт ли прогон (submit или live) — для блокировки кнопки. */
|
|
8736
|
+
isValidating: boolean;
|
|
8737
|
+
}
|
|
8738
|
+
```
|
|
8739
|
+
|
|
8740
|
+
_Source: src/form/hooks/use-form-validation.ts_
|
|
8741
|
+
|
|
8474
8742
|
### ValidateAsyncOptions
|
|
8475
8743
|
|
|
8476
8744
|
**Kind:** `interface`
|
package/package.json
CHANGED