@reformer/core 8.0.0 → 9.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -57
- package/dist/behaviors-Br4Im38V.js +263 -0
- package/dist/behaviors.js +146 -146
- package/dist/core/factories/node-factory.d.ts +5 -4
- package/dist/core/model/behaviors.d.ts +15 -0
- package/dist/core/model/types.d.ts +41 -12
- package/dist/core/model/validate-model.d.ts +8 -4
- package/dist/core/nodes/field-node.d.ts +2 -2
- package/dist/core/nodes/group-node.d.ts +39 -3
- package/dist/core/nodes/model-array-node.d.ts +13 -1
- package/dist/core/types/deep-schema.d.ts +39 -5
- package/dist/core/types/form-proxy.d.ts +27 -2
- package/dist/core/types/index.d.ts +4 -10
- package/dist/core/types/schema-node.d.ts +79 -0
- package/dist/core/types/validation-schema.d.ts +37 -16
- package/dist/core/utils/create-form.d.ts +4 -3
- package/dist/core/utils/derived-registry.d.ts +29 -0
- package/dist/core/utils/error-handler.d.ts +4 -1
- package/dist/core/utils/form-submitter.d.ts +8 -4
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/safe-effect.d.ts +27 -7
- package/dist/core/utils/type-guards.d.ts +3 -3
- package/dist/core/validation/validators/date-utils.d.ts +5 -1
- package/dist/core/validation/validators/integer.d.ts +1 -1
- package/dist/core/validation/validators/is-number.d.ts +1 -1
- package/dist/core/validation/validators/max.d.ts +1 -1
- package/dist/core/validation/validators/min.d.ts +1 -1
- package/dist/core/validation/validators/multiple-of.d.ts +1 -1
- package/dist/core/validation/validators/non-negative.d.ts +1 -1
- package/dist/core/validation/validators/non-zero.d.ts +1 -1
- package/dist/core/validation/validators/required.d.ts +2 -1
- package/dist/{date-utils-xUWFslTj.js → date-utils-dTT_x_Be.js} +12 -10
- package/dist/hooks/types.d.ts +31 -1
- package/dist/hooks/useArrayLength.d.ts +1 -2
- package/dist/hooks/useFormControl.d.ts +2 -2
- package/dist/hooks/useSignalSubscription.d.ts +8 -1
- package/dist/index.js +625 -617
- package/dist/signals.d.ts +13 -0
- package/dist/signals.js +9 -0
- package/dist/validators/future-date.js +1 -1
- package/dist/validators/is-date.js +1 -1
- package/dist/validators/max-age.js +1 -1
- package/dist/validators/max-date.js +1 -1
- package/dist/validators/min-age.js +1 -1
- package/dist/validators/min-date.js +1 -1
- package/dist/validators/multiple-of.js +11 -7
- package/dist/validators/past-date.js +1 -1
- package/dist/validators/pattern.js +7 -6
- package/dist/validators/required.js +5 -9
- package/llms.txt +715 -175
- package/package.json +6 -2
- package/dist/behaviors-O9a1Djj9.js +0 -147
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReadonlySignal } from '@preact/signals-core';
|
|
1
|
+
import { Signal, ReadonlySignal } from '@preact/signals-core';
|
|
2
2
|
import { FormNode, SetValueOptions } from './form-node';
|
|
3
3
|
import { ValidationError, FieldStatus, FormSchema, GroupNodeConfig, FormValue } from '../types';
|
|
4
4
|
import { FormProxy } from '../types/form-proxy';
|
|
@@ -54,6 +54,18 @@ export declare class GroupNode<T> extends FormNode<T> {
|
|
|
54
54
|
private readonly _disabled;
|
|
55
55
|
/** Form-level validation errors */
|
|
56
56
|
private readonly _formErrors;
|
|
57
|
+
/**
|
|
58
|
+
* M1: валидация уровня модели/схемы (`validateFormModel(model, schema)`). Привязывается
|
|
59
|
+
* `createForm({ model, schema })`, т.к. под M1 валидаторы срезаны с FieldNode и живут на слое
|
|
60
|
+
* модели. Без неё `validate()`/`submit()` пропускали бы все schema-валидаторы. @internal
|
|
61
|
+
*/
|
|
62
|
+
private _modelValidate?;
|
|
63
|
+
/**
|
|
64
|
+
* M1: связь листовой ноды-ребёнка → её сигнал модели (тот, что помечает `markDerived`).
|
|
65
|
+
* Нужна bulk-сеттерам (`setValue`/`patchValue`), чтобы корректно определять derived-поля:
|
|
66
|
+
* `field.value` — computed-обёртка, отличная от записываемого сигнала модели. @internal
|
|
67
|
+
*/
|
|
68
|
+
private readonly _fieldSignals;
|
|
57
69
|
readonly value: ReadonlySignal<T>;
|
|
58
70
|
readonly valid: ReadonlySignal<boolean>;
|
|
59
71
|
readonly invalid: ReadonlySignal<boolean>;
|
|
@@ -156,7 +168,12 @@ export declare class GroupNode<T> extends FormNode<T> {
|
|
|
156
168
|
*
|
|
157
169
|
* @param onSubmit - Callback для отправки данных
|
|
158
170
|
* @param options - Опции submit (skipValidation, skipTouch)
|
|
159
|
-
* @returns Результат от onSubmit или null если валидация не пройдена
|
|
171
|
+
* @returns Результат от onSubmit или `null` если валидация не пройдена
|
|
172
|
+
*
|
|
173
|
+
* @remarks
|
|
174
|
+
* `null` перегружен: он означает и «валидация не пройдена», и легитимный `null`-результат
|
|
175
|
+
* `onSubmit` (или void-обработчик). Если вызывающей стороне нужно различать эти случаи —
|
|
176
|
+
* используйте {@link submitWithResult}, который возвращает явный флаг `success`.
|
|
160
177
|
*/
|
|
161
178
|
submit<R>(onSubmit: (values: T) => Promise<R> | R, options?: SubmitOptions): Promise<R | null>;
|
|
162
179
|
/**
|
|
@@ -244,7 +261,13 @@ export declare class GroupNode<T> extends FormNode<T> {
|
|
|
244
261
|
* useEffect(() => dispose, []);
|
|
245
262
|
* ```
|
|
246
263
|
*/
|
|
247
|
-
|
|
264
|
+
/** Подписка на top-level поле — value типизирован как `T[K]`. */
|
|
265
|
+
watchField<K extends keyof T & string>(fieldPath: K, callback: (value: T[K]) => void | Promise<void>): () => void;
|
|
266
|
+
/**
|
|
267
|
+
* Подписка на вложенное поле по строковому пути ("address.city").
|
|
268
|
+
* Путь нельзя выразить в типах узла → value честно `unknown`, потребитель сужает.
|
|
269
|
+
*/
|
|
270
|
+
watchFieldByPath(path: string, callback: (value: unknown) => void | Promise<void>): () => void;
|
|
248
271
|
/**
|
|
249
272
|
* Hook: вызывается после disable()
|
|
250
273
|
*/
|
|
@@ -258,6 +281,19 @@ export declare class GroupNode<T> extends FormNode<T> {
|
|
|
258
281
|
* @internal
|
|
259
282
|
*/
|
|
260
283
|
attachBehaviorCleanup(cleanup: () => void): void;
|
|
284
|
+
/**
|
|
285
|
+
* Прикрепить валидацию уровня модели/схемы (M1). Вызывается `createForm({ model, schema })`,
|
|
286
|
+
* чтобы `validate()`/`submit()` прогоняли schema-валидаторы (`validateFormModel`), а не только
|
|
287
|
+
* пустые FieldNode. Возвращаемый флаг — валидна ли модель по схеме.
|
|
288
|
+
* @internal
|
|
289
|
+
*/
|
|
290
|
+
attachModelValidator(validate: () => Promise<boolean>): void;
|
|
291
|
+
/**
|
|
292
|
+
* Связать листовую ноду-ребёнка с её сигналом модели (M1). Вызывается `createForm` для каждого
|
|
293
|
+
* листового поля на его владеющей группе — используется bulk-сеттерами для derived-guard (F9).
|
|
294
|
+
* @internal
|
|
295
|
+
*/
|
|
296
|
+
registerFieldSignal(node: FormNode<any>, signal: Signal<unknown>): void;
|
|
261
297
|
/**
|
|
262
298
|
* Очистить все ресурсы узла
|
|
263
299
|
*/
|
|
@@ -58,6 +58,7 @@ export declare class ModelArrayNode<T extends object> extends FormNode<T[]> {
|
|
|
58
58
|
private readonly buildItem;
|
|
59
59
|
private readonly itemNodes;
|
|
60
60
|
private readonly cache;
|
|
61
|
+
private mountedModels;
|
|
61
62
|
private readonly initial;
|
|
62
63
|
private readonly _arrayErrors;
|
|
63
64
|
private readonly disposeSync;
|
|
@@ -104,6 +105,17 @@ export declare class ModelArrayNode<T extends object> extends FormNode<T[]> {
|
|
|
104
105
|
setErrors(errors: ValidationError[]): void;
|
|
105
106
|
clearErrors(): void;
|
|
106
107
|
protected onMarkAsTouched(): void;
|
|
107
|
-
|
|
108
|
+
protected onMarkAsUntouched(): void;
|
|
109
|
+
protected onMarkAsDirty(): void;
|
|
110
|
+
protected onMarkAsPristine(): void;
|
|
111
|
+
protected onDisable(): void;
|
|
112
|
+
protected onEnable(): void;
|
|
113
|
+
/** Диспозит форму элемента (эффекты/поведения/вложенные подписки); терпима к не-GroupNode. */
|
|
114
|
+
private disposeItemNode;
|
|
115
|
+
/**
|
|
116
|
+
* Очистка подписки синхронизации И детерминированный teardown всех форм элементов.
|
|
117
|
+
* Без пробрасывания dispose в itemNodes эффекты/поведения каждой формы элемента (в т.ч. disposeSync
|
|
118
|
+
* вложенных ModelArrayNode) остались бы жить после teardown формы — утечка подписок.
|
|
119
|
+
*/
|
|
108
120
|
dispose(): void;
|
|
109
121
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { ComponentType } from 'react';
|
|
2
2
|
import { Signal } from '@preact/signals-core';
|
|
3
|
-
import { ValidatorFn, AsyncValidatorFn,
|
|
3
|
+
import { ValidatorFn, AsyncValidatorFn, AnyFunction } from './index';
|
|
4
4
|
/**
|
|
5
5
|
* Конфигурация поля
|
|
6
6
|
* @group Types
|
|
7
7
|
* @category Configuration Types
|
|
8
|
+
* @remarks
|
|
9
|
+
* В схеме `createForm({ model, schema })` лист связывают через **`value: model.$.field`**: harvest
|
|
10
|
+
* ловит узел по идентичности `Signal` (`node.value instanceof Signal` — см. `create-form.ts`) и
|
|
11
|
+
* нормализует его в `valueSignal`. Писать `valueSignal:` **в самой схеме `createForm` нельзя** —
|
|
12
|
+
* harvest его не подхватит (проверка идёт по `node.value`), и `component`/`componentProps` этого
|
|
13
|
+
* узла молча отбросятся. `value`/`valueSignal` на прямом FieldNode-конфиге — это
|
|
14
|
+
* post-normalization / legacy-слой, а не форма записи в схеме `createForm`.
|
|
8
15
|
*/
|
|
9
16
|
export interface FieldConfig<T> {
|
|
10
17
|
/**
|
|
@@ -30,22 +37,48 @@ export interface FieldConfig<T> {
|
|
|
30
37
|
/** Задержка (в мс) перед запуском асинхронной валидации */
|
|
31
38
|
debounce?: number;
|
|
32
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* «Хотя бы одно поле обязано присутствовать»: превращает объект со всеми опциональными
|
|
42
|
+
* свойствами в объединение вариантов, где ровно один ключ обязателен, а остальные опциональны.
|
|
43
|
+
* Пустой объект `{}` не assignable ни к одному варианту и потому отклоняется.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
type RequireAtLeastOne<T> = {
|
|
48
|
+
[K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
|
|
49
|
+
}[keyof T];
|
|
50
|
+
/**
|
|
51
|
+
* {@link FieldConfig}, из которого нельзя сделать пустой `{}`.
|
|
52
|
+
*
|
|
53
|
+
* Все свойства {@link FieldConfig} опциональны, поэтому в {@link FormSchema} опечатка вида
|
|
54
|
+
* `{ email: {} }` молча проходит проверку типов и в рантайме становится пустой подгруппой
|
|
55
|
+
* вместо поля. `NonEmptyFieldConfig` требует наличия хотя бы одного свойства (value/valueSignal/
|
|
56
|
+
* component/validators/…), оставляя каждое из них опциональным по отдельности.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
type NonEmptyFieldConfig<T> = RequireAtLeastOne<FieldConfig<T>>;
|
|
33
61
|
/**
|
|
34
62
|
* Конфигурация массива
|
|
35
63
|
* @group Types
|
|
36
64
|
* @category Configuration Types
|
|
37
65
|
*/
|
|
38
|
-
export interface ArrayConfig<T extends
|
|
66
|
+
export interface ArrayConfig<T extends object> {
|
|
39
67
|
itemSchema: FormSchema<T>;
|
|
40
68
|
initial?: Partial<T>[];
|
|
41
69
|
}
|
|
42
70
|
/**
|
|
43
|
-
*
|
|
71
|
+
* **Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
|
|
72
|
+
* (или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
|
|
44
73
|
* - `T[] -> [FormSchema<T>]` (массив с одним элементом)
|
|
45
74
|
* - `object -> FormSchema<T>` (группа)
|
|
46
75
|
* - `primitive -> FieldConfig<T>` (поле)
|
|
47
76
|
*
|
|
48
|
-
* Использует NonNullable для корректной обработки опциональных
|
|
77
|
+
* Использует NonNullable для корректной обработки опциональных полей.
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
|
|
80
|
+
* контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
|
|
81
|
+
* (ключи = поля данных), а не узел дерева.
|
|
49
82
|
*
|
|
50
83
|
* @group Types
|
|
51
84
|
* @category Configuration Types
|
|
@@ -78,5 +111,6 @@ export interface ArrayConfig<T extends FormFields> {
|
|
|
78
111
|
* ```
|
|
79
112
|
*/
|
|
80
113
|
export type FormSchema<T> = {
|
|
81
|
-
[K in keyof T]: NonNullable<T[K]> extends string | number | boolean ?
|
|
114
|
+
[K in keyof T]: NonNullable<T[K]> extends string | number | boolean ? NonEmptyFieldConfig<T[K]> : NonNullable<T[K]> extends Array<infer U> ? U extends string | number | boolean ? NonEmptyFieldConfig<T[K]> : U extends Date | File | Blob | AnyFunction ? NonEmptyFieldConfig<T[K]> : [FormSchema<U>] : NonNullable<T[K]> extends Date | File | Blob | AnyFunction ? NonEmptyFieldConfig<T[K]> : FormSchema<NonNullable<T[K]>>;
|
|
82
115
|
};
|
|
116
|
+
export {};
|
|
@@ -12,7 +12,7 @@ import { ArrayNode } from '../nodes/array-node';
|
|
|
12
12
|
*
|
|
13
13
|
* @internal
|
|
14
14
|
*/
|
|
15
|
-
type IsGroupObject<V> = V extends ReadonlyArray<unknown> ? false : V extends Date | File | Blob ? false : V extends object ? true : false;
|
|
15
|
+
type IsGroupObject<V> = V extends ReadonlyArray<unknown> ? false : V extends Date | File | Blob ? false : V extends (...args: never[]) => unknown ? false : V extends object ? true : false;
|
|
16
16
|
/**
|
|
17
17
|
* Мапит тип модели данных T на правильные типы узлов формы
|
|
18
18
|
*
|
|
@@ -64,8 +64,33 @@ export type FormControlsProxy<T> = {
|
|
|
64
64
|
* form.email.setValue('test@mail.com');
|
|
65
65
|
* form.profile.name.setValue('John');
|
|
66
66
|
* ```
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* **Зарезервированные имена.** Прямой доступ `form.<name>` отдаёт приоритет собственным
|
|
70
|
+
* членам {@link GroupNode} (`value`, `status`, `id`, `errors`, `disabled`, `reset`, `submit`, …).
|
|
71
|
+
* Поэтому поле модели, названное любым из этих имён, недостижимо как `form.<name>` — там будет
|
|
72
|
+
* агрегированный сигнал/метод узла, а не {@link FieldNode}. Такие имена исключены из полевого
|
|
73
|
+
* пространства имён на уровне типов (через `Omit<…, keyof GroupNode>`), чтобы затенение не
|
|
74
|
+
* компилировалось молча. Для доступа к любому полю по имени — включая затенённые — используйте
|
|
75
|
+
* escape-hatch пространство {@link FormProxy.$}: `form.$.status`, `form.$.id`, `form.$.value`.
|
|
67
76
|
*/
|
|
68
|
-
export type FormProxy<T> = GroupNode<T> & FormControlsProxy<T
|
|
77
|
+
export type FormProxy<T> = GroupNode<T> & Omit<FormControlsProxy<T>, keyof GroupNode<T>> & {
|
|
78
|
+
/**
|
|
79
|
+
* Escape-hatch пространство имён «controls»: типобезопасный доступ ко ВСЕМ полям формы
|
|
80
|
+
* по имени, включая поля, чьи имена совпадают с членами {@link GroupNode}
|
|
81
|
+
* (`value`/`status`/`id`/`errors`/…), недостижимые через `form.<name>`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* // модель: { status: string; email: string }
|
|
86
|
+
* form.status; // ReadonlySignal<FieldStatus> — агрегат GroupNode (не поле!)
|
|
87
|
+
* form.$.status; // FieldNode<string> — поле пользователя
|
|
88
|
+
* form.$.status.setValue('active');
|
|
89
|
+
* form.$.email; // FieldNode<string> — так же доступны и незатенённые поля
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
readonly $: FormControlsProxy<T>;
|
|
93
|
+
};
|
|
69
94
|
/**
|
|
70
95
|
* Комбинированный тип для ArrayNode с Proxy доступом к элементам
|
|
71
96
|
*
|
|
@@ -70,7 +70,7 @@ export type AsyncValidatorFn<T = FormValue> = (value: T, options?: AsyncValidato
|
|
|
70
70
|
export interface ValidationError {
|
|
71
71
|
code: string;
|
|
72
72
|
message: string;
|
|
73
|
-
params?:
|
|
73
|
+
params?: Record<string, FormValue>;
|
|
74
74
|
/** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
|
|
75
75
|
severity?: 'error' | 'warning';
|
|
76
76
|
}
|
|
@@ -85,7 +85,7 @@ export interface ErrorFilterOptions {
|
|
|
85
85
|
/** Фильтр по сообщению (поддерживает частичное совпадение) */
|
|
86
86
|
message?: string;
|
|
87
87
|
/** Фильтр по параметрам ошибки */
|
|
88
|
-
params?:
|
|
88
|
+
params?: Record<string, FormValue>;
|
|
89
89
|
/** Кастомный предикат для фильтрации */
|
|
90
90
|
predicate?: (error: ValidationError) => boolean;
|
|
91
91
|
}
|
|
@@ -98,6 +98,7 @@ export type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
|
|
|
98
98
|
export type { FieldConfig } from './deep-schema';
|
|
99
99
|
export type { Validator, AsyncValidator, ConditionFn, ValidateOptions, ValidateAsyncOptions, } from './validation-schema';
|
|
100
100
|
export type { FormSchema, ArrayConfig } from './deep-schema';
|
|
101
|
+
export type { FormSchemaNode, SchemaArrayControl, SchemaValidator } from './schema-node';
|
|
101
102
|
export type { FormControlsProxy, FormProxy, FormArrayProxy } from './form-proxy';
|
|
102
103
|
/**
|
|
103
104
|
* Конфигурация GroupNode.
|
|
@@ -117,7 +118,7 @@ export interface GroupNodeConfig<T> {
|
|
|
117
118
|
* Используется вместо инлайнового `Record<string, unknown>`
|
|
118
119
|
* @internal
|
|
119
120
|
*/
|
|
120
|
-
export type UnknownRecord = Record<string,
|
|
121
|
+
export type UnknownRecord = Record<string, unknown>;
|
|
121
122
|
/**
|
|
122
123
|
* Интерфейс для узлов с методом applyValidationSchema
|
|
123
124
|
* @internal
|
|
@@ -156,13 +157,6 @@ export interface ConfigWithSchema {
|
|
|
156
157
|
export interface ConfigWithValue {
|
|
157
158
|
value: unknown;
|
|
158
159
|
}
|
|
159
|
-
/**
|
|
160
|
-
* Тип для конфига с полями (FormSchema generic constraint)
|
|
161
|
-
* Используется вместо `Record<string, any>` для схем форм
|
|
162
|
-
* @deprecated
|
|
163
|
-
* @internal
|
|
164
|
-
*/
|
|
165
|
-
export type FormFields = Record<string, FormValue>;
|
|
166
160
|
/**
|
|
167
161
|
* Тип для путей к полям (field paths)
|
|
168
162
|
* Используется в навигации по полям вместо any
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
import { ValidationError } from './index';
|
|
3
|
+
/**
|
|
4
|
+
* Валидатор поля схемы. Узел хранит validators гетерогенно: движок `validate*` вызывает их как
|
|
5
|
+
* `(value, scope, root)`, но сюда кладут любой контракт — `ValidatorFn` (value),
|
|
6
|
+
* `Validator` (value, control, root) или `ModelValidator` (value, model, root). Поэтому тип
|
|
7
|
+
* намеренно широкий по параметрам.
|
|
8
|
+
*
|
|
9
|
+
* @group Types
|
|
10
|
+
*/
|
|
11
|
+
export type SchemaValidator = (value: any, ...rest: any[]) => ValidationError | null | Promise<ValidationError | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Минимальный контракт реактивного массива модели ({@link FormSchemaNode.array}).
|
|
14
|
+
* Совпадает по форме с рантайм-фасадом `model.<array>` (см. `ModelArray`); рендерерский
|
|
15
|
+
* `RenderModelArrayControl` — его расширение (добавляет `move`).
|
|
16
|
+
*
|
|
17
|
+
* @group Types
|
|
18
|
+
*/
|
|
19
|
+
export interface SchemaArrayControl {
|
|
20
|
+
/** Путь массива в модели (dot-нотация) — нужен для резолва узла массива. */
|
|
21
|
+
readonly __path: string;
|
|
22
|
+
/** Реактивная длина. */
|
|
23
|
+
readonly length: number;
|
|
24
|
+
at(index: number): unknown;
|
|
25
|
+
push(item: unknown): void;
|
|
26
|
+
removeAt(index: number): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Узел единой схемы M1 — дерево, обходимое `createForm({ model, schema })`,
|
|
30
|
+
* `validateModel`/`validateFormModel` и рендерерами.
|
|
31
|
+
*
|
|
32
|
+
* Узел совмещает несколько ролей (различаются рантаймом по форме):
|
|
33
|
+
* - **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
|
|
34
|
+
* - **массив** — `{ array: model.<path>, item(itemModel) }`;
|
|
35
|
+
* - **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
|
|
36
|
+
* - **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).
|
|
37
|
+
*
|
|
38
|
+
* Индексная сигнатура (`[key: string]: unknown`) отражает свободный рекурсивный обход: под-узлы
|
|
39
|
+
* допустимы под любым ключом. Известные поля типизированы (даёт автокомплит и проверку их типов).
|
|
40
|
+
*
|
|
41
|
+
* @group Types
|
|
42
|
+
*/
|
|
43
|
+
export interface FormSchemaNode {
|
|
44
|
+
/**
|
|
45
|
+
* «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма
|
|
46
|
+
* зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до
|
|
47
|
+
* `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
|
|
48
|
+
*/
|
|
49
|
+
value?: unknown;
|
|
50
|
+
/** UI-компонент. Опционален: core-часть работает без UI (значение/валидация). */
|
|
51
|
+
component?: ComponentType<any>;
|
|
52
|
+
/** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
|
|
53
|
+
componentProps?: Record<string, unknown>;
|
|
54
|
+
validators?: SchemaValidator[];
|
|
55
|
+
asyncValidators?: SchemaValidator[];
|
|
56
|
+
updateOn?: 'change' | 'blur' | 'submit';
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
/** Задержка (мс) перед запуском асинхронной валидации. */
|
|
59
|
+
debounce?: number;
|
|
60
|
+
/** Идентификатор узла (для wizard/tabs/renderBehavior). */
|
|
61
|
+
selector?: string;
|
|
62
|
+
testId?: string;
|
|
63
|
+
/** Дочерние узлы (даёт контекстную типизацию вложенным литералам — value/validators/when). */
|
|
64
|
+
children?: readonly FormSchemaNode[];
|
|
65
|
+
/** Условие включения поддерева (branch-узел `{ when, children }`). */
|
|
66
|
+
when?: (scope: any, root: any) => boolean;
|
|
67
|
+
/** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */
|
|
68
|
+
array?: SchemaArrayControl;
|
|
69
|
+
/** Схема элемента массива: под-модель элемента → узел поддерева. */
|
|
70
|
+
item?: (itemModel: any) => FormSchemaNode;
|
|
71
|
+
/**
|
|
72
|
+
* Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,
|
|
73
|
+
* либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`
|
|
74
|
+
* схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.
|
|
75
|
+
*/
|
|
76
|
+
initialValue?: unknown;
|
|
77
|
+
/** Свободная вложенность: record-of-fields и произвольные под-узлы. */
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
}
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { FormValue, ValidationError } from './index';
|
|
2
|
+
import { FormModel } from '../model/types';
|
|
3
3
|
/**
|
|
4
4
|
* Чистый синхронный валидатор поля.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Сигнатура зеркалит то, что реально вызывает движок M1
|
|
7
|
+
* (`validateModel`/`validateFormModel`/`validateModelSync`): `validator(value, scope, root)`.
|
|
8
|
+
* - `value` — значение поля (`TField`);
|
|
9
|
+
* - `scope` — ближайшая scope-**модель** (под-модель элемента массива или корень). Из `TForm`/`TField`
|
|
10
|
+
* её тип не выводится, поэтому `unknown` — потребитель сужает сам (для типизированного scope
|
|
11
|
+
* используйте {@link ModelValidator}`<TField, TScope, TForm>`);
|
|
12
|
+
* - `root` — корневая модель формы `FormModel<TForm>` (реактивный value-proxy: `root.field` читает
|
|
13
|
+
* значение). Возвращает `ValidationError` либо `null`. Не знает про реестр валидации.
|
|
14
|
+
*
|
|
15
|
+
* Совместим с полем `validators` узла схемы (см. `SchemaValidator`) — там же лежат `ModelValidator`
|
|
16
|
+
* и `ValidatorFn`. Встроенные фабрики (`required()`/`email()`/…) возвращают `(value) => …` и
|
|
17
|
+
* дополнительные аргументы игнорируют.
|
|
8
18
|
*
|
|
9
19
|
* @example Кастомный валидатор в массиве `validators` поля схемы
|
|
10
20
|
* ```typescript
|
|
11
|
-
* const isAdult: Validator<MyForm, number> = (value,
|
|
21
|
+
* const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
|
|
12
22
|
* if (value < 18) return { code: 'tooYoung', message: '18+' };
|
|
23
|
+
* // root — FormModel<MyForm>: root.someOtherField читается как значение
|
|
13
24
|
* return null;
|
|
14
25
|
* };
|
|
15
26
|
*
|
|
@@ -21,30 +32,40 @@ import { FormProxy } from './form-proxy';
|
|
|
21
32
|
* };
|
|
22
33
|
* ```
|
|
23
34
|
*/
|
|
24
|
-
export type Validator<TForm, TField> = (value: TField,
|
|
35
|
+
export type Validator<TForm, TField> = (value: TField, scope: unknown, root: FormModel<TForm>) => ValidationError | null;
|
|
25
36
|
/**
|
|
26
|
-
* Чистый асинхронный валидатор поля.
|
|
37
|
+
* Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
|
|
38
|
+
* {@link Validator}, но возвращает `Promise`.
|
|
27
39
|
*
|
|
28
|
-
*
|
|
40
|
+
* @deprecated Осиротевший остаток удалённого оператора `validateAsync` (Ф7). Рантаймом не
|
|
41
|
+
* потребляется — живой async-путь узла поля использует `AsyncValidatorFn` `(value, { signal })`.
|
|
42
|
+
* Экспортируется только ради обратной совместимости.
|
|
29
43
|
*/
|
|
30
|
-
export type AsyncValidator<TForm, TField> = (value: TField,
|
|
44
|
+
export type AsyncValidator<TForm, TField> = (value: TField, scope: unknown, root: FormModel<TForm>) => Promise<ValidationError | null>;
|
|
31
45
|
/**
|
|
32
|
-
*
|
|
46
|
+
* Функция-предикат `(value) => boolean`.
|
|
47
|
+
*
|
|
48
|
+
* @deprecated Осиротевший остаток удалённого оператора `applyWhen` (Ф7). Рантаймом не
|
|
49
|
+
* потребляется; экспортируется только ради обратной совместимости.
|
|
33
50
|
*/
|
|
34
51
|
export type ConditionFn<T> = (value: T) => boolean;
|
|
35
52
|
/**
|
|
36
|
-
* Опции
|
|
53
|
+
* Опции валидатора-фабрики (`required()`/`pattern()`/…). Передаются вторым (или последним)
|
|
54
|
+
* аргументом в фабрику и попадают в возвращаемую {@link ValidationError}.
|
|
37
55
|
*/
|
|
38
56
|
export interface ValidateOptions {
|
|
39
|
-
/**
|
|
57
|
+
/** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
|
|
58
|
+
* резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */
|
|
40
59
|
message?: string;
|
|
41
|
-
/** Параметры ошибки */
|
|
42
|
-
params?:
|
|
60
|
+
/** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
|
|
61
|
+
params?: Record<string, FormValue>;
|
|
43
62
|
}
|
|
44
63
|
/**
|
|
45
|
-
*
|
|
64
|
+
* @deprecated Осиротевший остаток удалённого оператора `validateAsync` (Ф7): опция `debounce`
|
|
65
|
+
* подключалась тем оператором, которого больше нет. Рантаймом не потребляется; экспортируется
|
|
66
|
+
* только ради обратной совместимости.
|
|
46
67
|
*/
|
|
47
68
|
export interface ValidateAsyncOptions extends ValidateOptions {
|
|
48
|
-
/** Задержка перед выполнением валидации (в мс) */
|
|
69
|
+
/** Задержка перед выполнением валидации (в мс). */
|
|
49
70
|
debounce?: number;
|
|
50
71
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FormProxy, GroupNodeConfig, FormSchema } from '../types';
|
|
2
|
+
import { FormSchemaNode } from '../types/schema-node';
|
|
2
3
|
import { FormModel } from '../model/types';
|
|
3
4
|
import { FormBehavior } from '../../behaviors';
|
|
4
5
|
/**
|
|
@@ -11,10 +12,10 @@ export interface CreateFormFromModelArgs<T> {
|
|
|
11
12
|
/** Реактивная модель данных (источник истины значений). */
|
|
12
13
|
model: FormModel<T>;
|
|
13
14
|
/**
|
|
14
|
-
* Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг
|
|
15
|
-
* по идентичности сигнала (`node.value === model.$.path`). Опциональна.
|
|
15
|
+
* Единая Schema (дерево узлов {@link FormSchemaNode}). createForm обходит её и привязывает конфиг
|
|
16
|
+
* поля к ноде по идентичности сигнала (`node.value === model.$.path`). Опциональна.
|
|
16
17
|
*/
|
|
17
|
-
schema?:
|
|
18
|
+
schema?: FormSchemaNode;
|
|
18
19
|
/**
|
|
19
20
|
* Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
|
|
20
21
|
* заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
|
|
@@ -6,6 +6,9 @@ import { Signal } from '@preact/signals-core';
|
|
|
6
6
|
* bulk-сеттеры (`model.set`/`model.patch`, `patchValue`/`setValue`) пропускают это поле, чтобы
|
|
7
7
|
* значение из payload не затирало вычисляемое. Прикладной код напрямую обычно не вызывает.
|
|
8
8
|
*
|
|
9
|
+
* Идемпотентна на уровне флага: повторные вызовы наращивают счётчик ссылок, чтобы каждый оператор,
|
|
10
|
+
* пишущий в этот сигнал, был сбалансирован своим {@link unmarkDerived} при dispose.
|
|
11
|
+
*
|
|
9
12
|
* @param signal - Сигнал значения из {@link FormModel}, которым владеет compute
|
|
10
13
|
*
|
|
11
14
|
* @example
|
|
@@ -17,9 +20,35 @@ import { Signal } from '@preact/signals-core';
|
|
|
17
20
|
* ```
|
|
18
21
|
*
|
|
19
22
|
* @see {@link isDerived} - проверка пометки
|
|
23
|
+
* @see {@link unmarkDerived} - снять пометку при dispose оператора
|
|
20
24
|
* @group Utilities
|
|
21
25
|
*/
|
|
22
26
|
export declare function markDerived(signal: Signal<any>): void;
|
|
27
|
+
/**
|
|
28
|
+
* Снять пометку производного сигнала (обратная операция к {@link markDerived}).
|
|
29
|
+
*
|
|
30
|
+
* Вызывается из очистки (`onDispose`) оператора `compute`/`computeFrom`, когда поведение снимается,
|
|
31
|
+
* но модель/сигнал продолжают жить (динамическая перекоммутация: удалить под-схему → сохранить модель
|
|
32
|
+
* → bulk-set). Без этого сигнал остаётся помеченным навсегда, и bulk-сеттеры навсегда пропускают
|
|
33
|
+
* поле, которое больше ничем не вычисляется.
|
|
34
|
+
*
|
|
35
|
+
* Учитывает счётчик ссылок: пометка снимается только когда снят последний владелец. Вызов на
|
|
36
|
+
* непомеченном сигнале — безопасный no-op.
|
|
37
|
+
*
|
|
38
|
+
* @param signal - Сигнал значения из {@link FormModel}, ранее помеченный {@link markDerived}
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { markDerived, unmarkDerived } from '@reformer/core';
|
|
43
|
+
*
|
|
44
|
+
* markDerived(model.$.total);
|
|
45
|
+
* onDispose(() => unmarkDerived(model.$.total)); // при снятии compute снова разрешаем bulk-set
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @see {@link markDerived} - пометить сигнал производным
|
|
49
|
+
* @group Utilities
|
|
50
|
+
*/
|
|
51
|
+
export declare function unmarkDerived(signal: Signal<any>): void;
|
|
23
52
|
/**
|
|
24
53
|
* Производный ли сигнал (помечен через {@link markDerived}).
|
|
25
54
|
*
|
|
@@ -93,7 +93,10 @@ export declare class FormErrorHandler {
|
|
|
93
93
|
* }
|
|
94
94
|
* ```
|
|
95
95
|
*/
|
|
96
|
-
static handle(error: unknown, context: string, strategy
|
|
96
|
+
static handle(error: unknown, context: string, strategy: ErrorStrategy.CONVERT): ValidationError;
|
|
97
|
+
static handle(error: unknown, context: string, strategy: ErrorStrategy.THROW): never;
|
|
98
|
+
static handle(error: unknown, context: string, strategy: ErrorStrategy.LOG): void;
|
|
99
|
+
static handle(error: unknown, context: string): never;
|
|
97
100
|
/**
|
|
98
101
|
* Извлечь сообщение из ошибки
|
|
99
102
|
*
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { ReadonlySignal } from '@preact/signals-core';
|
|
2
|
-
import { FormFields } from '../types';
|
|
3
2
|
/**
|
|
4
3
|
* Интерфейс формы для FormSubmitter
|
|
5
4
|
* Минимальный контракт для работы с любой формой
|
|
6
5
|
*/
|
|
7
|
-
export interface SubmittableForm<T extends
|
|
6
|
+
export interface SubmittableForm<T extends object> {
|
|
8
7
|
/** Пометить все поля как touched */
|
|
9
8
|
markAsTouched(): void;
|
|
10
9
|
/** Валидировать форму */
|
|
@@ -50,7 +49,7 @@ export interface SubmitResult<R> {
|
|
|
50
49
|
* }
|
|
51
50
|
* ```
|
|
52
51
|
*/
|
|
53
|
-
export declare class FormSubmitter<T extends
|
|
52
|
+
export declare class FormSubmitter<T extends object> {
|
|
54
53
|
private readonly form;
|
|
55
54
|
/** Внутренний сигнал состояния отправки */
|
|
56
55
|
private readonly _submitting;
|
|
@@ -71,7 +70,12 @@ export declare class FormSubmitter<T extends FormFields> {
|
|
|
71
70
|
*
|
|
72
71
|
* @param onSubmit - Callback для отправки данных
|
|
73
72
|
* @param options - Опции submit
|
|
74
|
-
* @returns Результат от onSubmit или null если валидация не пройдена
|
|
73
|
+
* @returns Результат от onSubmit или `null` если валидация не пройдена
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* Возвращаемый `null` неоднозначен: он сигналит и провал валидации, и легитимный `null`,
|
|
77
|
+
* который вернул `onSubmit` (или void-обработчик). Чтобы различать эти случаи, используйте
|
|
78
|
+
* {@link submitWithResult} — он отдаёт явный флаг `success`.
|
|
75
79
|
*
|
|
76
80
|
* @example
|
|
77
81
|
* ```typescript
|
|
@@ -9,7 +9,7 @@ export { FormErrorHandler, ErrorStrategy } from './error-handler';
|
|
|
9
9
|
export { createForm, createFormFromModel } from './create-form';
|
|
10
10
|
export type { CreateFormFromModelArgs } from './create-form';
|
|
11
11
|
export { registerSignalNode, getNodeForSignal } from './signal-node-registry';
|
|
12
|
-
export { markDerived, isDerived } from './derived-registry';
|
|
12
|
+
export { markDerived, isDerived, unmarkDerived } from './derived-registry';
|
|
13
13
|
export { uniqueId } from './unique-id';
|
|
14
14
|
export { safeCallback, runOutsideEffect, safeDebouncedCallback } from './safe-effect';
|
|
15
15
|
export { FormSubmitter } from './form-submitter';
|
|
@@ -4,14 +4,28 @@
|
|
|
4
4
|
* Эти утилиты решают проблему "Cycle detected" в @preact/signals-core,
|
|
5
5
|
* которая возникает при модификации сигналов внутри effect.
|
|
6
6
|
*
|
|
7
|
+
* Отложенная запись ({@link runOutsideEffect}) откладывается на микротаск и при этом:
|
|
8
|
+
* - маршрутизирует синхронные throws и async-rejection через {@link FormErrorHandler} вместо того,
|
|
9
|
+
* чтобы они всплывали неперехваченными в точке микротаска / тонули как unhandledRejection;
|
|
10
|
+
* - возвращает отменитель (canceller) для liveness — вызывающий может отменить ещё не выполненную
|
|
11
|
+
* запись при dispose, чтобы она не сработала против снесённого узла/формы.
|
|
12
|
+
*
|
|
13
|
+
* NB: намеренно НЕ оборачиваем записи в `batch()`. Реципрокные guard'ы слоя данных (например
|
|
14
|
+
* `syncFields` в {@link module:core/model/behaviors}) держат флаг «идёт синхронизация» и сбрасывают его
|
|
15
|
+
* в `finally` ПОСЛЕ записи, полагаясь на то, что встречный эффект флашится СИНХРОННО во время записи
|
|
16
|
+
* (вне batch). `batch()` отложил бы встречный флаш за пределы сброса флага и сломал бы эту защиту
|
|
17
|
+
* (сходимость осталась бы за peek-guard'ом, но появились бы лишние круги). Коалесинг записей в один
|
|
18
|
+
* batch — законная оптимизация, но она требует, чтобы эти guard'ы стали batch-aware, поэтому делается
|
|
19
|
+
* не здесь. См. также `aggregateInto`, где коалесинг реализован на уровне самого оператора.
|
|
20
|
+
*
|
|
7
21
|
* @group Utils
|
|
8
22
|
* @module utils/safe-effect
|
|
9
23
|
*/
|
|
10
24
|
/**
|
|
11
25
|
* Создает callback, который выполняется вне контекста effect
|
|
12
26
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
27
|
+
* Откладывает вызов через {@link runOutsideEffect} — то есть получает маршрутизацию ошибок и
|
|
28
|
+
* обработку async-rejection «бесплатно».
|
|
15
29
|
*
|
|
16
30
|
* @param callback - Функция для выполнения
|
|
17
31
|
* @returns Обёрнутая функция, безопасная для вызова внутри effect
|
|
@@ -33,27 +47,33 @@
|
|
|
33
47
|
*/
|
|
34
48
|
export declare function safeCallback<TArgs extends unknown[]>(callback: (...args: TArgs) => void | Promise<void>): (...args: TArgs) => void;
|
|
35
49
|
/**
|
|
36
|
-
* Выполняет функцию вне контекста effect
|
|
50
|
+
* Выполняет функцию вне контекста effect (отложенная запись на микротаск).
|
|
37
51
|
*
|
|
38
|
-
*
|
|
52
|
+
* Сбои fn (синхронный throw и async-rejection) маршрутизируются через {@link FormErrorHandler}, а не
|
|
53
|
+
* всплывают неперехваченными. Возвращает отменитель: вызов до срабатывания микротаска отменяет запись
|
|
54
|
+
* (liveness-охрана — не писать против снесённого узла при dispose в том же тике).
|
|
39
55
|
*
|
|
40
56
|
* @param fn - Функция для выполнения
|
|
57
|
+
* @returns Отменитель ещё не выполненной записи (no-op, если запись уже выполнена)
|
|
41
58
|
*
|
|
42
59
|
* @example
|
|
43
60
|
* ```typescript
|
|
44
61
|
* effect(() => {
|
|
45
62
|
* const value = signal.value;
|
|
46
|
-
* runOutsideEffect(() => {
|
|
63
|
+
* const cancel = runOutsideEffect(() => {
|
|
47
64
|
* otherSignal.value = transform(value);
|
|
48
65
|
* });
|
|
66
|
+
* onDispose(cancel); // отменить ожидающую запись, если узел снесут в этом же тике
|
|
49
67
|
* });
|
|
50
68
|
* ```
|
|
51
69
|
*/
|
|
52
|
-
export declare function runOutsideEffect(fn: () => void | Promise<void>): void;
|
|
70
|
+
export declare function runOutsideEffect(fn: () => void | Promise<void>): () => void;
|
|
53
71
|
/**
|
|
54
72
|
* Создает версию callback с поддержкой debounce, безопасную для effect
|
|
55
73
|
*
|
|
56
|
-
*
|
|
74
|
+
* `withDebounce` уже откладывает вызов (таймер) — этого достаточно для выхода из effect-контекста,
|
|
75
|
+
* поэтому дополнительного `queueMicrotask` нет (устранён двойной defer). Сбои callback (в т.ч.
|
|
76
|
+
* async-rejection) маршрутизируются через {@link FormErrorHandler}.
|
|
57
77
|
*
|
|
58
78
|
* @param callback - Функция для выполнения
|
|
59
79
|
* @param withDebounce - Функция debounce обёртки из BehaviorRegistry
|