@reformer/core 5.0.2 → 7.0.0-beta.1

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 (131) hide show
  1. package/README.md +7 -5
  2. package/dist/behaviors-O9a1Djj9.js +147 -0
  3. package/dist/behaviors.d.ts +131 -4
  4. package/dist/behaviors.js +298 -27
  5. package/dist/core/model/behaviors.d.ts +123 -0
  6. package/dist/core/model/form-model.d.ts +18 -0
  7. package/dist/core/model/index.d.ts +12 -0
  8. package/dist/core/model/types.d.ts +112 -0
  9. package/dist/core/model/validate-model.d.ts +46 -0
  10. package/dist/core/nodes/array-node.d.ts +34 -2
  11. package/dist/core/nodes/group-node.d.ts +19 -71
  12. package/dist/core/nodes/model-array-node.d.ts +71 -0
  13. package/dist/core/types/deep-schema.d.ts +16 -2
  14. package/dist/core/types/form-proxy.d.ts +17 -6
  15. package/dist/core/types/index.d.ts +5 -23
  16. package/dist/core/types/validation-schema.d.ts +15 -66
  17. package/dist/core/utils/create-form.d.ts +24 -0
  18. package/dist/core/utils/derived-registry.d.ts +5 -0
  19. package/dist/core/utils/index.d.ts +4 -4
  20. package/dist/core/utils/signal-node-registry.d.ts +12 -0
  21. package/dist/core/utils/type-guards.d.ts +2 -3
  22. package/dist/core/validation/index.d.ts +5 -11
  23. package/dist/core/validation/validators/email.d.ts +5 -29
  24. package/dist/core/validation/validators/future-date.d.ts +6 -28
  25. package/dist/core/validation/validators/index.d.ts +11 -4
  26. package/dist/core/validation/validators/integer.d.ts +13 -0
  27. package/dist/core/validation/validators/is-date.d.ts +5 -28
  28. package/dist/core/validation/validators/is-number.d.ts +14 -0
  29. package/dist/core/validation/validators/max-age.d.ts +6 -29
  30. package/dist/core/validation/validators/max-date.d.ts +6 -29
  31. package/dist/core/validation/validators/max-length.d.ts +5 -30
  32. package/dist/core/validation/validators/max.d.ts +7 -29
  33. package/dist/core/validation/validators/min-age.d.ts +6 -29
  34. package/dist/core/validation/validators/min-date.d.ts +6 -29
  35. package/dist/core/validation/validators/min-length.d.ts +5 -30
  36. package/dist/core/validation/validators/min.d.ts +7 -29
  37. package/dist/core/validation/validators/multiple-of.d.ts +13 -0
  38. package/dist/core/validation/validators/non-negative.d.ts +13 -0
  39. package/dist/core/validation/validators/non-zero.d.ts +13 -0
  40. package/dist/core/validation/validators/past-date.d.ts +5 -28
  41. package/dist/core/validation/validators/pattern.d.ts +5 -32
  42. package/dist/core/validation/validators/phone.d.ts +9 -19
  43. package/dist/core/validation/validators/required.d.ts +10 -31
  44. package/dist/core/validation/validators/url.d.ts +13 -15
  45. package/dist/index.d.ts +3 -4
  46. package/dist/index.js +1299 -875
  47. package/dist/validators/email.js +7 -9
  48. package/dist/validators/future-date.js +11 -12
  49. package/dist/validators/integer.d.ts +2 -0
  50. package/dist/validators/integer.js +10 -0
  51. package/dist/validators/is-date.js +6 -7
  52. package/dist/validators/is-number.d.ts +2 -0
  53. package/dist/validators/is-number.js +10 -0
  54. package/dist/validators/max-age.js +11 -12
  55. package/dist/validators/max-date.js +11 -12
  56. package/dist/validators/max-length.js +12 -8
  57. package/dist/validators/max.js +5 -6
  58. package/dist/validators/min-age.js +10 -11
  59. package/dist/validators/min-date.js +12 -13
  60. package/dist/validators/min-length.js +12 -8
  61. package/dist/validators/min.js +6 -7
  62. package/dist/validators/multiple-of.d.ts +2 -0
  63. package/dist/validators/multiple-of.js +10 -0
  64. package/dist/validators/non-negative.d.ts +2 -0
  65. package/dist/validators/non-negative.js +10 -0
  66. package/dist/validators/non-zero.d.ts +2 -0
  67. package/dist/validators/non-zero.js +10 -0
  68. package/dist/validators/past-date.js +11 -12
  69. package/dist/validators/pattern.js +6 -7
  70. package/dist/validators/phone.js +14 -32
  71. package/dist/validators/required.js +9 -10
  72. package/dist/validators/url.js +19 -16
  73. package/dist/validators.js +67 -54
  74. package/llms.txt +959 -2584
  75. package/package.json +19 -35
  76. package/dist/behaviors/compute-from.d.ts +0 -2
  77. package/dist/behaviors/compute-from.js +0 -31
  78. package/dist/behaviors/copy-from.d.ts +0 -2
  79. package/dist/behaviors/copy-from.js +0 -29
  80. package/dist/behaviors/enable-when.d.ts +0 -2
  81. package/dist/behaviors/enable-when.js +0 -25
  82. package/dist/behaviors/reset-when.d.ts +0 -2
  83. package/dist/behaviors/reset-when.js +0 -24
  84. package/dist/behaviors/revalidate-when.d.ts +0 -2
  85. package/dist/behaviors/revalidate-when.js +0 -18
  86. package/dist/behaviors/sync-fields.d.ts +0 -2
  87. package/dist/behaviors/sync-fields.js +0 -41
  88. package/dist/behaviors/transform-value.d.ts +0 -2
  89. package/dist/behaviors/transform-value.js +0 -45
  90. package/dist/behaviors/watch-field.d.ts +0 -2
  91. package/dist/behaviors/watch-field.js +0 -21
  92. package/dist/core/behavior/behavior-context.d.ts +0 -47
  93. package/dist/core/behavior/behavior-registry.d.ts +0 -85
  94. package/dist/core/behavior/behaviors/compute-from.d.ts +0 -70
  95. package/dist/core/behavior/behaviors/copy-from.d.ts +0 -56
  96. package/dist/core/behavior/behaviors/enable-when.d.ts +0 -118
  97. package/dist/core/behavior/behaviors/index.d.ts +0 -11
  98. package/dist/core/behavior/behaviors/reset-when.d.ts +0 -64
  99. package/dist/core/behavior/behaviors/revalidate-when.d.ts +0 -53
  100. package/dist/core/behavior/behaviors/sync-fields.d.ts +0 -48
  101. package/dist/core/behavior/behaviors/transform-value.d.ts +0 -192
  102. package/dist/core/behavior/behaviors/watch-field.d.ts +0 -80
  103. package/dist/core/behavior/compose-behavior.d.ts +0 -96
  104. package/dist/core/behavior/index.d.ts +0 -11
  105. package/dist/core/behavior/types.d.ts +0 -146
  106. package/dist/core/types/field-path.d.ts +0 -42
  107. package/dist/core/types/form-context.d.ts +0 -89
  108. package/dist/core/utils/field-path-navigator.d.ts +0 -240
  109. package/dist/core/utils/field-path.d.ts +0 -60
  110. package/dist/core/utils/registry-helpers.d.ts +0 -43
  111. package/dist/core/validation/core/apply-when.d.ts +0 -22
  112. package/dist/core/validation/core/apply.d.ts +0 -52
  113. package/dist/core/validation/core/index.d.ts +0 -8
  114. package/dist/core/validation/core/validate-async.d.ts +0 -36
  115. package/dist/core/validation/core/validate-tree.d.ts +0 -35
  116. package/dist/core/validation/core/validate.d.ts +0 -26
  117. package/dist/core/validation/validate-form.d.ts +0 -48
  118. package/dist/core/validation/validation-applicator.d.ts +0 -70
  119. package/dist/core/validation/validation-context.d.ts +0 -86
  120. package/dist/core/validation/validation-registry.d.ts +0 -142
  121. package/dist/core/validation/validators/array-validators.d.ts +0 -53
  122. package/dist/core/validation/validators/number.d.ts +0 -31
  123. package/dist/field-path-DuKdGcIE.js +0 -66
  124. package/dist/hooks/useHiddenCondition.d.ts +0 -25
  125. package/dist/index-D25LsbRm.js +0 -73
  126. package/dist/registry-helpers-Bv_BJ1s-.js +0 -615
  127. package/dist/safe-effect-Dh8uw81c.js +0 -20
  128. package/dist/validate-C3XiA_zf.js +0 -10
  129. package/dist/validators/number.d.ts +0 -2
  130. package/dist/validators/number.js +0 -35
  131. package/dist/validators-BGsNOgT1.js +0 -207
