@reformer/core 1.1.0 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/behaviors-DzYL8kY_.js +499 -0
- package/dist/behaviors.d.ts +6 -2
- package/dist/behaviors.js +19 -227
- package/dist/core/behavior/behavior-context.d.ts +6 -2
- package/dist/core/behavior/create-field-path.d.ts +3 -16
- package/dist/core/nodes/group-node.d.ts +14 -193
- package/dist/core/types/form-context.d.ts +10 -4
- package/dist/core/utils/field-path.d.ts +48 -0
- package/dist/core/utils/index.d.ts +1 -0
- package/dist/core/validation/core/validate-tree.d.ts +10 -4
- package/dist/core/validation/field-path.d.ts +3 -39
- package/dist/core/validation/validation-context.d.ts +23 -0
- package/dist/hooks/types.d.ts +328 -0
- package/dist/hooks/useFormControl.d.ts +13 -37
- package/dist/hooks/useFormControlValue.d.ts +167 -0
- package/dist/hooks/useSignalSubscription.d.ts +17 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2886 -8
- package/dist/{create-field-path-CdPF3lIK.js → registry-helpers-BRxAr6nG.js} +133 -347
- package/dist/validators-gXoHPdqM.js +418 -0
- package/dist/validators.d.ts +6 -2
- package/dist/validators.js +29 -296
- package/llms.txt +1283 -22
- package/package.json +8 -4
- package/dist/core/behavior/behavior-applicator.d.ts +0 -71
- package/dist/core/behavior/behavior-applicator.js +0 -92
- package/dist/core/behavior/behavior-context.js +0 -38
- package/dist/core/behavior/behavior-registry.js +0 -198
- package/dist/core/behavior/behaviors/compute-from.js +0 -84
- package/dist/core/behavior/behaviors/copy-from.js +0 -64
- package/dist/core/behavior/behaviors/enable-when.js +0 -81
- package/dist/core/behavior/behaviors/index.js +0 -11
- package/dist/core/behavior/behaviors/reset-when.js +0 -63
- package/dist/core/behavior/behaviors/revalidate-when.js +0 -51
- package/dist/core/behavior/behaviors/sync-fields.js +0 -66
- package/dist/core/behavior/behaviors/transform-value.js +0 -110
- package/dist/core/behavior/behaviors/watch-field.js +0 -56
- package/dist/core/behavior/compose-behavior.js +0 -166
- package/dist/core/behavior/create-field-path.js +0 -69
- package/dist/core/behavior/index.js +0 -17
- package/dist/core/behavior/types.js +0 -7
- package/dist/core/context/form-context-impl.js +0 -37
- package/dist/core/factories/index.js +0 -6
- package/dist/core/factories/node-factory.js +0 -281
- package/dist/core/nodes/array-node.js +0 -534
- package/dist/core/nodes/field-node.js +0 -510
- package/dist/core/nodes/form-node.js +0 -343
- package/dist/core/nodes/group-node/field-registry.d.ts +0 -191
- package/dist/core/nodes/group-node/field-registry.js +0 -215
- package/dist/core/nodes/group-node/index.d.ts +0 -11
- package/dist/core/nodes/group-node/index.js +0 -11
- package/dist/core/nodes/group-node/proxy-builder.d.ts +0 -71
- package/dist/core/nodes/group-node/proxy-builder.js +0 -161
- package/dist/core/nodes/group-node/state-manager.d.ts +0 -184
- package/dist/core/nodes/group-node/state-manager.js +0 -265
- package/dist/core/nodes/group-node.js +0 -770
- package/dist/core/types/deep-schema.js +0 -11
- package/dist/core/types/field-path.js +0 -4
- package/dist/core/types/form-context.js +0 -25
- package/dist/core/types/group-node-proxy.js +0 -31
- package/dist/core/types/index.js +0 -4
- package/dist/core/types/validation-schema.js +0 -10
- package/dist/core/utils/create-form.js +0 -24
- package/dist/core/utils/debounce.js +0 -197
- package/dist/core/utils/error-handler.js +0 -226
- package/dist/core/utils/field-path-navigator.js +0 -374
- package/dist/core/utils/index.js +0 -14
- package/dist/core/utils/registry-helpers.js +0 -79
- package/dist/core/utils/registry-stack.js +0 -86
- package/dist/core/utils/resources.js +0 -69
- package/dist/core/utils/subscription-manager.js +0 -214
- package/dist/core/utils/type-guards.js +0 -169
- package/dist/core/validation/core/apply-when.js +0 -41
- package/dist/core/validation/core/apply.js +0 -38
- package/dist/core/validation/core/index.js +0 -8
- package/dist/core/validation/core/validate-async.js +0 -45
- package/dist/core/validation/core/validate-tree.js +0 -37
- package/dist/core/validation/core/validate.js +0 -38
- package/dist/core/validation/field-path.js +0 -147
- package/dist/core/validation/index.js +0 -33
- package/dist/core/validation/validate-form.js +0 -152
- package/dist/core/validation/validation-applicator.js +0 -217
- package/dist/core/validation/validation-context.js +0 -75
- package/dist/core/validation/validation-registry.js +0 -298
- package/dist/core/validation/validators/array-validators.js +0 -86
- package/dist/core/validation/validators/date.js +0 -117
- package/dist/core/validation/validators/email.js +0 -60
- package/dist/core/validation/validators/index.js +0 -14
- package/dist/core/validation/validators/max-length.js +0 -60
- package/dist/core/validation/validators/max.js +0 -60
- package/dist/core/validation/validators/min-length.js +0 -60
- package/dist/core/validation/validators/min.js +0 -60
- package/dist/core/validation/validators/number.js +0 -90
- package/dist/core/validation/validators/pattern.js +0 -62
- package/dist/core/validation/validators/phone.js +0 -58
- package/dist/core/validation/validators/required.js +0 -69
- package/dist/core/validation/validators/url.js +0 -55
- package/dist/hooks/useFormControl.js +0 -298
- package/dist/node-factory-D7DOnSSN.js +0 -3200
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FieldNode - узел поля формы
|
|
3
|
-
*
|
|
4
|
-
* Представляет одно поле формы с валидацией и состоянием
|
|
5
|
-
* Наследует от FormNode и реализует все его абстрактные методы
|
|
6
|
-
*
|
|
7
|
-
* @group Nodes
|
|
8
|
-
*/
|
|
9
|
-
import { signal, computed, effect } from '@preact/signals-core';
|
|
10
|
-
import { FormNode } from './form-node';
|
|
11
|
-
import { SubscriptionManager } from '../utils/subscription-manager';
|
|
12
|
-
import { FormErrorHandler, ErrorStrategy } from '../utils/error-handler';
|
|
13
|
-
/**
|
|
14
|
-
* FieldNode - узел для отдельного поля формы
|
|
15
|
-
*
|
|
16
|
-
* @group Nodes
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```typescript
|
|
20
|
-
* const field = new FieldNode({
|
|
21
|
-
* value: '',
|
|
22
|
-
* component: Input,
|
|
23
|
-
* validators: [required, email],
|
|
24
|
-
* });
|
|
25
|
-
*
|
|
26
|
-
* field.setValue('test@mail.com');
|
|
27
|
-
* await field.validate();
|
|
28
|
-
* console.log(field.valid.value); // true
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export class FieldNode extends FormNode {
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// Приватные сигналы
|
|
34
|
-
// ============================================================================
|
|
35
|
-
_value;
|
|
36
|
-
_errors;
|
|
37
|
-
// _touched, _dirty, _status наследуются от FormNode (protected)
|
|
38
|
-
_pending;
|
|
39
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
-
_componentProps;
|
|
41
|
-
// ============================================================================
|
|
42
|
-
// Публичные computed signals
|
|
43
|
-
// ============================================================================
|
|
44
|
-
value;
|
|
45
|
-
valid;
|
|
46
|
-
invalid;
|
|
47
|
-
// touched, dirty, status наследуются от FormNode
|
|
48
|
-
pending;
|
|
49
|
-
errors;
|
|
50
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
-
componentProps;
|
|
52
|
-
/**
|
|
53
|
-
* Вычисляемое свойство: нужно ли показывать ошибку
|
|
54
|
-
* Ошибка показывается если поле невалидно И (touched ИЛИ dirty)
|
|
55
|
-
*/
|
|
56
|
-
shouldShowError;
|
|
57
|
-
// ============================================================================
|
|
58
|
-
// Конфигурация
|
|
59
|
-
// ============================================================================
|
|
60
|
-
validators;
|
|
61
|
-
asyncValidators;
|
|
62
|
-
updateOn;
|
|
63
|
-
initialValue;
|
|
64
|
-
currentValidationId = 0;
|
|
65
|
-
debounceMs;
|
|
66
|
-
validateDebounceTimer;
|
|
67
|
-
validateDebounceResolve;
|
|
68
|
-
/**
|
|
69
|
-
* Менеджер подписок для централизованного cleanup
|
|
70
|
-
* Использует SubscriptionManager вместо массива для управления подписками
|
|
71
|
-
*/
|
|
72
|
-
disposers = new SubscriptionManager();
|
|
73
|
-
component;
|
|
74
|
-
// ============================================================================
|
|
75
|
-
// Конструктор
|
|
76
|
-
// ============================================================================
|
|
77
|
-
constructor(config) {
|
|
78
|
-
super();
|
|
79
|
-
// Сохраняем конфигурацию
|
|
80
|
-
// FieldConfig.value имеет тип T | null, но FieldNode всегда работает с T
|
|
81
|
-
// null трактуется как начальное значение типа T
|
|
82
|
-
this.initialValue = config.value;
|
|
83
|
-
this.validators = config.validators || [];
|
|
84
|
-
this.asyncValidators = config.asyncValidators || [];
|
|
85
|
-
this.updateOn = config.updateOn || 'blur';
|
|
86
|
-
this.debounceMs = config.debounce || 0;
|
|
87
|
-
this.component = config.component;
|
|
88
|
-
// Инициализация приватных сигналов
|
|
89
|
-
this._value = signal(config.value);
|
|
90
|
-
this._errors = signal([]);
|
|
91
|
-
// _touched, _dirty, _status инициализируются в FormNode
|
|
92
|
-
this._pending = signal(false);
|
|
93
|
-
this._componentProps = signal(config.componentProps || {});
|
|
94
|
-
// Установка начального статуса если поле отключено
|
|
95
|
-
if (config.disabled) {
|
|
96
|
-
this._status.value = 'disabled';
|
|
97
|
-
}
|
|
98
|
-
// Создание computed signals
|
|
99
|
-
this.value = computed(() => this._value.value);
|
|
100
|
-
this.valid = computed(() => this._status.value === 'valid');
|
|
101
|
-
this.invalid = computed(() => this._status.value === 'invalid');
|
|
102
|
-
// touched, dirty, status создаются в FormNode
|
|
103
|
-
this.pending = computed(() => this._pending.value);
|
|
104
|
-
this.errors = computed(() => this._errors.value);
|
|
105
|
-
this.componentProps = computed(() => this._componentProps.value);
|
|
106
|
-
this.shouldShowError = computed(() => this._status.value === 'invalid' && (this._touched.value || this._dirty.value));
|
|
107
|
-
}
|
|
108
|
-
// ============================================================================
|
|
109
|
-
// Реализация абстрактных методов FormNode
|
|
110
|
-
// ============================================================================
|
|
111
|
-
getValue() {
|
|
112
|
-
return this._value.peek();
|
|
113
|
-
}
|
|
114
|
-
setValue(value, options) {
|
|
115
|
-
this._value.value = value;
|
|
116
|
-
this._dirty.value = true;
|
|
117
|
-
if (options?.emitEvent === false) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const hasOwnValidators = this.validators.length > 0 || this.asyncValidators.length > 0;
|
|
121
|
-
const hasErrors = this._errors.value.length > 0;
|
|
122
|
-
// 1. Если updateOn === 'change' → всегда валидируем
|
|
123
|
-
if (this.updateOn === 'change') {
|
|
124
|
-
this.validate();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
// 2. Если updateOn === 'blur' или 'submit':
|
|
128
|
-
// Валидируем только если у поля есть ошибки и собственные валидаторы
|
|
129
|
-
// Это позволяет скрывать ошибку при исправлении значения
|
|
130
|
-
// Поведение:
|
|
131
|
-
// - Если значение некорректно → обновляем/показываем ошибку
|
|
132
|
-
// - Если значение корректно → скрываем ошибку
|
|
133
|
-
// Но первая валидация произойдет только при blur/submit
|
|
134
|
-
if (hasErrors && hasOwnValidators) {
|
|
135
|
-
this.validate();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
patchValue(value) {
|
|
139
|
-
this.setValue(value);
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Сбросить поле к указанному значению (или к initialValue)
|
|
143
|
-
*
|
|
144
|
-
* @param value - опциональное значение для сброса. Если не указано, используется initialValue
|
|
145
|
-
*
|
|
146
|
-
* @remarks
|
|
147
|
-
* Этот метод:
|
|
148
|
-
* - Устанавливает значение в value или initialValue
|
|
149
|
-
* - Очищает ошибки валидации
|
|
150
|
-
* - Сбрасывает touched/dirty флаги
|
|
151
|
-
* - Устанавливает статус в 'valid'
|
|
152
|
-
*
|
|
153
|
-
* Если вам нужно сбросить к исходному значению, используйте resetToInitial()
|
|
154
|
-
*
|
|
155
|
-
* @example
|
|
156
|
-
* ```typescript
|
|
157
|
-
* // Сброс к initialValue
|
|
158
|
-
* field.reset();
|
|
159
|
-
*
|
|
160
|
-
* // Сброс к новому значению
|
|
161
|
-
* field.reset('new value');
|
|
162
|
-
* ```
|
|
163
|
-
*/
|
|
164
|
-
reset(value) {
|
|
165
|
-
this._value.value = value !== undefined ? value : this.initialValue;
|
|
166
|
-
this._errors.value = [];
|
|
167
|
-
this._touched.value = false;
|
|
168
|
-
this._dirty.value = false;
|
|
169
|
-
this._status.value = 'valid';
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Сбросить поле к исходному значению (initialValue)
|
|
173
|
-
*
|
|
174
|
-
* @remarks
|
|
175
|
-
* Алиас для reset() без параметров, но более явный:
|
|
176
|
-
* - resetToInitial() - явно показывает намерение вернуться к начальному значению
|
|
177
|
-
* - reset() - может принимать новое значение
|
|
178
|
-
*
|
|
179
|
-
* Полезно когда:
|
|
180
|
-
* - Пользователь нажал "Cancel" - вернуть форму в исходное состояние
|
|
181
|
-
* - Форма была изменена через reset(newValue), но нужно вернуться к самому началу
|
|
182
|
-
* - Явное намерение показать "отмену всех изменений"
|
|
183
|
-
*
|
|
184
|
-
* @example
|
|
185
|
-
* ```typescript
|
|
186
|
-
* const field = new FieldNode({ value: 'initial', component: Input });
|
|
187
|
-
*
|
|
188
|
-
* field.setValue('changed');
|
|
189
|
-
* field.reset('temp value');
|
|
190
|
-
* console.log(field.value.value); // 'temp value'
|
|
191
|
-
*
|
|
192
|
-
* field.resetToInitial();
|
|
193
|
-
* console.log(field.value.value); // 'initial'
|
|
194
|
-
* ```
|
|
195
|
-
*/
|
|
196
|
-
resetToInitial() {
|
|
197
|
-
this.reset(this.initialValue);
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Запустить валидацию поля
|
|
201
|
-
* @param options - опции валидации
|
|
202
|
-
* @returns `Promise<boolean>` - true если поле валидно
|
|
203
|
-
*
|
|
204
|
-
* @remarks
|
|
205
|
-
* Метод защищен от race conditions через validationId.
|
|
206
|
-
* При быстром вводе только последняя валидация применяет результаты.
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* ```typescript
|
|
210
|
-
* // Обычная валидация
|
|
211
|
-
* await field.validate();
|
|
212
|
-
*
|
|
213
|
-
* // С debounce
|
|
214
|
-
* await field.validate({ debounce: 300 });
|
|
215
|
-
* ```
|
|
216
|
-
*/
|
|
217
|
-
async validate(options) {
|
|
218
|
-
const debounce = options?.debounce ?? this.debounceMs;
|
|
219
|
-
// Если задан debounce, откладываем валидацию
|
|
220
|
-
if (debounce > 0 && this.asyncValidators.length > 0) {
|
|
221
|
-
return new Promise((resolve) => {
|
|
222
|
-
// Запоминаем текущий validationId перед debounce
|
|
223
|
-
const currentId = this.currentValidationId;
|
|
224
|
-
// Resolve предыдущий promise (если есть) как cancelled
|
|
225
|
-
if (this.validateDebounceResolve) {
|
|
226
|
-
this.validateDebounceResolve(false);
|
|
227
|
-
}
|
|
228
|
-
// Отменяем предыдущий таймер
|
|
229
|
-
if (this.validateDebounceTimer) {
|
|
230
|
-
clearTimeout(this.validateDebounceTimer);
|
|
231
|
-
}
|
|
232
|
-
// Сохраняем resolver для возможности отмены
|
|
233
|
-
this.validateDebounceResolve = resolve;
|
|
234
|
-
this.validateDebounceTimer = setTimeout(async () => {
|
|
235
|
-
// Очищаем resolver
|
|
236
|
-
this.validateDebounceResolve = undefined;
|
|
237
|
-
// Проверяем, не была ли запущена новая валидация во время debounce
|
|
238
|
-
// (другой вызов validate увеличил бы currentValidationId в validateImmediate)
|
|
239
|
-
if (currentId !== this.currentValidationId) {
|
|
240
|
-
// Эта валидация устарела
|
|
241
|
-
resolve(false);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const result = await this.validateImmediate();
|
|
245
|
-
resolve(result);
|
|
246
|
-
}, debounce);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
return this.validateImmediate();
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Немедленная валидация без debounce
|
|
253
|
-
* @private
|
|
254
|
-
* @remarks
|
|
255
|
-
* Защищена от race conditions:
|
|
256
|
-
* - Проверка validationId после синхронной валидации
|
|
257
|
-
* - Проверка перед установкой pending
|
|
258
|
-
* - Проверка после Promise.all
|
|
259
|
-
* - Проверка перед обработкой async результатов
|
|
260
|
-
* - Проверка перед очисткой errors
|
|
261
|
-
*/
|
|
262
|
-
async validateImmediate() {
|
|
263
|
-
const validationId = ++this.currentValidationId;
|
|
264
|
-
// Синхронная валидация
|
|
265
|
-
const syncErrors = [];
|
|
266
|
-
for (const validator of this.validators) {
|
|
267
|
-
const error = validator(this._value.value);
|
|
268
|
-
if (error)
|
|
269
|
-
syncErrors.push(error);
|
|
270
|
-
}
|
|
271
|
-
// Проверка #1: после синхронной валидации
|
|
272
|
-
if (validationId !== this.currentValidationId) {
|
|
273
|
-
return false; // Эта валидация устарела
|
|
274
|
-
}
|
|
275
|
-
if (syncErrors.length > 0) {
|
|
276
|
-
this._errors.value = syncErrors;
|
|
277
|
-
this._status.value = 'invalid';
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
// Асинхронная валидация - ПАРАЛЛЕЛЬНО
|
|
281
|
-
if (this.asyncValidators.length > 0) {
|
|
282
|
-
// Проверка #2: перед установкой pending
|
|
283
|
-
if (validationId !== this.currentValidationId) {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
this._pending.value = true;
|
|
287
|
-
this._status.value = 'pending';
|
|
288
|
-
// Выполняем все async валидаторы параллельно
|
|
289
|
-
// Каждый validator обернут в try-catch для обработки исключений
|
|
290
|
-
const asyncResults = await Promise.all(this.asyncValidators.map(async (validator) => {
|
|
291
|
-
try {
|
|
292
|
-
return await validator(this._value.value);
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
// Используем централизованный обработчик ошибок
|
|
296
|
-
return FormErrorHandler.handle(error, 'FieldNode AsyncValidator', ErrorStrategy.CONVERT);
|
|
297
|
-
}
|
|
298
|
-
}));
|
|
299
|
-
// Проверка #3: после Promise.all (основная проверка)
|
|
300
|
-
if (validationId !== this.currentValidationId) {
|
|
301
|
-
// Не сбрасываем pending, т.к. новая валидация может еще выполняться
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
this._pending.value = false;
|
|
305
|
-
// Проверка #4: перед обработкой async результатов
|
|
306
|
-
if (validationId !== this.currentValidationId) {
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
const asyncErrors = asyncResults.filter(Boolean);
|
|
310
|
-
if (asyncErrors.length > 0) {
|
|
311
|
-
this._errors.value = asyncErrors;
|
|
312
|
-
this._status.value = 'invalid';
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Проверка #5: перед очисткой errors (финальная проверка)
|
|
317
|
-
if (validationId !== this.currentValidationId) {
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
// Очищаем ошибки только если у поля есть собственные валидаторы
|
|
321
|
-
// Если валидаторов нет, значит используется ValidationSchema на уровне формы
|
|
322
|
-
// и ошибки устанавливаются извне через setErrors()
|
|
323
|
-
const hasOwnValidators = this.validators.length > 0 || this.asyncValidators.length > 0;
|
|
324
|
-
if (hasOwnValidators) {
|
|
325
|
-
this._errors.value = [];
|
|
326
|
-
this._status.value = 'valid';
|
|
327
|
-
}
|
|
328
|
-
return this._errors.value.length === 0;
|
|
329
|
-
}
|
|
330
|
-
setErrors(errors) {
|
|
331
|
-
this._errors.value = errors;
|
|
332
|
-
this._status.value = errors.length > 0 ? 'invalid' : 'valid';
|
|
333
|
-
}
|
|
334
|
-
clearErrors() {
|
|
335
|
-
this._errors.value = [];
|
|
336
|
-
this._status.value = 'valid';
|
|
337
|
-
}
|
|
338
|
-
// ============================================================================
|
|
339
|
-
// Protected hooks (Template Method pattern)
|
|
340
|
-
// ============================================================================
|
|
341
|
-
/**
|
|
342
|
-
* Hook: вызывается после markAsTouched()
|
|
343
|
-
*
|
|
344
|
-
* Для FieldNode: если updateOn === 'blur', запускаем валидацию
|
|
345
|
-
*/
|
|
346
|
-
onMarkAsTouched() {
|
|
347
|
-
if (this.updateOn === 'blur') {
|
|
348
|
-
this.validate();
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Hook: вызывается после disable()
|
|
353
|
-
*
|
|
354
|
-
* Для FieldNode: очищаем ошибки валидации
|
|
355
|
-
*/
|
|
356
|
-
onDisable() {
|
|
357
|
-
this._errors.value = [];
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Hook: вызывается после enable()
|
|
361
|
-
*
|
|
362
|
-
* Для FieldNode: запускаем валидацию
|
|
363
|
-
*/
|
|
364
|
-
onEnable() {
|
|
365
|
-
this.validate();
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Обновляет свойства компонента (например, опции для Select)
|
|
369
|
-
*
|
|
370
|
-
* @example
|
|
371
|
-
* ```typescript
|
|
372
|
-
* // Обновление опций для Select после загрузки справочников
|
|
373
|
-
* form.registrationAddress.city.updateComponentProps({
|
|
374
|
-
* options: cities
|
|
375
|
-
* });
|
|
376
|
-
* ```
|
|
377
|
-
*/
|
|
378
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
|
-
updateComponentProps(props) {
|
|
380
|
-
this._componentProps.value = {
|
|
381
|
-
...this._componentProps.value,
|
|
382
|
-
...props,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Динамически изменяет триггер валидации (updateOn)
|
|
387
|
-
* Полезно для адаптивной валидации - например, переключиться на instant feedback после первого submit
|
|
388
|
-
*
|
|
389
|
-
* @param updateOn - новый триггер валидации: 'change' | 'blur' | 'submit'
|
|
390
|
-
*
|
|
391
|
-
* @example
|
|
392
|
-
* ```typescript
|
|
393
|
-
* // Сценарий 1: Instant feedback после submit
|
|
394
|
-
* const form = new GroupNode({
|
|
395
|
-
* email: {
|
|
396
|
-
* value: '',
|
|
397
|
-
* component: Input,
|
|
398
|
-
* updateOn: 'submit', // Изначально валидация только при submit
|
|
399
|
-
* validators: [required, email]
|
|
400
|
-
* }
|
|
401
|
-
* });
|
|
402
|
-
*
|
|
403
|
-
* await form.submit(async (values) => {
|
|
404
|
-
* // После submit переключаем на instant feedback
|
|
405
|
-
* form.email.setUpdateOn('change');
|
|
406
|
-
* await api.save(values);
|
|
407
|
-
* });
|
|
408
|
-
*
|
|
409
|
-
* // Теперь валидация происходит при каждом изменении
|
|
410
|
-
*
|
|
411
|
-
* // Сценарий 2: Прогрессивное улучшение
|
|
412
|
-
* form.email.setUpdateOn('blur'); // Сначала только при blur
|
|
413
|
-
* // ... пользователь начинает вводить ...
|
|
414
|
-
* form.email.setUpdateOn('change'); // Переключаем на change для real-time feedback
|
|
415
|
-
* ```
|
|
416
|
-
*/
|
|
417
|
-
setUpdateOn(updateOn) {
|
|
418
|
-
this.updateOn = updateOn;
|
|
419
|
-
}
|
|
420
|
-
getUpdateOn() {
|
|
421
|
-
return this.updateOn;
|
|
422
|
-
}
|
|
423
|
-
// ============================================================================
|
|
424
|
-
// Методы-помощники для реактивности (Фаза 1)
|
|
425
|
-
// ============================================================================
|
|
426
|
-
/**
|
|
427
|
-
* Подписка на изменения значения поля
|
|
428
|
-
* Автоматически отслеживает изменения через @preact/signals effect
|
|
429
|
-
*
|
|
430
|
-
* @param callback - Функция, вызываемая при изменении значения
|
|
431
|
-
* @returns Функция отписки для cleanup
|
|
432
|
-
*
|
|
433
|
-
* @example
|
|
434
|
-
* ```typescript
|
|
435
|
-
* const unsubscribe = form.email.watch((value) => {
|
|
436
|
-
* console.log('Email changed:', value);
|
|
437
|
-
* });
|
|
438
|
-
*
|
|
439
|
-
* // Cleanup
|
|
440
|
-
* useEffect(() => unsubscribe, []);
|
|
441
|
-
* ```
|
|
442
|
-
*/
|
|
443
|
-
watch(callback) {
|
|
444
|
-
const dispose = effect(() => {
|
|
445
|
-
const currentValue = this.value.value; // track changes
|
|
446
|
-
callback(currentValue);
|
|
447
|
-
});
|
|
448
|
-
// Регистрируем через SubscriptionManager и возвращаем unsubscribe
|
|
449
|
-
const key = `watch-${Date.now()}-${Math.random()}`;
|
|
450
|
-
return this.disposers.add(key, dispose);
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Вычисляемое значение из других полей
|
|
454
|
-
* Автоматически обновляет текущее поле при изменении источников
|
|
455
|
-
*
|
|
456
|
-
* @param sources - Массив ReadonlySignal для отслеживания
|
|
457
|
-
* @param computeFn - Функция вычисления нового значения
|
|
458
|
-
* @returns Функция отписки для cleanup
|
|
459
|
-
*
|
|
460
|
-
* @example
|
|
461
|
-
* ```typescript
|
|
462
|
-
* // Автоматический расчет первоначального взноса (20% от стоимости)
|
|
463
|
-
* const dispose = form.initialPayment.computeFrom(
|
|
464
|
-
* [form.propertyValue.value],
|
|
465
|
-
* (propertyValue) => {
|
|
466
|
-
* return propertyValue ? propertyValue * 0.2 : null;
|
|
467
|
-
* }
|
|
468
|
-
* );
|
|
469
|
-
*
|
|
470
|
-
* // Cleanup
|
|
471
|
-
* useEffect(() => dispose, []);
|
|
472
|
-
* ```
|
|
473
|
-
*/
|
|
474
|
-
computeFrom(sources, computeFn) {
|
|
475
|
-
const dispose = effect(() => {
|
|
476
|
-
// Читаем все источники для отслеживания
|
|
477
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
478
|
-
const sourceValues = sources.map((source) => source.value);
|
|
479
|
-
// Вычисляем новое значение
|
|
480
|
-
const newValue = computeFn(...sourceValues);
|
|
481
|
-
// Устанавливаем значение без триггера событий (избегаем циклов)
|
|
482
|
-
this.setValue(newValue, { emitEvent: false });
|
|
483
|
-
});
|
|
484
|
-
// Регистрируем через SubscriptionManager и возвращаем unsubscribe
|
|
485
|
-
const key = `computeFrom-${Date.now()}-${Math.random()}`;
|
|
486
|
-
return this.disposers.add(key, dispose);
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* Очистить все ресурсы и таймеры
|
|
490
|
-
* Должен вызываться при unmount компонента
|
|
491
|
-
*
|
|
492
|
-
* @example
|
|
493
|
-
* ```typescript
|
|
494
|
-
* useEffect(() => {
|
|
495
|
-
* return () => {
|
|
496
|
-
* field.dispose();
|
|
497
|
-
* };
|
|
498
|
-
* }, []);
|
|
499
|
-
* ```
|
|
500
|
-
*/
|
|
501
|
-
dispose() {
|
|
502
|
-
// Очищаем все subscriptions через SubscriptionManager
|
|
503
|
-
this.disposers.dispose();
|
|
504
|
-
// Очищаем debounce таймер если он есть
|
|
505
|
-
if (this.validateDebounceTimer) {
|
|
506
|
-
clearTimeout(this.validateDebounceTimer);
|
|
507
|
-
this.validateDebounceTimer = undefined;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|