@reformer/core 10.0.0-beta.1 → 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 +274 -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
|
|
@@ -1920,6 +1921,11 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
|
|
|
1920
1921
|
|
|
1921
1922
|
### Integration with UI libraries (shadcn etc.)
|
|
1922
1923
|
|
|
1924
|
+
> **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
|
|
1925
|
+
> `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
|
|
1926
|
+
> компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
|
|
1927
|
+
> контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
|
|
1928
|
+
|
|
1923
1929
|
Если есть существующая design system — оборачивай её компоненты в один
|
|
1924
1930
|
`MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
|
|
1925
1931
|
тип input'а — пусть `componentProps.type` диспатчит внутри.
|
|
@@ -4108,7 +4114,199 @@ loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: tr
|
|
|
4108
4114
|
- [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
|
|
4109
4115
|
- [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)
|
|
4110
4116
|
|
|
4111
|
-
## 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
|
|
4112
4310
|
|
|
4113
4311
|
_Auto-generated from JSDoc on public exports._
|
|
4114
4312
|
|
|
@@ -8466,6 +8664,81 @@ return (
|
|
|
8466
8664
|
|
|
8467
8665
|
_Source: src/form/hooks/useFormControlValue.ts_
|
|
8468
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
|
+
|
|
8469
8742
|
### ValidateAsyncOptions
|
|
8470
8743
|
|
|
8471
8744
|
**Kind:** `interface`
|
package/package.json
CHANGED