@reformer/core 10.1.0 → 10.2.0

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/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. API Reference
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reformer/core",
3
- "version": "10.1.0",
3
+ "version": "10.2.0",
4
4
  "description": "Reactive form state management library for React with signals-based architecture",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",