@reformer/core 8.0.0 → 9.0.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.
Files changed (40) hide show
  1. package/dist/core/factories/node-factory.d.ts +5 -4
  2. package/dist/core/model/types.d.ts +41 -12
  3. package/dist/core/model/validate-model.d.ts +8 -4
  4. package/dist/core/nodes/field-node.d.ts +2 -2
  5. package/dist/core/nodes/group-node.d.ts +7 -1
  6. package/dist/core/nodes/model-array-node.d.ts +5 -0
  7. package/dist/core/types/deep-schema.d.ts +16 -4
  8. package/dist/core/types/form-proxy.d.ts +1 -1
  9. package/dist/core/types/index.d.ts +4 -10
  10. package/dist/core/types/schema-node.d.ts +79 -0
  11. package/dist/core/types/validation-schema.d.ts +22 -11
  12. package/dist/core/utils/create-form.d.ts +4 -3
  13. package/dist/core/utils/error-handler.d.ts +4 -1
  14. package/dist/core/utils/form-submitter.d.ts +2 -3
  15. package/dist/core/utils/type-guards.d.ts +3 -3
  16. package/dist/core/validation/validators/date-utils.d.ts +5 -1
  17. package/dist/core/validation/validators/integer.d.ts +1 -1
  18. package/dist/core/validation/validators/is-number.d.ts +1 -1
  19. package/dist/core/validation/validators/max.d.ts +1 -1
  20. package/dist/core/validation/validators/min.d.ts +1 -1
  21. package/dist/core/validation/validators/multiple-of.d.ts +1 -1
  22. package/dist/core/validation/validators/non-negative.d.ts +1 -1
  23. package/dist/core/validation/validators/non-zero.d.ts +1 -1
  24. package/dist/{date-utils-xUWFslTj.js → date-utils-dTT_x_Be.js} +12 -10
  25. package/dist/hooks/types.d.ts +1 -1
  26. package/dist/hooks/useArrayLength.d.ts +1 -2
  27. package/dist/hooks/useFormControl.d.ts +2 -2
  28. package/dist/index.js +514 -514
  29. package/dist/signals.d.ts +13 -0
  30. package/dist/signals.js +9 -0
  31. package/dist/validators/future-date.js +1 -1
  32. package/dist/validators/is-date.js +1 -1
  33. package/dist/validators/max-age.js +1 -1
  34. package/dist/validators/max-date.js +1 -1
  35. package/dist/validators/min-age.js +1 -1
  36. package/dist/validators/min-date.js +1 -1
  37. package/dist/validators/multiple-of.js +11 -7
  38. package/dist/validators/past-date.js +1 -1
  39. package/llms.txt +568 -155
  40. package/package.json +6 -2
@@ -1,4 +1,5 @@
1
1
  import { FormNode } from '../nodes/form-node';
2
+ import { ConfigWithSchema, ConfigWithValue, UnknownRecord } from '../types';
2
3
  /**
3
4
  * Фабрика для создания узлов формы.
4
5
  *
@@ -59,7 +60,7 @@ export declare class NodeFactory {
59
60
  * ]);
60
61
  * ```
61
62
  */
62
- createNode<T>(config: unknown): FormNode<T>;
63
+ createNode(config: unknown): FormNode<unknown>;
63
64
  /**
64
65
  * Создать ArrayNode из массива [schema, ...initialItems]
65
66
  *
@@ -135,7 +136,7 @@ export declare class NodeFactory {
135
136
  * factory.isFieldConfig(null); // false
136
137
  * ```
137
138
  */
138
- isFieldConfig(config: unknown): boolean;
139
+ isFieldConfig(config: unknown): config is ConfigWithValue;
139
140
  /**
140
141
  * Проверяет, является ли конфиг конфигурацией массива (ArrayConfig)
141
142
  *
@@ -157,7 +158,7 @@ export declare class NodeFactory {
157
158
  * factory.isArrayConfig({ email: { value: '' } }); // false
158
159
  * ```
159
160
  */
160
- isArrayConfig(config: unknown): boolean;
161
+ isArrayConfig(config: unknown): config is ConfigWithSchema;
161
162
  /**
162
163
  * Проверяет, является ли конфиг конфигурацией группы (GroupConfig)
163
164
  *
@@ -183,5 +184,5 @@ export declare class NodeFactory {
183
184
  * factory.isGroupConfig(null); // false
184
185
  * ```
185
186
  */