@@ -0,0 +1,123 @@
1
+ import { Signal, ReadonlySignal } from '@preact/signals-core';
2
+ /** Функция отписки от behavior-эффекта. */
3
+ export type BehaviorCleanup = () => void;
4
+ /**
5
+ * Вычисляемое поле: `target = fn(...sourceValues)` при изменении источников.
6
+ *
7
+ * @group Model
8
+ * @param sources Сигналы-источники (`model.$.a`, `model.$.b`).
9
+ * @param target Сигнал-цель (`model.$.total`).
10
+ * @param fn Функция вычисления значения.
11
+ * @param options.when Опциональное условие (получает значения источников); если false — пропуск.
12
+ * @returns Cleanup для отписки.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * computeFrom([model.$.price, model.$.qty], model.$.total, (price, qty) => price * qty);
17
+ * ```
18
+ */
19
+ export declare function computeFrom<R>(sources: ReadonlySignal<any>[], target: Signal<R>, fn: (...values: any[]) => R, options?: {
20
+ when?: (...values: any[]) => boolean;
21
+ }): BehaviorCleanup;
22
+ /**
23
+ * Копирование значения `source → target` (опционально по условию/с трансформом).
24
+ *
25
+ * @group Model
26
+ * @example
27
+ * ```typescript
28
+ * copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail });
29
+ * ```
30
+ */
31
+ export declare function copyFrom<T>(source: ReadonlySignal<T>, target: Signal<T>, options?: {
32
+ when?: () => boolean;
33
+ transform?: (value: T) => T;
34
+ }): BehaviorCleanup;
35
+ /**
36
+ * Реакция на изменение поля: вызывает `cb(value)` при каждом изменении (по умолчанию без вызова на
37
+ * инициализации; `immediate: true` — вызвать сразу).
38
+ *
39
+ * @group Model
40
+ * @example
41
+ * ```typescript
42
+ * watchField(model.$.country, async (country) => {
43
+ * model.city = '';
44
+ * // ... загрузить города
45
+ * });
46
+ * ```
47
+ */
48
+ export declare function watchField<T>(source: ReadonlySignal<T>, cb: (value: T) => void, options?: {
49
+ immediate?: boolean;
50
+ }): BehaviorCleanup;
51
+ /**
52
+ * Условное включение поля (state-операция). Резолвит ноду по сигналу-цели через реестр
53
+ * сигнал→нода (заполняется `createForm`) и вызывает `enable()`/`disable()` (+`reset()` при
54
+ * `resetOnDisable`). `condition` реактивен (читает свои сигналы модели).
55
+ *
56
+ * Запись состояния отложена через `runOutsideEffect` (микротаск) для защиты от «Cycle detected».
57
+ * ⚠️ Поле должно быть материализовано в форме (`createForm`) — иначе ноды в реестре нет (например,
58
+ * элемент массива, который строится per-item).
59
+ *
60
+ * @group Model
61
+ * @example
62
+ * ```typescript
63
+ * enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', { resetOnDisable: true });
64
+ * ```
65
+ */
66
+ export declare function enableWhen(target: ReadonlySignal<unknown>, condition: () => boolean, options?: {
67
+ resetOnDisable?: boolean;
68
+ }): BehaviorCleanup;
69
+ /**
70
+ * Условное выключение поля (инверсия {@link enableWhen}).
71
+ *
72
+ * @group Model
73
+ */
74
+ export declare function disableWhen(target: ReadonlySignal<unknown>, condition: () => boolean, options?: {
75
+ resetOnDisable?: boolean;
76
+ }): BehaviorCleanup;
77
+ /**
78
+ * Трансформация значения поля (идемпотентная): при изменении пишет `transformer(value)` обратно.
79
+ * Запись отложена (`runOutsideEffect`) во избежание «Cycle detected» (эффект читает и пишет один сигнал).
80
+ *
81
+ * @group Model
82
+ * @example
83
+ * ```typescript
84
+ * transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
85
+ * ```
86
+ */
87
+ export declare function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): BehaviorCleanup;
88
+ /**
89
+ * Сброс значения поля к `resetValue` (по умолчанию `null`), когда `condition` истинно.
90
+ *
91
+ * @group Model
92
+ * @example
93
+ * ```typescript
94
+ * resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
95
+ * ```
96
+ */
97
+ export declare function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: {
98
+ resetValue?: T;
99
+ }): BehaviorCleanup;
100
+ /**
101
+ * Двусторонняя синхронизация двух полей (опционально с трансформом `a → b`).
102
+ * Сходимость обеспечивается peek-guard'ом; флаг предотвращает лишние круги.
103
+ *
104
+ * @group Model
105
+ * @example
106
+ * ```typescript
107
+ * syncFields(model.$.field1, model.$.field2);
108
+ * ```
109
+ */
110
+ export declare function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: {
111
+ transform?: (value: T) => T;
112
+ }): BehaviorCleanup;
113
+ /**
114
+ * Вызывает `revalidate()` при изменении зависимостей (не на инициализации). Под M1 валидация
115
+ * on-demand (`validateFormModel`), поэтому ревалидация выражается явным колбэком.
116
+ *
117
+ * @group Model
118
+ * @example
119
+ * ```typescript
120
+ * revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
121
+ * ```
122
+ */
123
+ export declare function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void): BehaviorCleanup;
@@ -0,0 +1,18 @@
1
+ import { FormModel } from './types';
2
+ /**
3
+ * Создать реактивную модель данных формы (слой M1).
4
+ *
5
+ * @group Model
6
+ * @param initial Начальные значения (объект). Определяют форму данных и initial-снимок.
7
+ * @returns {@link FormModel} с value-доступом, `$`-сигналами и API (get/set/patch/isDirty/reset/signalAt).
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const model = createModel<{ email: string; tags: string[] }>({ email: '', tags: [] });
12
+ * model.email = 'a@b.c';
13
+ * model.$.email.value; // 'a@b.c' (сигнал)
14
+ * model.tags.push('x');
15
+ * model.get(); // { email: 'a@b.c', tags: ['x'] }
16
+ * ```
17
+ */
18
+ export declare function createModel<T extends object>(initial: T): FormModel<T>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Слой данных FormModel (M1) — публичные экспорты.
3
+ *
4
+ * @group Model
5
+ * @module core/model
6
+ */
7
+ export { createModel } from './form-model';
8
+ export { validateModel, validateModelSync, validateFormModel } from './validate-model';
9
+ export type { ModelValidator, ModelValidationResult } from './validate-model';
10
+ export { computeFrom, copyFrom, watchField, enableWhen, disableWhen, transformValue, resetWhen, syncFields, revalidateWhen, } from './behaviors';
11
+ export type { BehaviorCleanup } from './behaviors';
12
+ export type { FormModel, ModelArray, ModelObject, ModelValue, ModelSignals, ModelApi, PathAwareSignal, } from './types';
@@ -0,0 +1,112 @@
1
+ import { Signal } from '@preact/signals-core';
2
+ /**
3
+ * Сигнал, который знает свой путь в модели (`'personalData.lastName'`).
4
+ * Используется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.
5
+ *
6
+ * @group Model
7
+ */
8
+ export type PathAwareSignal<T> = Signal<T> & {
9
+ /** Путь поля в модели (dot-нотация). На элементах массива включает индекс. */
10
+ readonly __path: string;
11
+ };
12
+ /**
13
+ * Спец-объекты, которые трактуются как листья (не разворачиваются в под-модель).
14
+ * @internal
15
+ */
16
+ type Opaque = Date | File | Blob;
17
+ /**
18
+ * Значение поля в value-доступе модели:
19
+ * - массив → {@link ModelArray}
20
+ * - спец-объект (Date/File/Blob) → как есть
21
+ * - объект → вложенный value-proxy
22
+ * - примитив → значение
23
+ *
24
+ * @group Model
25
+ */
26
+ export type ModelValue<V> = NonNullable<V> extends ReadonlyArray<infer U> ? ModelArray<U> : NonNullable<V> extends Opaque ? V : NonNullable<V> extends object ? ModelObject<NonNullable<V>> : V;
27
+ /**
28
+ * value-proxy объекта: поля доступны как обычные свойства (чтение реактивно
29
+ * внутри `effect`/`computed`, запись — присваиванием).
30
+ *
31
+ * @group Model
32
+ */
33
+ export type ModelObject<T> = {
34
+ [K in keyof T]: ModelValue<T[K]>;
35
+ };
36
+ /**
37
+ * Реактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;
38
+ * `map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.
39
+ *
40
+ * @group Model
41
+ */
42
+ export interface ModelArray<U> {
43
+ /** Реактивная длина. */
44
+ readonly length: number;
45
+ /** Добавить элемент в конец (значение элемента целиком). */
46
+ push(item: U): void;
47
+ /** Вставить элемент по индексу. */
48
+ insertAt(index: number, item: U): void;
49
+ /** Удалить элемент по индексу. */
50
+ removeAt(index: number): void;
51
+ /** Переместить элемент. */
52
+ move(from: number, to: number): void;
53
+ /** Поменять местами два элемента. */
54
+ swap(a: number, b: number): void;
55
+ /** Очистить массив. */
56
+ 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;
63
+ /** Снимок массива значений (без подписки). */
64
+ toArray(): U[];
65
+ /** Индексный value-доступ. */
66
+ [index: number]: ModelValue<U>;
67
+ }
68
+ /**
69
+ * Дерево сигналов (escape-hatch `model.$`): листья → {@link PathAwareSignal},
70
+ * объекты → вложенное дерево, массивы → индексируемое дерево под-сигналов.
71
+ *
72
+ * @group Model
73
+ */
74
+ 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]>;
79
+ };
80
+ /**
81
+ * API уровня модели (доступно на корне и под-моделях элементов массива).
82
+ *
83
+ * ⚠️ Имена методов (`$`/`get`/`set`/`patch`/`isDirty`/`reset`/`signalAt`/`captureInitial`)
84
+ * зарезервированы: одноимённое поле формы их затеняет (редкий краевой случай).
85
+ *
86
+ * @group Model
87
+ */
88
+ export interface ModelApi<T> {
89
+ /** Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`. */
90
+ readonly $: ModelSignals<T>;
91
+ /** Снимок значений (без подписки) — для submit. */
92
+ get(): T;
93
+ /** Массовая замена значений (load с сервера). Не меняет initial-снимок. */
94
+ set(value: Partial<T>): void;
95
+ /** Частичное слияние значений. */
96
+ patch(value: Partial<T>): void;
97
+ /** Отличаются ли текущие значения от initial-снимка (value-diff). */
98
+ isDirty(): boolean;
99
+ /** Сбросить значения к initial-снимку. */
100
+ reset(): void;
101
+ /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */
102
+ captureInitial(): void;
103
+ /** Резолв строкового пути в сигнал (для error-routing/мостов). */
104
+ signalAt(path: string): PathAwareSignal<unknown> | undefined;
105
+ }
106
+ /**
107
+ * FormModel — value-proxy объекта `T` + {@link ModelApi}.
108
+ *
109
+ * @group Model
110
+ */
111
+ export type FormModel<T> = ModelObject<T> & ModelApi<T>;
112
+ export {};
@@ -0,0 +1,46 @@
1
+ import { ValidationError } from '../types';
2
+ import { FormModel } from './types';
3
+ /**
4
+ * Валидатор слоя данных. `value` — значение поля; `model` — ближайший scope (под-модель элемента
5
+ * массива или корень); `root` — корневая модель.
6
+ *
7
+ * @group Model
8
+ */
9
+ export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (value: TValue, model: TModel, root: TRoot) => ValidationError | null | Promise<ValidationError | null>;
10
+ /**
11
+ * Результат валидации: ошибки по пути поля.
12
+ *
13
+ * @group Model
14
+ */
15
+ export interface ModelValidationResult {
16
+ valid: boolean;
17
+ /** Ключ — путь поля (`'coBorrowers.0.relationship'`). Только поля с ошибками. */
18
+ errors: Record<string, ValidationError[]>;
19
+ }
20
+ /**
21
+ * Синхронная headless-валидация данных. Асинхронные валидаторы пропускаются.
22
+ *
23
+ * @group Model
24
+ */
25
+ export declare function validateModelSync<T>(model: FormModel<T>, schema: unknown): ModelValidationResult;
26
+ /**
27
+ * Headless-валидация данных (sync + async). Работает без UI/нод.
28
+ *
29
+ * @group Model
30
+ * @example
31
+ * ```typescript
32
+ * const res = await validateModel(model, schema);
33
+ * if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
34
+ * ```
35
+ */
36
+ export declare function validateModel<T>(model: FormModel<T>, schema: unknown): Promise<ModelValidationResult>;
37
+ /**
38
+ * In-form валидация: прогоняет {@link validateModel} и роутит ошибки в ноды формы
39
+ * (через реестр сигнал→нода): `node.setErrors(errors[path] ?? [])` — заодно очищает прошедшие поля
40
+ * И поля выключенных веток (`{ when, children }` с ложным условием). Дерево обходится ОДИН раз.
41
+ *
42
+ * @group Model
43
+ * @remarks Поля, не материализованные в форме (элементы массива — строятся per-item), не роутятся
44
+ * в родительскую форму; их ошибки доступны в возвращаемом результате по пути.
45
+ */
46
+ export declare function validateFormModel<T>(model: FormModel<T>, schema: unknown): Promise<ModelValidationResult>;
@@ -1,6 +1,6 @@
1
1
  import { ReadonlySignal } from '@preact/signals-core';
