@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.
- package/README.md +7 -5
- package/dist/behaviors-O9a1Djj9.js +147 -0
- package/dist/behaviors.d.ts +131 -4
- package/dist/behaviors.js +298 -27
- package/dist/core/model/behaviors.d.ts +123 -0
- package/dist/core/model/form-model.d.ts +18 -0
- package/dist/core/model/index.d.ts +12 -0
- package/dist/core/model/types.d.ts +112 -0
- package/dist/core/model/validate-model.d.ts +46 -0
- package/dist/core/nodes/array-node.d.ts +34 -2
- package/dist/core/nodes/group-node.d.ts +19 -71
- package/dist/core/nodes/model-array-node.d.ts +71 -0
- package/dist/core/types/deep-schema.d.ts +16 -2
- package/dist/core/types/form-proxy.d.ts +17 -6
- package/dist/core/types/index.d.ts +5 -23
- package/dist/core/types/validation-schema.d.ts +15 -66
- package/dist/core/utils/create-form.d.ts +24 -0
- package/dist/core/utils/derived-registry.d.ts +5 -0
- package/dist/core/utils/index.d.ts +4 -4
- package/dist/core/utils/signal-node-registry.d.ts +12 -0
- package/dist/core/utils/type-guards.d.ts +2 -3
- package/dist/core/validation/index.d.ts +5 -11
- package/dist/core/validation/validators/email.d.ts +5 -29
- package/dist/core/validation/validators/future-date.d.ts +6 -28
- package/dist/core/validation/validators/index.d.ts +11 -4
- package/dist/core/validation/validators/integer.d.ts +13 -0
- package/dist/core/validation/validators/is-date.d.ts +5 -28
- package/dist/core/validation/validators/is-number.d.ts +14 -0
- package/dist/core/validation/validators/max-age.d.ts +6 -29
- package/dist/core/validation/validators/max-date.d.ts +6 -29
- package/dist/core/validation/validators/max-length.d.ts +5 -30
- package/dist/core/validation/validators/max.d.ts +7 -29
- package/dist/core/validation/validators/min-age.d.ts +6 -29
- package/dist/core/validation/validators/min-date.d.ts +6 -29
- package/dist/core/validation/validators/min-length.d.ts +5 -30
- package/dist/core/validation/validators/min.d.ts +7 -29
- package/dist/core/validation/validators/multiple-of.d.ts +13 -0
- package/dist/core/validation/validators/non-negative.d.ts +13 -0
- package/dist/core/validation/validators/non-zero.d.ts +13 -0
- package/dist/core/validation/validators/past-date.d.ts +5 -28
- package/dist/core/validation/validators/pattern.d.ts +5 -32
- package/dist/core/validation/validators/phone.d.ts +9 -19
- package/dist/core/validation/validators/required.d.ts +10 -31
- package/dist/core/validation/validators/url.d.ts +13 -15
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1299 -875
- package/dist/validators/email.js +7 -9
- package/dist/validators/future-date.js +11 -12
- package/dist/validators/integer.d.ts +2 -0
- package/dist/validators/integer.js +10 -0
- package/dist/validators/is-date.js +6 -7
- package/dist/validators/is-number.d.ts +2 -0
- package/dist/validators/is-number.js +10 -0
- package/dist/validators/max-age.js +11 -12
- package/dist/validators/max-date.js +11 -12
- package/dist/validators/max-length.js +12 -8
- package/dist/validators/max.js +5 -6
- package/dist/validators/min-age.js +10 -11
- package/dist/validators/min-date.js +12 -13
- package/dist/validators/min-length.js +12 -8
- package/dist/validators/min.js +6 -7
- package/dist/validators/multiple-of.d.ts +2 -0
- package/dist/validators/multiple-of.js +10 -0
- package/dist/validators/non-negative.d.ts +2 -0
- package/dist/validators/non-negative.js +10 -0
- package/dist/validators/non-zero.d.ts +2 -0
- package/dist/validators/non-zero.js +10 -0
- package/dist/validators/past-date.js +11 -12
- package/dist/validators/pattern.js +6 -7
- package/dist/validators/phone.js +14 -32
- package/dist/validators/required.js +9 -10
- package/dist/validators/url.js +19 -16
- package/dist/validators.js +67 -54
- package/llms.txt +959 -2584
- package/package.json +19 -35
- package/dist/behaviors/compute-from.d.ts +0 -2
- package/dist/behaviors/compute-from.js +0 -31
- package/dist/behaviors/copy-from.d.ts +0 -2
- package/dist/behaviors/copy-from.js +0 -29
- package/dist/behaviors/enable-when.d.ts +0 -2
- package/dist/behaviors/enable-when.js +0 -25
- package/dist/behaviors/reset-when.d.ts +0 -2
- package/dist/behaviors/reset-when.js +0 -24
- package/dist/behaviors/revalidate-when.d.ts +0 -2
- package/dist/behaviors/revalidate-when.js +0 -18
- package/dist/behaviors/sync-fields.d.ts +0 -2
- package/dist/behaviors/sync-fields.js +0 -41
- package/dist/behaviors/transform-value.d.ts +0 -2
- package/dist/behaviors/transform-value.js +0 -45
- package/dist/behaviors/watch-field.d.ts +0 -2
- package/dist/behaviors/watch-field.js +0 -21
- package/dist/core/behavior/behavior-context.d.ts +0 -47
- package/dist/core/behavior/behavior-registry.d.ts +0 -85
- package/dist/core/behavior/behaviors/compute-from.d.ts +0 -70
- package/dist/core/behavior/behaviors/copy-from.d.ts +0 -56
- package/dist/core/behavior/behaviors/enable-when.d.ts +0 -118
- package/dist/core/behavior/behaviors/index.d.ts +0 -11
- package/dist/core/behavior/behaviors/reset-when.d.ts +0 -64
- package/dist/core/behavior/behaviors/revalidate-when.d.ts +0 -53
- package/dist/core/behavior/behaviors/sync-fields.d.ts +0 -48
- package/dist/core/behavior/behaviors/transform-value.d.ts +0 -192
- package/dist/core/behavior/behaviors/watch-field.d.ts +0 -80
- package/dist/core/behavior/compose-behavior.d.ts +0 -96
- package/dist/core/behavior/index.d.ts +0 -11
- package/dist/core/behavior/types.d.ts +0 -146
- package/dist/core/types/field-path.d.ts +0 -42
- package/dist/core/types/form-context.d.ts +0 -89
- package/dist/core/utils/field-path-navigator.d.ts +0 -240
- package/dist/core/utils/field-path.d.ts +0 -60
- package/dist/core/utils/registry-helpers.d.ts +0 -43
- package/dist/core/validation/core/apply-when.d.ts +0 -22
- package/dist/core/validation/core/apply.d.ts +0 -52
- package/dist/core/validation/core/index.d.ts +0 -8
- package/dist/core/validation/core/validate-async.d.ts +0 -36
- package/dist/core/validation/core/validate-tree.d.ts +0 -35
- package/dist/core/validation/core/validate.d.ts +0 -26
- package/dist/core/validation/validate-form.d.ts +0 -48
- package/dist/core/validation/validation-applicator.d.ts +0 -70
- package/dist/core/validation/validation-context.d.ts +0 -86
- package/dist/core/validation/validation-registry.d.ts +0 -142
- package/dist/core/validation/validators/array-validators.d.ts +0 -53
- package/dist/core/validation/validators/number.d.ts +0 -31
- package/dist/field-path-DuKdGcIE.js +0 -66
- package/dist/hooks/useHiddenCondition.d.ts +0 -25
- package/dist/index-D25LsbRm.js +0 -73
- package/dist/registry-helpers-Bv_BJ1s-.js +0 -615
- package/dist/safe-effect-Dh8uw81c.js +0 -20
- package/dist/validate-C3XiA_zf.js +0 -10
- package/dist/validators/number.d.ts +0 -2
- package/dist/validators/number.js +0 -35
- 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
|
|
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
|
|
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,
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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-инстанс
|
|
40
|
+
* Ссылка на Proxy-инстанс
|
|
59
41
|
*/
|
|
60
42
|
private _proxyInstance?;
|
|
61
43
|
/**
|
|
62
|
-
*
|
|
44
|
+
* Cleanup декларативной схемы поведения (createForm({ behavior })). Вызывается в dispose().
|
|
63
45
|
*/
|
|
64
|
-
private
|
|
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
|
-
|
|
10
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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]
|
|
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
|
|
101
|
+
export type FormArrayProxy<T extends object> = ArrayNode<T> & {
|
|
91
102
|
/**
|
|
92
103
|
* Безопасный доступ к элементу массива по индексу
|
|
93
104
|
* Возвращает GroupNode с типизированными полями или undefined
|