186
- isGroupConfig(config: unknown): boolean;
187
+ isGroupConfig(config: unknown): config is UnknownRecord;
187
188
  }
@@ -14,6 +14,14 @@ export type PathAwareSignal<T> = Signal<T> & {
14
14
  * @internal
15
15
  */
16
16
  type Opaque = Date | File | Blob;
17
+ /**
18
+ * Фасад элемента массива при обходе (`at`/`map`/`forEach`): массив → {@link ModelArray},
19
+ * Opaque (Date/File/Blob) и примитив → значение, объект → под-модель {@link FormModel}.
20
+ * Зеркалит листовой/объектный сплит {@link ModelValue} (но объект → под-модель с API),
21
+ * чтобы `files: File[]`/`dates: Date[]`/вложенные массивы не мис-типизировались как `FormModel`.
22
+ * @internal
23
+ */
24
+ type ModelArrayItem<U> = NonNullable<U> extends ReadonlyArray<infer E> ? ModelArray<E> : NonNullable<U> extends Opaque ? U : NonNullable<U> extends object ? FormModel<NonNullable<U>> : U;
17
25
  /**
18
26
  * Значение поля в value-доступе модели:
19
27
  * - массив → {@link ModelArray}
@@ -40,6 +48,11 @@ export type ModelObject<T> = {
40
48
  * @group Model
41
49
  */
42
50
  export interface ModelArray<U> {
51
+ /**
52
+ * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
53
+ * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
54
+ */
55
+ readonly __path: string;
43
56
  /** Реактивная длина. */
44
57
  readonly length: number;
45
58
  /** Добавить элемент в конец (значение элемента целиком). */
@@ -54,12 +67,12 @@ export interface ModelArray<U> {
54
67
  swap(a: number, b: number): void;
55
68
  /** Очистить массив. */
56
69
  clear(): void;
57
- /** Под-модель элемента по индексу (для объектных элементов) или значение (для примитивных). */
58
- at(index: number): U extends object ? FormModel<U> : U | undefined;
59
- /** Map по элементам: объектные → {@link FormModel}, примитивныезначение. */
60
- map<R>(fn: (item: U extends object ? FormModel<U> : U, index: number) => R): R[];
61
- /** Итерация по элементам. */
62
- forEach(fn: (item: U extends object ? FormModel<U> : U, index: number) => void): void;
70
+ /** Элемент по индексу: объект под-модель, массив {@link ModelArray}, лист → значение. */
71
+ at(index: number): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
72
+ /** Map по элементам (объект → {@link FormModel}, Opaque/примитивзначение, массив → {@link ModelArray}). */
73
+ map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
74
+ /** Итерация по элементам (см. {@link ModelArrayItem}). */
75
+ forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
63
76
  /** Снимок массива значений (без подписки). */
64
77
  toArray(): U[];
65
78
  /** Индексный value-доступ. */
@@ -72,11 +85,19 @@ export interface ModelArray<U> {
72
85
  * @group Model
73
86
  */
74
87
  export type ModelSignals<T> = {
75
- [K in keyof T]: NonNullable<T[K]> extends ReadonlyArray<infer U> ? {
76
- readonly length: number;
77
- readonly [index: number]: ModelSignals<U & object>;
78
- } : NonNullable<T[K]> extends Opaque ? PathAwareSignal<T[K]> : NonNullable<T[K]> extends object ? ModelSignals<NonNullable<T[K]>> : PathAwareSignal<T[K]>;
88
+ [K in keyof T]: ModelSignalNode<T[K]>;
79
89
  };
90
+ /**
91
+ * Узел дерева сигналов `model.$`: массив → индексируемый узел под-сигналов, Opaque (Date/File/Blob)
92
+ * и примитив → {@link PathAwareSignal}, объект → вложенное дерево {@link ModelSignals}. Для примитивных
93
+ * и Opaque массивов элемент — сам сигнал листа (а не под-дерево), поэтому `model.$.tags[0]` — это
94
+ * `PathAwareSignal<string>`, а не `ModelSignals<never>`.
95
+ * @internal
96
+ */
97
+ type ModelSignalNode<V> = NonNullable<V> extends ReadonlyArray<infer U> ? {
98
+ readonly length: number;
99
+ readonly [index: number]: ModelSignalNode<U>;
100
+ } : NonNullable<V> extends Opaque ? PathAwareSignal<V> : NonNullable<V> extends object ? ModelSignals<NonNullable<V>> : PathAwareSignal<V>;
80
101
  /**
81
102
  * API уровня модели (доступно на корне и под-моделях элементов массива).
82
103
  *
@@ -90,9 +111,17 @@ export interface ModelApi<T> {
90
111
  readonly $: ModelSignals<T>;
91
112
  /** Снимок значений (без подписки) — для submit. */
92
113
  get(): T;
93
- /** Массовая замена значений (load с сервера). Не меняет initial-снимок. */
114
+ /**
115
+ * @deprecated Алиас {@link ModelApi.patch}: `set` выполняет то же частичное **слияние** —
116
+ * обновляет только переданные ключи, отсутствующие НЕ трогает (это НЕ полная замена, вопреки
117
+ * старому названию). Используйте `patch`; для замены с обнулением лишних полей делайте
118
+ * `patch({ ...defaults, ...dto })` явно. Не меняет initial-снимок.
119
+ */
94
120
  set(value: Partial<T>): void;
95
- /** Частичное слияние значений. */
121
+ /**
122
+ * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
123
+ * отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
124
+ */
96
125
  patch(value: Partial<T>): void;
97
126
  /** Отличаются ли текущие значения от initial-снимка (value-diff). */
98
127
  isDirty(): boolean;
@@ -1,9 +1,13 @@
1
1
  import { ValidationError } from '../types';
2
+ import { FormSchemaNode } from '../types/schema-node';
2
3
  import { FormModel } from './types';
3
4
  /**
4
- * Валидатор слоя данных. `value` — значение поля; `model` — ближайший scope (под-модель элемента
5
+ * Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
5
6
  * массива или корень); `root` — корневая модель.
6
7
  *
8
+ * Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
9
+ * `FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
10
+ *
7
11
  * @group Model
8
12
  */
9
13
  export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (value: TValue, model: TModel, root: TRoot) => ValidationError | null | Promise<ValidationError | null>;
@@ -32,7 +36,7 @@ export interface ModelValidationResult {
32
36
  * if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
33
37
  * ```
34
38
  */
35
- export declare function validateModelSync<T>(model: FormModel<T>, schema: unknown): ModelValidationResult;
39
+ export declare function validateModelSync<T>(model: FormModel<T>, schema: FormSchemaNode): ModelValidationResult;
36
40
  /**
37
41
  * Headless-валидация данных (sync + async). Работает без UI/нод.
38
42
  *
@@ -43,7 +47,7 @@ export declare function validateModelSync<T>(model: FormModel<T>, schema: unknow
43
47
  * if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
44
48
  * ```
45
49
  */
46
- export declare function validateModel<T>(model: FormModel<T>, schema: unknown): Promise<ModelValidationResult>;
50
+ export declare function validateModel<T>(model: FormModel<T>, schema: FormSchemaNode): Promise<ModelValidationResult>;
47
51
  /**
48
52
  * In-form валидация: прогоняет {@link validateModel} и роутит ошибки в ноды формы
49
53
  * (через реестр сигнал→нода): `node.setErrors(errors[path] ?? [])` — заодно очищает прошедшие поля
@@ -65,4 +69,4 @@ export declare function validateModel<T>(model: FormModel<T>, schema: unknown):
65
69
  * if (res.valid) await api.save(model.get());
66
70
  * ```
67
71
  */
68
- export declare function validateFormModel<T>(model: FormModel<T>, schema: unknown): Promise<ModelValidationResult>;
72
+ export declare function validateFormModel<T>(model: FormModel<T>, schema: FormSchemaNode): Promise<ModelValidationResult>;
@@ -35,7 +35,7 @@ export declare class FieldNode<T> extends FormNode<T> {
35
35
  readonly status: ReadonlySignal<FieldStatus>;
36
36
  readonly disabled: ReadonlySignal<boolean>;
37
37
  readonly errors: ReadonlySignal<ValidationError[]>;
38
- readonly componentProps: ReadonlySignal<Record<string, any>>;
38
+ readonly componentProps: ReadonlySignal<Record<string, unknown>>;
39
39
  /**
40
40
  * Вычисляемое свойство: нужно ли показывать ошибку
41
41
  * Ошибка показывается если поле невалидно И (touched ИЛИ dirty)
@@ -186,7 +186,7 @@ export declare class FieldNode<T> extends FormNode<T> {
186
186
  * });
187
187
  * ```
188
188
  */
189
- updateComponentProps(props: Partial<Record<string, any>>): void;
189
+ updateComponentProps(props: Partial<Record<string, unknown>>): void;
190
190
  /**
191
191
  * Динамически изменяет триггер валидации (updateOn)
192
192
  * Полезно для адаптивной валидации - например, переключиться на instant feedback после первого submit
@@ -244,7 +244,13 @@ export declare class GroupNode<T> extends FormNode<T> {
244
244
  * useEffect(() => dispose, []);
245
245
  * ```
246
246
  */
247
- watchField<K extends keyof T>(fieldPath: K extends string ? K : string, callback: (value: T[K]) => void | Promise<void>): () => void;
247
+ /** Подписка на top-level поле value типизирован как `T[K]`. */
248
+ watchField<K extends keyof T & string>(fieldPath: K, callback: (value: T[K]) => void | Promise<void>): () => void;
249
+ /**
250
+ * Подписка на вложенное поле по строковому пути ("address.city").
251
+ * Путь нельзя выразить в типах узла → value честно `unknown`, потребитель сужает.
252
+ */
253
+ watchFieldByPath(path: string, callback: (value: unknown) => void | Promise<void>): () => void;
248
254
  /**
249
255
  * Hook: вызывается после disable()
250
256
  */
@@ -104,6 +104,11 @@ export declare class ModelArrayNode<T extends object> extends FormNode<T[]> {
104
104
  setErrors(errors: ValidationError[]): void;
105
105
  clearErrors(): void;
106
106
  protected onMarkAsTouched(): void;
107
+ protected onMarkAsUntouched(): void;
108
+ protected onMarkAsDirty(): void;
109
+ protected onMarkAsPristine(): void;
110
+ protected onDisable(): void;
111
+ protected onEnable(): void;
107
112
  /** Очистка подписки синхронизации. */
108
113
  dispose(): void;
109
114
  }
@@ -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
  /**
@@ -35,17 +42,22 @@ export interface FieldConfig<T> {
35
42
  * @group Types
36
43
  * @category Configuration Types
37
44
  */
38
- export interface ArrayConfig<T extends FormFields> {
45
+ export interface ArrayConfig<T extends object> {
39
46
  itemSchema: FormSchema<T>;
40
47
  initial?: Partial<T>[];
41
48
  }
42
49
  /**
43
- * Автоматически определяет тип схемы на основе TypeScript типа:
50
+ * **Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
51
+ * (или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
44
52
  * - `T[] -> [FormSchema<T>]` (массив с одним элементом)
45
53
  * - `object -> FormSchema<T>` (группа)
46
54
  * - `primitive -> FieldConfig<T>` (поле)
47
55
  *
48
- * Использует NonNullable для корректной обработки опциональных полей
56
+ * Использует NonNullable для корректной обработки опциональных полей.
57
+ *
58
+ * ⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
59
+ * контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
60
+ * (ключи = поля данных), а не узел дерева.
49
61
  *
50
62
  * @group Types
51
63
  * @category Configuration Types
@@ -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
  *
@@ -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,13 +32,13 @@ 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
- * Чистый асинхронный валидатор поля.
27
- *
28
- * Регистрируется через `validateAsync(path, validator, { debounce })`.
37
+ * Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
38
+ * {@link Validator}, но возвращает `Promise`. Регистрируется через
39
+ * `validateAsync(path, validator, { debounce })`.
29
40
  */
30
- export type AsyncValidator<TForm, TField> = (value: TField, control: FormProxy<TField>, root: FormProxy<TForm>) => Promise<ValidationError | null>;
41
+ export type AsyncValidator<TForm, TField> = (value: TField, scope: unknown, root: FormModel<TForm>) => Promise<ValidationError | null>;
31
42
  /**
32
43
  * Функция условия для applyWhen.
33
44
  */
@@ -39,7 +50,7 @@ export interface ValidateOptions {
39
50
  /** Сообщение об ошибке */
40
51
  message?: string;
41
52
  /** Параметры ошибки */
42
- params?: FormFields;
53
+ params?: Record<string, FormValue>;
43
54
  }
44
55
  /**
45
56
  * Опции для функции validateAsync.
@@ -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()`.
@@ -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;
@@ -2,7 +2,7 @@ import { FormNode } from '../nodes/form-node';
2
2
  import { FieldNode } from '../nodes/field-node';
3
3
  import { GroupNode } from '../nodes/group-node';
4
4
  import { ArrayNode } from '../nodes/array-node';
5
- import { FormFields, FormValue } from '../types';
5
+ import { FormValue } from '../types';
6
6
  /**
7
7
  * Проверить, является ли значение любым FormNode
8
8
  *
@@ -61,7 +61,7 @@ export declare function isFieldNode(value: unknown): value is FieldNode<FormValu
61
61
  * }
62
62
  * ```
63
63
  */
64
- export declare function isGroupNode(value: unknown): value is GroupNode<FormFields>;
64
+ export declare function isGroupNode(value: unknown): value is GroupNode<object>;
65
65
  /**
66
66
  * Проверить, является ли значение ArrayNode (массив форм)
67
67
  *
@@ -80,7 +80,7 @@ export declare function isGroupNode(value: unknown): value is GroupNode<FormFiel
80
80
  * }
81
81
  * ```
82
82
  */
83
- export declare function isArrayNode(value: unknown): value is ArrayNode<FormFields>;
83
+ export declare function isArrayNode(value: unknown): value is ArrayNode<object>;
84
84
  /**
85
85
  * Получить тип узла как строку (для отладки)
86
86
  *
@@ -21,6 +21,10 @@ export declare function getToday(): Date;
21
21
  */
22
22
  export declare function normalizeDate(date: Date): Date;
23
23
  /**
24
- * Вычисляет возраст на основе даты рождения
24
+ * Вычисляет полный возраст годах) по календарным полям.
25
+ *
26
+ * Считаем по годам/месяцам/дням, а не делением миллисекунд на усреднённый год (365.25 дн): усреднение
27
+ * даёт off-by-one на границе — в день N-летия накопленных реальных дней меньше `N * 365.25`, и `floor`
28
+ * роняет возраст до `N - 1` (т.е. `minAge(18)` ошибочно отклонял 18-летнего в его день рождения).
25
29
  */
26
30
  export declare function calculateAge(birthDate: Date): number;
@@ -19,4 +19,4 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
19
19
  * },
20
20
  * ```
21
21
  */
22
- export declare function integer<TForm = unknown, TField extends number | undefined = number>(options?: ValidateOptions): Validator<TForm, TField>;
22
+ export declare function integer<TForm = unknown, TField extends number | null | undefined = number>(options?: ValidateOptions): Validator<TForm, TField>;
@@ -21,4 +21,4 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
21
21
  * },
22
22
  * ```
23
23
  */
24
- export declare function isNumber<TForm = unknown, TField extends number | undefined = number>(options?: ValidateOptions): Validator<TForm, TField>;
24
+ export declare function isNumber<TForm = unknown, TField extends number | null | undefined = number>(options?: ValidateOptions): Validator<TForm, TField>;
@@ -22,4 +22,4 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
22
22
  * },
23
23
  * ```
24
24
  */
25
- export declare function max<TForm = unknown, TField extends number | undefined = number>(maxValue: number, options?: ValidateOptions): Validator<TForm, TField>;
25
+ export declare function max<TForm = unknown, TField extends number | null | undefined = number>(maxValue: number, options?: ValidateOptions): Validator<TForm, TField>;
@@ -22,4 +22,4 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
22
22
  * },
23
23
  * ```
24
24
  */
25
- export declare function min<TForm = unknown, TField extends number | undefined = number>(minValue: number, options?: ValidateOptions): Validator<TForm, TField>;
25
+ export declare function min<TForm = unknown, TField extends number | null | undefined = number>(minValue: number, options?: ValidateOptions): Validator<TForm, TField>;
@@ -21,4 +21,4 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
21
21
  * },
22
22
  * ```
23
23
  */
24
- export declare function multipleOf<TForm = unknown, TField extends number | undefined = number>(divisor: number, options?: ValidateOptions): Validator<TForm, TField>;
24
+ export declare function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(divisor: number, options?: ValidateOptions): Validator<TForm, TField>;
@@ -19,4 +19,4 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
19
19
  * },
20
20
  * ```
21
21
  */
22
- export declare function nonNegative<TForm = unknown, TField extends number | undefined = number>(options?: ValidateOptions): Validator<TForm, TField>;
22
+ export declare function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(options?: ValidateOptions): Validator<TForm, TField>;
@@ -19,4 +19,4 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
19
19
  * },
20
20
  * ```
21
21
  */
22
- export declare function nonZero<TForm = unknown, TField extends number | undefined = number>(options?: ValidateOptions): Validator<TForm, TField>;
22
+ export declare function nonZero<TForm = unknown, TField extends number | null | undefined = number>(options?: ValidateOptions): Validator<TForm, TField>;