2
2
  import { FormNode, SetValueOptions } from './form-node';
3
- import { FieldStatus, ValidationError, FormFields } from '../types';
3
+ import { FieldStatus, ValidationError } from '../types';
4
4
  import { FormSchema } from '../types/deep-schema';
5
5
  import { FormProxy } from '../types/form-proxy';
6
6
  /**
@@ -20,7 +20,7 @@ import { FormProxy } from '../types/form-proxy';
20
20
  * console.log(array.length.value); // 1
21
21
  * ```
22
22
  */
23
- export declare class ArrayNode<T extends FormFields> extends FormNode<T[]> {
23
+ export declare class ArrayNode<T extends object> extends FormNode<T[]> {
24
24
  private items;
25
25
  private itemSchema;
26
26
  private initialItems;
@@ -62,6 +62,38 @@ export declare class ArrayNode<T extends FormFields> extends FormNode<T[]> {
62
62
  * @param initialValue - Начальные значения для нового элемента
63
63
  */
64
64
  insert(index: number, initialValue?: Partial<T>): void;
65
+ /**
66
+ * Переместить элемент массива с позиции `from` на позицию `to`
67
+ *
68
+ * @param from - Индекс перемещаемого элемента
69
+ * @param to - Целевой индекс
70
+ *
71
+ * @remarks
72
+ * Реордер не уничтожает элемент (dispose НЕ вызывается) — тот же инстанс под-формы
73
+ * переезжает на новую позицию, состояние/валидация сохраняются. Длина не меняется.
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * // Поднять элемент #2 на одну позицию вверх
78
+ * form.items.move(2, 1);
79
+ * ```
80
+ */
81
+ move(from: number, to: number): void;
82
+ /**
83
+ * Поменять местами два элемента массива
84
+ *
85
+ * @param a - Индекс первого элемента
86
+ * @param b - Индекс второго элемента
87
+ *
88
+ * @remarks
89
+ * Как и {@link move}, не уничтожает элементы — меняет только их порядок.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * form.items.swap(0, 2);
94
+ * ```
95
+ */
96
+ swap(a: number, b: number): void;
65
97
  /**
66
98
  * Удалить все элементы массива
67
99
  *
@@ -1,47 +1,29 @@
1
1
  import { ReadonlySignal } from '@preact/signals-core';
2
2
  import { FormNode, SetValueOptions } from './form-node';
3
- import { ValidationError, FieldStatus, ValidationSchemaFn, ValidatorRegistration, FormSchema, GroupNodeConfig, FormValue } from '../types';
3
+ import { ValidationError, FieldStatus, FormSchema, GroupNodeConfig, FormValue } from '../types';
4
4
  import { FormProxy } from '../types/form-proxy';
5
- import { BehaviorSchemaFn } from '../behavior/types';
6
5
  import { SubmitOptions, SubmitResult } from '../utils/form-submitter';
7
6
  /**
8
7
  * GroupNode - узел для группы полей
9
8
  *
10
- * Поддерживает два API:
11
- * 1. Старый API (только schema) - обратная совместимость
12
- * 2. Новый API (config с form, behavior, validation) - автоматическое применение схем
9
+ * Создаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`
10
+ * (M1: `createForm({ model, schema })`); валидация/behavior живут на слое модели
11
+ * (`validateFormModel`, `computeFrom`/`enableWhen`/…), а не на ноде.
13
12
  *
14
13
  * @group Nodes
15
14
  *
16
15
  * @example
17
16
  * ```typescript
18
- * // 1. Старый способ (обратная совместимость)
19
- * const simpleForm = new GroupNode({
20
- * email: { value: '', component: Input },
21
- * password: { value: '', component: Input },
22
- * });
23
- *
24
- * // 2. Новый способ (с behavior и validation схемами)
25
- * const fullForm = new GroupNode({
26
- * form: {
27
- * email: { value: '', component: Input },
28
- * password: { value: '', component: Input },
29
- * },
30
- * behavior: (path) => {
31
- * computeFrom(path.email, [path.email], (values) => values[0]?.trim());
32
- * },
33
- * validation: (path) => {
34
- * required(path.email, { message: 'Email обязателен' });
35
- * email(path.email);
36
- * required(path.password);
37
- * minLength(path.password, 8);
38
- * },
17
+ * const form = new GroupNode({
18
+ * email: { valueSignal: model.$.email, component: Input },
19
+ * password: { valueSignal: model.$.password, component: Input },
39
20
  * });
40
21
  *
41
22
  * // Прямой доступ к полям через Proxy
42
- * fullForm.email.setValue('test@mail.com');
43
- * await fullForm.validate();
44
- * console.log(fullForm.valid.value); // true
23
+ * const proxy = form.getProxy();
24
+ * proxy.email.setValue('test@mail.com');
25
+ * await proxy.validate();
26
+ * console.log(proxy.valid.value);
45
27
  * ```
46
28
  */
47
29
  export declare class GroupNode<T> extends FormNode<T> {
@@ -55,31 +37,17 @@ export declare class GroupNode<T> extends FormNode<T> {
55
37
  */
56
38
  private disposers;
57
39
  /**
58
- * Ссылка на Proxy-инстанс для использования в BehaviorContext
40
+ * Ссылка на Proxy-инстанс
59
41
  */
60
42
  private _proxyInstance?;
61
43
  /**
62
- * Навигатор для работы с путями к полям
44
+ * Cleanup декларативной схемы поведения (createForm({ behavior })). Вызывается в dispose().
63
45
  */
64
- private readonly pathNavigator;
46
+ private _behaviorCleanup?;
65
47
  /**
66
48
  * Фабрика для создания узлов формы
67
49
  */
68
50
  private readonly nodeFactory;
69
- /**
70
- * Реестр валидаторов для этой формы
71
- * Может быть инжектирован через config._validationRegistry для тестирования
72
- */
73
- private readonly validationRegistry;
74
- /**
75
- * Реестр behaviors для этой формы
76
- * Может быть инжектирован через config._behaviorRegistry для тестирования
77
- */
78
- private readonly behaviorRegistry;
79
- /**
80
- * Аппликатор для применения валидаторов к форме
81
- */
82
- private readonly validationApplicator;
83
51
  /** Управление отправкой формы */
84
52
  private readonly formSubmitter;
85
53
  /** Флаг disabled состояния */
@@ -201,18 +169,6 @@ export declare class GroupNode<T> extends FormNode<T> {
201
169
  * @returns Объект SubmitResult с данными, статусом и возможной ошибкой
202
170
  */
203
171
  submitWithResult<R>(onSubmit: (values: T) => Promise<R> | R, options?: SubmitOptions): Promise<SubmitResult<R>>;
204
- /**
205
- * Применить validation schema к форме
206
- *
207
- * Использует локальный реестр валидаторов (this.validationRegistry)
208
- * вместо глобального Singleton для изоляции форм друг от друга.
209
- */
210
- applyValidationSchema(schemaFn: ValidationSchemaFn<T>): void;
211
- /**
212
- * Применить behavior schema к форме
213
- * @returns Функция cleanup для отписки от всех behaviors
214
- */
215
- applyBehaviorSchema(schemaFn: BehaviorSchemaFn<T>): () => void;
216
172
  /**
217
173
  * Получить вложенное поле по пути
218
174
  *
@@ -243,19 +199,6 @@ export declare class GroupNode<T> extends FormNode<T> {
243
199
  * ```
244
200
  */
245
201
  getFieldByPath(path: string): FormNode<FormValue> | undefined;
246
- /**
247
- * Применить contextual валидаторы к полям
248
- *
249
- * ✅ РЕФАКТОРИНГ: Делегирование ValidationApplicator (SRP)
250
- *
251
- * Логика применения валидаторов извлечена в ValidationApplicator для:
252
- * - Соблюдения Single Responsibility Principle
253
- * - Уменьшения размера GroupNode (~120 строк)
254
- * - Улучшения тестируемости
255
- *
256
- * @param validators Зарегистрированные валидаторы
257
- */
258
- applyContextualValidators(validators: ValidatorRegistration[]): Promise<void>;
259
202
  /**
260
203
  * Создать узел на основе конфигурации
261
204
  *
@@ -312,6 +255,11 @@ export declare class GroupNode<T> extends FormNode<T> {
312
255
  * Hook: вызывается после enable()
313
256
  */
314
257
  protected onEnable(): void;
258
+ /**
259
+ * Прикрепить cleanup декларативной схемы поведения (вызывается createForm).
260
+ * @internal
261
+ */
262
+ attachBehaviorCleanup(cleanup: () => void): void;
315
263
  /**
316
264
  * Очистить все ресурсы узла
317
265
  */
@@ -0,0 +1,71 @@
1
+ import { Signal, ReadonlySignal } from '@preact/signals-core';
2
+ import { FormNode } from './form-node';
3
+ import { FieldStatus, ValidationError } from '../types';
4
+ import { FormModel } from '../model/types';
5
+ import { FormProxy } from '../types/form-proxy';
6
+ /** Минимальный контракт реактивного массива модели, используемый узлом. */
7
+ export interface ModelArrayControl<TItem extends object> {
8
+ readonly length: number;
9
+ at(index: number): FormModel<TItem> | undefined;
10
+ push(item: TItem): void;
11
+ insertAt(index: number, item: TItem): void;
12
+ removeAt(index: number): void;
13
+ move(from: number, to: number): void;
14
+ swap(a: number, b: number): void;
15
+ clear(): void;
16
+ toArray(): TItem[];
17
+ }
18
+ export declare class ModelArrayNode<T extends object> extends FormNode<T[]> {
19
+ private readonly control;
20
+ private readonly buildItem;
21
+ private readonly itemNodes;
22
+ private readonly cache;
23
+ private readonly initial;
24
+ private readonly _arrayErrors;
25
+ private readonly disposeSync;
26
+ /**
27
+ * Сигнал per-item форм. Публичен в т.ч. чтобы `isArrayNode` распознавал узел как массив
28
+ * (duck-typing проверяет наличие `items`).
29
+ */
30
+ get items(): Signal<FormProxy<T>[]>;
31
+ readonly value: ReadonlySignal<T[]>;
32
+ readonly valid: ReadonlySignal<boolean>;
33
+ readonly invalid: ReadonlySignal<boolean>;
34
+ readonly pending: ReadonlySignal<boolean>;
35
+ readonly touched: ReadonlySignal<boolean>;
36
+ readonly dirty: ReadonlySignal<boolean>;
37
+ readonly errors: ReadonlySignal<ValidationError[]>;
38
+ readonly status: ReadonlySignal<FieldStatus>;
39
+ readonly length: ReadonlySignal<number>;
40
+ /**
41
+ * @param control Реактивный массив модели (`model.<path>`).
42
+ * @param buildItem Строитель формы элемента по его под-модели (инъекция, чтобы избежать цикла
43
+ * импорта с `create-form`). Обычно `(item) => createForm({ model: item, schema: itemSchema(item) })`.
44
+ */
45
+ constructor(control: ModelArrayControl<T>, buildItem: (item: FormModel<T>) => FormProxy<T>);
46
+ at(index: number): FormProxy<T> | undefined;
47
+ map<R>(fn: (item: FormProxy<T>, index: number) => R): R[];
48
+ forEach(fn: (item: FormProxy<T>, index: number) => void): void;
49
+ push(item?: Partial<T>): void;
50
+ insert(index: number, item?: Partial<T>): void;
51
+ removeAt(index: number): void;
52
+ /**
53
+ * Переместить элемент модель-массива. Делегирует мутацию массиву модели; внутренний `effect`
54
+ * пересоберёт `itemNodes` в новом порядке (per-item формы сохраняются по кешу идентичности).
55
+ */
56
+ move(from: number, to: number): void;
57
+ /** Поменять местами два элемента модель-массива (см. {@link move}). */
58
+ swap(a: number, b: number): void;
59
+ clear(): void;
60
+ getValue(): T[];
61
+ setValue(values: T[]): void;
62
+ patchValue(values: Partial<T[]>): void;
63
+ reset(values?: T[]): void;
64
+ resetToInitial(): void;
65
+ validate(): Promise<boolean>;
66
+ setErrors(errors: ValidationError[]): void;
67
+ clearErrors(): void;
68
+ protected onMarkAsTouched(): void;
69
+ /** Очистка подписки синхронизации. */
70
+ dispose(): void;
71
+ }
@@ -1,4 +1,5 @@
1
1
  import { ComponentType } from 'react';
2
+ import { Signal } from '@preact/signals-core';
2
3
  import { ValidatorFn, AsyncValidatorFn, FormFields, AnyFunction } from './index';
3
4
  /**
4
5
  * Конфигурация поля
@@ -6,8 +7,21 @@ import { ValidatorFn, AsyncValidatorFn, FormFields, AnyFunction } from './index'
6
7
  * @category Configuration Types
7
8
  */
8
9
  export interface FieldConfig<T> {
9
- value: T | null;
10
- component: ComponentType<any>;
10
+ /**
11
+ * Начальное значение-литерал (legacy-путь). Под архитектурой M1 значение приходит из
12
+ * {@link FieldConfig.valueSignal} (сигнал {@link FormModel}); тогда `value` не требуется.
13
+ */
14
+ value?: T | null;
15
+ /**
16
+ * Сигнал значения из {@link FormModel} (M1). Если задан — служит источником истины значения
17
+ * поля (нода не владеет значением, а ссылается на этот сигнал). Имеет приоритет над `value`.
18
+ */
19
+ valueSignal?: Signal<T>;
20
+ /**
21
+ * UI-компонент поля. Опционален: core-часть можно использовать без ссылки на компонент
22
+ * (значение/валидация работают без UI; компонент нужен только для рендеринга).
23
+ */
24
+ component?: ComponentType<any>;
11
25
  componentProps?: any;
12
26
  validators?: ValidatorFn<T>[];
13
27
  asyncValidators?: AsyncValidatorFn<T>[];
@@ -1,7 +1,18 @@
1
- import { FormFields } from './index';
2
- type FieldNode<_T> = any;
3
- type GroupNode<_T> = any;
4
- type ArrayNode<_T> = any;
1
+ import { FieldNode } from '../nodes/field-node';
2
+ import { GroupNode } from '../nodes/group-node';
3
+ import { ArrayNode } from '../nodes/array-node';
4
+ /**
5
+ * Признак «объект-группа» — обычный объект, который нужно обернуть в под-форму.
6
+ *
7
+ * Исключает массивы, спец-объекты (`Date`/`File`/`Blob`) и примитивы.
8
+ * Используется как дискриминатор вместо `extends FormFields` (`Record<string, FormValue>`):
9
+ * пользовательские модели почти всегда объявлены через `interface`, который НЕ assignable
10
+ * к Record-типу (у интерфейса нет неявной index signature), и потому ошибочно проваливался
11
+ * бы в ветку `FieldNode`. Проверка `extends object` корректно ловит и `interface`, и `type`.
12
+ *
13
+ * @internal
14
+ */
15
+ type IsGroupObject<V> = V extends ReadonlyArray<unknown> ? false : V extends Date | File | Blob ? false : V extends object ? true : false;
5
16
  /**
6
17
  * Мапит тип модели данных T на правильные типы узлов формы
7
18
  *
@@ -19,7 +30,7 @@ type ArrayNode<_T> = any;
19
30
  * @template T - Тип модели данных формы
20
31
  */
21
32
  export type FormControlsProxy<T> = {
22
- [K in keyof T]: NonNullable<T[K]> extends Array<infer U> ? U extends FormFields ? FormArrayProxy<U> : FieldNode<T[K]> : NonNullable<T[K]> extends FormFields ? NonNullable<T[K]> extends Date | File | Blob ? FieldNode<T[K]> : FormProxy<NonNullable<T[K]>> : FieldNode<T[K]>;
33
+ [K in keyof T]-?: NonNullable<T[K]> extends ReadonlyArray<infer U> ? IsGroupObject<U> extends true ? FormArrayProxy<U & object> : FieldNode<T[K]> : IsGroupObject<NonNullable<T[K]>> extends true ? FormProxy<NonNullable<T[K]>> : FieldNode<T[K]>;
23
34
  };
24
35
  /**
25
36
  * Комбинированный тип для GroupNode с Proxy доступом к полям
@@ -87,7 +98,7 @@ export type FormProxy<T> = GroupNode<T> & FormControlsProxy<T>;
87
98
  * });
88
99
  * ```
89
100
  */
90
- export type FormArrayProxy<T extends FormFields> = ArrayNode<T> & {
101
+ export type FormArrayProxy<T extends object> = ArrayNode<T> & {
91
102
  /**
92
103
  * Безопасный доступ к элементу массива по индексу
93
104
  * Возвращает GroupNode с типизированными полями или undefined