@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.
Files changed (52) hide show
  1. package/README.md +52 -57
  2. package/dist/behaviors-Br4Im38V.js +263 -0
  3. package/dist/behaviors.js +146 -146
  4. package/dist/core/factories/node-factory.d.ts +5 -4
  5. package/dist/core/model/behaviors.d.ts +15 -0
  6. package/dist/core/model/types.d.ts +41 -12
  7. package/dist/core/model/validate-model.d.ts +8 -4
  8. package/dist/core/nodes/field-node.d.ts +2 -2
  9. package/dist/core/nodes/group-node.d.ts +39 -3
  10. package/dist/core/nodes/model-array-node.d.ts +13 -1
  11. package/dist/core/types/deep-schema.d.ts +39 -5
  12. package/dist/core/types/form-proxy.d.ts +27 -2
  13. package/dist/core/types/index.d.ts +4 -10
  14. package/dist/core/types/schema-node.d.ts +79 -0
  15. package/dist/core/types/validation-schema.d.ts +37 -16
  16. package/dist/core/utils/create-form.d.ts +4 -3
  17. package/dist/core/utils/derived-registry.d.ts +29 -0
  18. package/dist/core/utils/error-handler.d.ts +4 -1
  19. package/dist/core/utils/form-submitter.d.ts +8 -4
  20. package/dist/core/utils/index.d.ts +1 -1
  21. package/dist/core/utils/safe-effect.d.ts +27 -7
  22. package/dist/core/utils/type-guards.d.ts +3 -3
  23. package/dist/core/validation/validators/date-utils.d.ts +5 -1
  24. package/dist/core/validation/validators/integer.d.ts +1 -1
  25. package/dist/core/validation/validators/is-number.d.ts +1 -1
  26. package/dist/core/validation/validators/max.d.ts +1 -1
  27. package/dist/core/validation/validators/min.d.ts +1 -1
  28. package/dist/core/validation/validators/multiple-of.d.ts +1 -1
  29. package/dist/core/validation/validators/non-negative.d.ts +1 -1
  30. package/dist/core/validation/validators/non-zero.d.ts +1 -1
  31. package/dist/core/validation/validators/required.d.ts +2 -1
  32. package/dist/{date-utils-xUWFslTj.js → date-utils-dTT_x_Be.js} +12 -10
  33. package/dist/hooks/types.d.ts +31 -1
  34. package/dist/hooks/useArrayLength.d.ts +1 -2
  35. package/dist/hooks/useFormControl.d.ts +2 -2
  36. package/dist/hooks/useSignalSubscription.d.ts +8 -1
  37. package/dist/index.js +625 -617
  38. package/dist/signals.d.ts +13 -0
  39. package/dist/signals.js +9 -0
  40. package/dist/validators/future-date.js +1 -1
  41. package/dist/validators/is-date.js +1 -1
  42. package/dist/validators/max-age.js +1 -1
  43. package/dist/validators/max-date.js +1 -1
  44. package/dist/validators/min-age.js +1 -1
  45. package/dist/validators/min-date.js +1 -1
  46. package/dist/validators/multiple-of.js +11 -7
  47. package/dist/validators/past-date.js +1 -1
  48. package/dist/validators/pattern.js +7 -6
  49. package/dist/validators/required.js +5 -9
  50. package/llms.txt +715 -175
  51. package/package.json +6 -2
  52. 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
- watchField<K extends keyof T>(fieldPath: K extends string ? K : string, callback: (value: T[K]) => void | Promise<void>): () => void;
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, FormFields, AnyFunction } from './index';
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 FormFields> {
66
+ export interface ArrayConfig<T extends object> {
39
67
  itemSchema: FormSchema<T>;
40
68
  initial?: Partial<T>[];
41
69
  }
42
70
  /**
43
- * Автоматически определяет тип схемы на основе TypeScript типа:
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 ? FieldConfig<T[K]> : NonNullable<T[K]> extends Array<infer U> ? U extends string | number | boolean ? FieldConfig<T[K]> : U extends Date | File | Blob | AnyFunction ? FieldConfig<T[K]> : [FormSchema<U>] : NonNullable<T[K]> extends Date | File | Blob | AnyFunction ? FieldConfig<T[K]> : FormSchema<NonNullable<T[K]>>;
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?: FormFields;
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?: FormFields;
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, any>;
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 { FormFields, ValidationError } from './index';
2
- import { FormProxy } from './form-proxy';
1
+ import { FormValue, ValidationError } from './index';
2
+ import { FormModel } from '../model/types';
3
3
  /**
4
4
  * Чистый синхронный валидатор поля.
5
5
  *
6
- * Принимает значение поля, прокси текущего поля (control) и прокси корня формы (root).
7
- * Возвращает ValidationError либо null. Не знает про реестр валидации.
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, control, root) => {
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, control: FormProxy<TField>, root: FormProxy<TForm>) => ValidationError | null;
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
- * Регистрируется через `validateAsync(path, validator, { debounce })`.
40
+ * @deprecated Осиротевший остаток удалённого оператора `validateAsync` (Ф7). Рантаймом не
41
+ * потребляется — живой async-путь узла поля использует `AsyncValidatorFn` `(value, { signal })`.
42
+ * Экспортируется только ради обратной совместимости.
29
43
  */
30
- export type AsyncValidator<TForm, TField> = (value: TField, control: FormProxy<TField>, root: FormProxy<TForm>) => Promise<ValidationError | null>;
44
+ export type AsyncValidator<TForm, TField> = (value: TField, scope: unknown, root: FormModel<TForm>) => Promise<ValidationError | null>;
31
45
  /**
32
- * Функция условия для applyWhen.
46
+ * Функция-предикат `(value) => boolean`.
47
+ *
48
+ * @deprecated Осиротевший остаток удалённого оператора `applyWhen` (Ф7). Рантаймом не
49
+ * потребляется; экспортируется только ради обратной совместимости.
33
50
  */
34
51
  export type ConditionFn<T> = (value: T) => boolean;
35
52
  /**
36
- * Опции для функции validate.
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?: FormFields;
60
+ /** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
61
+ params?: Record<string, FormValue>;
43
62
  }
44
63
  /**
45
- * Опции для функции validateAsync.
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?: unknown;
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?: ErrorStrategy): ValidationError | never | void;
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 FormFields> {
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 FormFields> {
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
- * Использует queueMicrotask для отложенного выполнения,
14
- * что позволяет модифицировать сигналы без вызова "Cycle detected"
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
- * Комбинирует debounce логику с выходом из effect контекста
74
+ * `withDebounce` уже откладывает вызов (таймер) — этого достаточно для выхода из effect-контекста,
75
+ * поэтому дополнительного `queueMicrotask` нет (устранён двойной defer). Сбои callback (в т.ч.
76
+ * async-rejection) маршрутизируются через {@link FormErrorHandler}.
57
77
  *
58
78
  * @param callback - Функция для выполнения
59
79
  * @param withDebounce - Функция debounce обёртки из BehaviorRegistry