@reformer/core 1.1.0-beta.4 → 1.1.0-beta.6

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.
@@ -1,3217 +0,0 @@
1
- import { d, w as l, E as c, V as A, c as k, B as x } from "./create-field-path-DcXDTWil.js";
2
- import { v4 as S } from "uuid";
3
- class b {
4
- // ============================================================================
5
- // Protected состояние (для Template Method паттерна)
6
- // ============================================================================
7
- /**
8
- * Пользователь взаимодействовал с узлом (touched)
9
- * Protected: наследники могут читать/изменять через методы
10
- */
11
- _touched = d(!1);
12
- /**
13
- * Значение узла было изменено (dirty)
14
- * Protected: наследники могут читать/изменять через методы
15
- */
16
- _dirty = d(!1);
17
- /**
18
- * Текущий статус узла
19
- * Protected: наследники могут читать/изменять через методы
20
- */
21
- _status = d("valid");
22
- // ============================================================================
23
- // Публичные computed signals (readonly для внешнего мира)
24
- // ============================================================================
25
- /**
26
- * Пользователь взаимодействовал с узлом (touched)
27
- * Computed из _touched для предоставления readonly интерфейса
28
- */
29
- touched = l(() => this._touched.value);
30
- /**
31
- * Пользователь не взаимодействовал с узлом (untouched)
32
- */
33
- untouched = l(() => !this._touched.value);
34
- /**
35
- * Значение узла было изменено (dirty)
36
- * Computed из _dirty для предоставления readonly интерфейса
37
- */
38
- dirty = l(() => this._dirty.value);
39
- /**
40
- * Значение узла не было изменено (pristine)
41
- */
42
- pristine = l(() => !this._dirty.value);
43
- /**
44
- * Текущий статус узла
45
- * Computed из _status для предоставления readonly интерфейса
46
- */
47
- status = l(() => this._status.value);
48
- /**
49
- * Узел отключен (disabled)
50
- */
51
- disabled = l(() => this._status.value === "disabled");
52
- /**
53
- * Узел включен (enabled)
54
- */
55
- enabled = l(() => this._status.value !== "disabled");
56
- /**
57
- * Получить ошибки валидации с фильтрацией
58
- *
59
- * Позволяет фильтровать ошибки по различным критериям:
60
- * - По коду ошибки
61
- * - По сообщению (частичное совпадение)
62
- * - По параметрам
63
- * - Через кастомный предикат
64
- *
65
- * Без параметров возвращает все ошибки (эквивалент errors.value)
66
- *
67
- * @param options - Опции фильтрации ошибок
68
- * @returns Отфильтрованный массив ошибок валидации
69
- *
70
- * @example
71
- * ```typescript
72
- * // Все ошибки
73
- * const allErrors = form.getErrors();
74
- *
75
- * // Ошибки с конкретным кодом
76
- * const requiredErrors = form.getErrors({ code: 'required' });
77
- *
78
- * // Ошибки с несколькими кодами
79
- * const errors = form.getErrors({ code: ['required', 'email'] });
80
- *
81
- * // Ошибки по сообщению
82
- * const passwordErrors = form.getErrors({ message: 'Password' });
83
- *
84
- * // Ошибки по параметрам
85
- * const minLengthErrors = form.getErrors({
86
- * params: { minLength: 8 }
87
- * });
88
- *
89
- * // Кастомная фильтрация
90
- * const customErrors = form.getErrors({
91
- * predicate: (err) => err.code.startsWith('custom_')
92
- * });
93
- * ```
94
- */
95
- getErrors(t) {
96
- const e = this.errors.value;
97
- return t ? e.filter((i) => {
98
- if (t.code !== void 0 && !(Array.isArray(t.code) ? t.code : [t.code]).includes(i.code) || t.message !== void 0 && !i.message.toLowerCase().includes(t.message.toLowerCase()))
99
- return !1;
100
- if (t.params !== void 0) {
101
- if (!i.params)
102
- return !1;
103
- for (const [s, a] of Object.entries(t.params))
104
- if (i.params[s] !== a)
105
- return !1;
106
- }
107
- return !(t.predicate !== void 0 && !t.predicate(i));
108
- }) : e;
109
- }
110
- // ============================================================================
111
- // Методы управления состоянием (Template Method)
112
- // ============================================================================
113
- /**
114
- * Отметить узел как touched (пользователь взаимодействовал)
115
- *
116
- * Template Method: обновляет signal в базовом классе,
117
- * вызывает hook для кастомной логики в наследниках
118
- */
119
- markAsTouched() {
120
- this._touched.value = !0, this.onMarkAsTouched();
121
- }
122
- /**
123
- * Отметить узел как untouched
124
- *
125
- * Template Method: обновляет signal в базовом классе,
126
- * вызывает hook для кастомной логики в наследниках
127
- */
128
- markAsUntouched() {
129
- this._touched.value = !1, this.onMarkAsUntouched();
130
- }
131
- /**
132
- * Отметить узел как dirty (значение изменено)
133
- *
134
- * Template Method: обновляет signal в базовом классе,
135
- * вызывает hook для кастомной логики в наследниках
136
- */
137
- markAsDirty() {
138
- this._dirty.value = !0, this.onMarkAsDirty();
139
- }
140
- /**
141
- * Отметить узел как pristine (значение не изменено)
142
- *
143
- * Template Method: обновляет signal в базовом классе,
144
- * вызывает hook для кастомной логики в наследниках
145
- */
146
- markAsPristine() {
147
- this._dirty.value = !1, this.onMarkAsPristine();
148
- }
149
- /**
150
- * Пометить все поля (включая вложенные) как touched
151
- * Алиас для markAsTouched(), но более явно показывает намерение
152
- * пометить ВСЕ поля рекурсивно
153
- *
154
- * Полезно для:
155
- * - Показа всех ошибок валидации перед submit
156
- * - Принудительного отображения ошибок при нажатии "Validate All"
157
- * - Отображения невалидных полей в wizard/step form
158
- *
159
- * @example
160
- * ```typescript
161
- * // Показать все ошибки перед submit
162
- * form.touchAll();
163
- * const isValid = await form.validate();
164
- * if (!isValid) {
165
- * // Все ошибки теперь видны пользователю
166
- * }
167
- *
168
- * // Или использовать submit() который уже вызывает touchAll
169
- * await form.submit(async (values) => {
170
- * await api.save(values);
171
- * });
172
- * ```
173
- */
174
- touchAll() {
175
- this.markAsTouched();
176
- }
177
- // ============================================================================
178
- // Методы управления доступностью (Template Method)
179
- // ============================================================================
180
- /**
181
- * Отключить узел
182
- *
183
- * Template Method: обновляет статус в базовом классе,
184
- * вызывает hook для кастомной логики в наследниках
185
- *
186
- * Отключенные узлы не проходят валидацию и не включаются в getValue()
187
- */
188
- disable() {
189
- this._status.value = "disabled", this.onDisable();
190
- }
191
- /**
192
- * Включить узел
193
- *
194
- * Template Method: обновляет статус в базовом классе,
195
- * вызывает hook для кастомной логики в наследниках
196
- */
197
- enable() {
198
- this._status.value = "valid", this.onEnable();
199
- }
200
- // ============================================================================
201
- // Protected hooks (для переопределения в наследниках)
202
- // ============================================================================
203
- /**
204
- * Hook: вызывается после markAsTouched()
205
- *
206
- * Переопределите в наследниках для дополнительной логики:
207
- * - GroupNode: пометить все дочерние узлы как touched
208
- * - ArrayNode: пометить все элементы массива как touched
209
- * - FieldNode: пустая реализация (нет дочерних узлов)
210
- *
211
- * @example
212
- * ```typescript
213
- * // GroupNode
214
- * protected onMarkAsTouched(): void {
215
- * this.fields.forEach(field => field.markAsTouched());
216
- * }
217
- * ```
218
- */
219
- onMarkAsTouched() {
220
- }
221
- /**
222
- * Hook: вызывается после markAsUntouched()
223
- *
224
- * Переопределите в наследниках для дополнительной логики:
225
- * - GroupNode: пометить все дочерние узлы как untouched
226
- * - ArrayNode: пометить все элементы массива как untouched
227
- * - FieldNode: пустая реализация (нет дочерних узлов)
228
- */
229
- onMarkAsUntouched() {
230
- }
231
- /**
232
- * Hook: вызывается после markAsDirty()
233
- *
234
- * Переопределите в наследниках для дополнительной логики:
235
- * - GroupNode: может обновить родительскую форму
236
- * - ArrayNode: может обновить родительскую форму
237
- * - FieldNode: пустая реализация
238
- */
239
- onMarkAsDirty() {
240
- }
241
- /**
242
- * Hook: вызывается после markAsPristine()
243
- *
244
- * Переопределите в наследниках для дополнительной логики:
245
- * - GroupNode: пометить все дочерние узлы как pristine
246
- * - ArrayNode: пометить все элементы массива как pristine
247
- * - FieldNode: пустая реализация
248
- */
249
- onMarkAsPristine() {
250
- }
251
- /**
252
- * Hook: вызывается после disable()
253
- *
254
- * Переопределите в наследниках для дополнительной логики:
255
- * - GroupNode: отключить все дочерние узлы
256
- * - ArrayNode: отключить все элементы массива
257
- * - FieldNode: очистить ошибки валидации
258
- *
259
- * @example
260
- * ```typescript
261
- * // GroupNode
262
- * protected onDisable(): void {
263
- * this.fields.forEach(field => field.disable());
264
- * }
265
- * ```
266
- */
267
- onDisable() {
268
- }
269
- /**
270
- * Hook: вызывается после enable()
271
- *
272
- * Переопределите в наследниках для дополнительной логики:
273
- * - GroupNode: включить все дочерние узлы
274
- * - ArrayNode: включить все элементы массива
275
- * - FieldNode: пустая реализация
276
- */
277
- onEnable() {
278
- }
279
- }
280
- class V {
281
- /**
282
- * Хранилище подписок
283
- * Ключ: уникальный идентификатор подписки
284
- * Значение: функция отписки (dispose)
285
- */
286
- subscriptions = /* @__PURE__ */ new Map();
287
- /**
288
- * Добавляет подписку
289
- *
290
- * Если подписка с таким ключом уже существует, отписывается от неё
291
- * и заменяет новой. Это предотвращает утечки памяти при повторной
292
- * регистрации подписки с тем же ключом.
293
- *
294
- * @param key Уникальный ключ подписки
295
- * @param dispose Функция отписки (обычно возвращаемая из effect())
296
- * @returns Функция для отписки от этой конкретной подписки
297
- *
298
- * @example
299
- * ```typescript
300
- * const manager = new SubscriptionManager();
301
- *
302
- * // Добавление подписки
303
- * const unsubscribe = manager.add('mySubscription', () => {
304
- * console.log('Disposing subscription');
305
- * });
306
- *
307
- * // Отписка через возвращаемую функцию
308
- * unsubscribe();
309
- *
310
- * // Или через manager.remove()
311
- * manager.add('anotherSub', disposeFn);
312
- * manager.remove('anotherSub');
313
- * ```
314
- */
315
- add(t, e) {
316
- return this.subscriptions.has(t) && (process.env.NODE_ENV !== "production" && console.warn(`[SubscriptionManager] Subscription "${t}" already exists, replacing`), this.subscriptions.get(t)?.()), this.subscriptions.set(t, e), () => this.remove(t);
317
- }
318
- /**
319
- * Удаляет подписку по ключу
320
- *
321
- * Вызывает функцию отписки и удаляет подписку из хранилища.
322
- * Если подписка с таким ключом не найдена, ничего не делает.
323
- *
324
- * @param key Ключ подписки
325
- * @returns true, если подписка была удалена, false если не найдена
326
- *
327
- * @example
328
- * ```typescript
329
- * const manager = new SubscriptionManager();
330
- * manager.add('mySub', disposeFn);
331
- *
332
- * // Удаление подписки
333
- * const removed = manager.remove('mySub'); // true
334
- *
335
- * // Попытка удалить несуществующую подписку
336
- * const removed2 = manager.remove('nonExistent'); // false
337
- * ```
338
- */
339
- remove(t) {
340
- const e = this.subscriptions.get(t);
341
- return e ? (e(), this.subscriptions.delete(t), !0) : !1;
342
- }
343
- /**
344
- * Очищает все подписки
345
- *
346
- * Вызывает функции отписки для всех активных подписок
347
- * и очищает хранилище. Обычно используется при dispose узла формы.
348
- *
349
- * @example
350
- * ```typescript
351
- * class FieldNode {
352
- * private subscriptions = new SubscriptionManager();
353
- *
354
- * dispose() {
355
- * // Отписываемся от всех effect
356
- * this.subscriptions.clear();
357
- * }
358
- * }
359
- * ```
360
- */
361
- clear() {
362
- this.subscriptions.forEach((t) => {
363
- t();
364
- }), this.subscriptions.clear();
365
- }
366
- /**
367
- * Возвращает количество активных подписок
368
- *
369
- * Полезно для отладки утечек памяти. Если количество подписок
370
- * растет без ограничений, это может указывать на то, что
371
- * компоненты не отписываются должным образом.
372
- *
373
- * @returns Количество активных подписок
374
- *
375
- * @example
376
- * ```typescript
377
- * const manager = new SubscriptionManager();
378
- * console.log(manager.size()); // 0
379
- *
380
- * manager.add('sub1', disposeFn1);
381
- * manager.add('sub2', disposeFn2);
382
- * console.log(manager.size()); // 2
383
- *
384
- * manager.clear();
385
- * console.log(manager.size()); // 0
386
- * ```
387
- */
388
- size() {
389
- return this.subscriptions.size;
390
- }
391
- /**
392
- * Проверяет, есть ли подписка с данным ключом
393
- *
394
- * @param key Ключ подписки
395
- * @returns true, если подписка существует
396
- *
397
- * @example
398
- * ```typescript
399
- * const manager = new SubscriptionManager();
400
- * manager.add('mySub', disposeFn);
401
- *
402
- * console.log(manager.has('mySub')); // true
403
- * console.log(manager.has('nonExistent')); // false
404
- * ```
405
- */
406
- has(t) {
407
- return this.subscriptions.has(t);
408
- }
409
- /**
410
- * Возвращает список всех ключей активных подписок
411
- *
412
- * Полезно для отладки: можно увидеть, какие подписки активны.
413
- *
414
- * @returns Массив ключей всех активных подписок
415
- *
416
- * @example
417
- * ```typescript
418
- * const manager = new SubscriptionManager();
419
- * manager.add('watch-value', disposeFn1);
420
- * manager.add('watch-errors', disposeFn2);
421
- *
422
- * console.log(manager.getKeys()); // ['watch-value', 'watch-errors']
423
- * ```
424
- */
425
- getKeys() {
426
- return Array.from(this.subscriptions.keys());
427
- }
428
- /**
429
- * Отписывается от всех подписок (алиас для clear())
430
- *
431
- * Используется при dispose() узла формы для совместимости с ожидаемым API.
432
- *
433
- * @example
434
- * ```typescript
435
- * class FieldNode {
436
- * private subscriptions = new SubscriptionManager();
437
- *
438
- * dispose() {
439
- * this.subscriptions.dispose();
440
- * }
441
- * }
442
- * ```
443
- */
444
- dispose() {
445
- this.clear();
446
- }
447
- }
448
- var y = /* @__PURE__ */ ((r) => (r.THROW = "throw", r.LOG = "log", r.CONVERT = "convert", r))(y || {});
449
- class p {
450
- /**
451
- * Обработать ошибку согласно заданной стратегии
452
- *
453
- * @param error Ошибка для обработки (Error | string | unknown)
454
- * @param context Контекст ошибки для логирования (например, 'AsyncValidator', 'BehaviorRegistry')
455
- * @param strategy Стратегия обработки (THROW | LOG | CONVERT)
456
- * @returns ValidationError если strategy = CONVERT, undefined если strategy = LOG, никогда не возвращается если strategy = THROW
457
- *
458
- * @example
459
- * ```typescript
460
- * // THROW - пробросить ошибку
461
- * try {
462
- * riskyOperation();
463
- * } catch (error) {
464
- * FormErrorHandler.handle(error, 'RiskyOperation', ErrorStrategy.THROW);
465
- * // Этот код никогда не выполнится
466
- * }
467
- *
468
- * // LOG - залогировать и продолжить
469
- * try {
470
- * nonCriticalOperation();
471
- * } catch (error) {
472
- * FormErrorHandler.handle(error, 'NonCritical', ErrorStrategy.LOG);
473
- * // Продолжаем выполнение
474
- * }
475
- *
476
- * // CONVERT - конвертировать в ValidationError
477
- * try {
478
- * await validator(value);
479
- * } catch (error) {
480
- * const validationError = FormErrorHandler.handle(
481
- * error,
482
- * 'AsyncValidator',
483
- * ErrorStrategy.CONVERT
484
- * );
485
- * return validationError;
486
- * }
487
- * ```
488
- */
489
- static handle(t, e, i = "throw") {
490
- const s = this.extractMessage(t);
491
- switch (i) {
492
- case "throw":
493
- throw t;
494
- case "log":
495
- return;
496
- case "convert":
497
- return {
498
- code: "validator_error",
499
- message: s,
500
- params: { field: e }
501
- };
502
- }
503
- }
504
- /**
505
- * Извлечь сообщение из ошибки
506
- *
507
- * Обрабатывает различные типы ошибок:
508
- * - Error объекты → error.message
509
- * - Строки → возвращает как есть
510
- * - Объекты с message → извлекает message
511
- * - Другое → String(error)
512
- *
513
- * @param error Ошибка для извлечения сообщения
514
- * @returns Сообщение ошибки
515
- * @private
516
- *
517
- * @example
518
- * ```typescript
519
- * FormErrorHandler.extractMessage(new Error('Test'));
520
- * // 'Test'
521
- *
522
- * FormErrorHandler.extractMessage('String error');
523
- * // 'String error'
524
- *
525
- * FormErrorHandler.extractMessage({ message: 'Object error' });
526
- * // 'Object error'
527
- *
528
- * FormErrorHandler.extractMessage(null);
529
- * // 'null'
530
- * ```
531
- */
532
- static extractMessage(t) {
533
- return t instanceof Error ? t.message : typeof t == "string" ? t : typeof t == "object" && t !== null && "message" in t ? String(t.message) : String(t);
534
- }
535
- /**
536
- * Создать ValidationError с заданными параметрами
537
- *
538
- * Утилитная функция для создания ValidationError объектов
539
- *
540
- * @param code Код ошибки
541
- * @param message Сообщение ошибки
542
- * @param field Поле (опционально)
543
- * @returns ValidationError объект
544
- *
545
- * @example
546
- * ```typescript
547
- * const error = FormErrorHandler.createValidationError(
548
- * 'required',
549
- * 'This field is required',
550
- * 'email'
551
- * );
552
- * // { code: 'required', message: 'This field is required', field: 'email' }
553
- * ```
554
- */
555
- static createValidationError(t, e, i) {
556
- return {
557
- code: t,
558
- message: e,
559
- params: i ? { field: i } : void 0
560
- };
561
- }
562
- /**
563
- * Проверить, является ли объект ValidationError
564
- *
565
- * Type guard для ValidationError
566
- *
567
- * @param value Значение для проверки
568
- * @returns true если value является ValidationError
569
- *
570
- * @example
571
- * ```typescript
572
- * if (FormErrorHandler.isValidationError(result)) {
573
- * console.log(result.code); // OK, типобезопасно
574
- * }
575
- * ```
576
- */
577
- static isValidationError(t) {
578
- return typeof t == "object" && t !== null && "code" in t && "message" in t && typeof t.code == "string" && typeof t.message == "string";
579
- }
580
- }
581
- class F extends b {
582
- // ============================================================================
583
- // Приватные сигналы
584
- // ============================================================================
585
- _value;
586
- _errors;
587
- // _touched, _dirty, _status наследуются от FormNode (protected)
588
- _pending;
589
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
590
- _componentProps;
591
- // ============================================================================
592
- // Публичные computed signals
593
- // ============================================================================
594
- value;
595
- valid;
596
- invalid;
597
- // touched, dirty, status наследуются от FormNode
598
- pending;
599
- errors;
600
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
601
- componentProps;
602
- /**
603
- * Вычисляемое свойство: нужно ли показывать ошибку
604
- * Ошибка показывается если поле невалидно И (touched ИЛИ dirty)
605
- */
606
- shouldShowError;
607
- // ============================================================================
608
- // Конфигурация
609
- // ============================================================================
610
- validators;
611
- asyncValidators;
612
- updateOn;
613
- initialValue;
614
- currentValidationId = 0;
615
- debounceMs;
616
- validateDebounceTimer;
617
- validateDebounceResolve;
618
- /**
619
- * Менеджер подписок для централизованного cleanup
620
- * Использует SubscriptionManager вместо массива для управления подписками
621
- */
622
- disposers = new V();
623
- component;
624
- // ============================================================================
625
- // Конструктор
626
- // ============================================================================
627
- constructor(t) {
628
- super(), this.initialValue = t.value, this.validators = t.validators || [], this.asyncValidators = t.asyncValidators || [], this.updateOn = t.updateOn || "blur", this.debounceMs = t.debounce || 0, this.component = t.component, this._value = d(t.value), this._errors = d([]), this._pending = d(!1), this._componentProps = d(t.componentProps || {}), t.disabled && (this._status.value = "disabled"), this.value = l(() => this._value.value), this.valid = l(() => this._status.value === "valid"), this.invalid = l(() => this._status.value === "invalid"), this.pending = l(() => this._pending.value), this.errors = l(() => this._errors.value), this.componentProps = l(() => this._componentProps.value), this.shouldShowError = l(
629
- () => this._status.value === "invalid" && (this._touched.value || this._dirty.value)
630
- );
631
- }
632
- // ============================================================================
633
- // Реализация абстрактных методов FormNode
634
- // ============================================================================
635
- getValue() {
636
- return this._value.peek();
637
- }
638
- setValue(t, e) {
639
- if (this._value.value = t, this._dirty.value = !0, e?.emitEvent === !1)
640
- return;
641
- const i = this.validators.length > 0 || this.asyncValidators.length > 0, s = this._errors.value.length > 0;
642
- if (this.updateOn === "change") {
643
- this.validate();
644
- return;
645
- }
646
- s && i && this.validate();
647
- }
648
- patchValue(t) {
649
- this.setValue(t);
650
- }
651
- /**
652
- * Сбросить поле к указанному значению (или к initialValue)
653
- *
654
- * @param value - опциональное значение для сброса. Если не указано, используется initialValue
655
- *
656
- * @remarks
657
- * Этот метод:
658
- * - Устанавливает значение в value или initialValue
659
- * - Очищает ошибки валидации
660
- * - Сбрасывает touched/dirty флаги
661
- * - Устанавливает статус в 'valid'
662
- *
663
- * Если вам нужно сбросить к исходному значению, используйте resetToInitial()
664
- *
665
- * @example
666
- * ```typescript
667
- * // Сброс к initialValue
668
- * field.reset();
669
- *
670
- * // Сброс к новому значению
671
- * field.reset('new value');
672
- * ```
673
- */
674
- reset(t) {
675
- this._value.value = t !== void 0 ? t : this.initialValue, this._errors.value = [], this._touched.value = !1, this._dirty.value = !1, this._status.value = "valid";
676
- }
677
- /**
678
- * Сбросить поле к исходному значению (initialValue)
679
- *
680
- * @remarks
681
- * Алиас для reset() без параметров, но более явный:
682
- * - resetToInitial() - явно показывает намерение вернуться к начальному значению
683
- * - reset() - может принимать новое значение
684
- *
685
- * Полезно когда:
686
- * - Пользователь нажал "Cancel" - вернуть форму в исходное состояние
687
- * - Форма была изменена через reset(newValue), но нужно вернуться к самому началу
688
- * - Явное намерение показать "отмену всех изменений"
689
- *
690
- * @example
691
- * ```typescript
692
- * const field = new FieldNode({ value: 'initial', component: Input });
693
- *
694
- * field.setValue('changed');
695
- * field.reset('temp value');
696
- * console.log(field.value.value); // 'temp value'
697
- *
698
- * field.resetToInitial();
699
- * console.log(field.value.value); // 'initial'
700
- * ```
701
- */
702
- resetToInitial() {
703
- this.reset(this.initialValue);
704
- }
705
- /**
706
- * Запустить валидацию поля
707
- * @param options - опции валидации
708
- * @returns `Promise<boolean>` - true если поле валидно
709
- *
710
- * @remarks
711
- * Метод защищен от race conditions через validationId.
712
- * При быстром вводе только последняя валидация применяет результаты.
713
- *
714
- * @example
715
- * ```typescript
716
- * // Обычная валидация
717
- * await field.validate();
718
- *
719
- * // С debounce
720
- * await field.validate({ debounce: 300 });
721
- * ```
722
- */
723
- async validate(t) {
724
- const e = t?.debounce ?? this.debounceMs;
725
- return e > 0 && this.asyncValidators.length > 0 ? new Promise((i) => {
726
- const s = this.currentValidationId;
727
- this.validateDebounceResolve && this.validateDebounceResolve(!1), this.validateDebounceTimer && clearTimeout(this.validateDebounceTimer), this.validateDebounceResolve = i, this.validateDebounceTimer = setTimeout(async () => {
728
- if (this.validateDebounceResolve = void 0, s !== this.currentValidationId) {
729
- i(!1);
730
- return;
731
- }
732
- const a = await this.validateImmediate();
733
- i(a);
734
- }, e);
735
- }) : this.validateImmediate();
736
- }
737
- /**
738
- * Немедленная валидация без debounce
739
- * @private
740
- * @remarks
741
- * Защищена от race conditions:
742
- * - Проверка validationId после синхронной валидации
743
- * - Проверка перед установкой pending
744
- * - Проверка после Promise.all
745
- * - Проверка перед обработкой async результатов
746
- * - Проверка перед очисткой errors
747
- */
748
- async validateImmediate() {
749
- const t = ++this.currentValidationId, e = [];
750
- for (const s of this.validators) {
751
- const a = s(this._value.value);
752
- a && e.push(a);
753
- }
754
- if (t !== this.currentValidationId)
755
- return !1;
756
- if (e.length > 0)
757
- return this._errors.value = e, this._status.value = "invalid", !1;
758
- if (this.asyncValidators.length > 0) {
759
- if (t !== this.currentValidationId)
760
- return !1;
761
- this._pending.value = !0, this._status.value = "pending";
762
- const s = await Promise.all(
763
- this.asyncValidators.map(async (n) => {
764
- try {
765
- return await n(this._value.value);
766
- } catch (o) {
767
- return p.handle(
768
- o,
769
- "FieldNode AsyncValidator",
770
- y.CONVERT
771
- );
772
- }
773
- })
774
- );
775
- if (t !== this.currentValidationId || (this._pending.value = !1, t !== this.currentValidationId))
776
- return !1;
777
- const a = s.filter(Boolean);
778
- if (a.length > 0)
779
- return this._errors.value = a, this._status.value = "invalid", !1;
780
- }
781
- return t !== this.currentValidationId ? !1 : ((this.validators.length > 0 || this.asyncValidators.length > 0) && (this._errors.value = [], this._status.value = "valid"), this._errors.value.length === 0);
782
- }
783
- setErrors(t) {
784
- this._errors.value = t, this._status.value = t.length > 0 ? "invalid" : "valid";
785
- }
786
- clearErrors() {
787
- this._errors.value = [], this._status.value = "valid";
788
- }
789
- // ============================================================================
790
- // Protected hooks (Template Method pattern)
791
- // ============================================================================
792
- /**
793
- * Hook: вызывается после markAsTouched()
794
- *
795
- * Для FieldNode: если updateOn === 'blur', запускаем валидацию
796
- */
797
- onMarkAsTouched() {
798
- this.updateOn === "blur" && this.validate();
799
- }
800
- /**
801
- * Hook: вызывается после disable()
802
- *
803
- * Для FieldNode: очищаем ошибки валидации
804
- */
805
- onDisable() {
806
- this._errors.value = [];
807
- }
808
- /**
809
- * Hook: вызывается после enable()
810
- *
811
- * Для FieldNode: запускаем валидацию
812
- */
813
- onEnable() {
814
- this.validate();
815
- }
816
- /**
817
- * Обновляет свойства компонента (например, опции для Select)
818
- *
819
- * @example
820
- * ```typescript
821
- * // Обновление опций для Select после загрузки справочников
822
- * form.registrationAddress.city.updateComponentProps({
823
- * options: cities
824
- * });
825
- * ```
826
- */
827
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
828
- updateComponentProps(t) {
829
- this._componentProps.value = {
830
- ...this._componentProps.value,
831
- ...t
832
- };
833
- }
834
- /**
835
- * Динамически изменяет триггер валидации (updateOn)
836
- * Полезно для адаптивной валидации - например, переключиться на instant feedback после первого submit
837
- *
838
- * @param updateOn - новый триггер валидации: 'change' | 'blur' | 'submit'
839
- *
840
- * @example
841
- * ```typescript
842
- * // Сценарий 1: Instant feedback после submit
843
- * const form = new GroupNode({
844
- * email: {
845
- * value: '',
846
- * component: Input,
847
- * updateOn: 'submit', // Изначально валидация только при submit
848
- * validators: [required, email]
849
- * }
850
- * });
851
- *
852
- * await form.submit(async (values) => {
853
- * // После submit переключаем на instant feedback
854
- * form.email.setUpdateOn('change');
855
- * await api.save(values);
856
- * });
857
- *
858
- * // Теперь валидация происходит при каждом изменении
859
- *
860
- * // Сценарий 2: Прогрессивное улучшение
861
- * form.email.setUpdateOn('blur'); // Сначала только при blur
862
- * // ... пользователь начинает вводить ...
863
- * form.email.setUpdateOn('change'); // Переключаем на change для real-time feedback
864
- * ```
865
- */
866
- setUpdateOn(t) {
867
- this.updateOn = t;
868
- }
869
- getUpdateOn() {
870
- return this.updateOn;
871
- }
872
- // ============================================================================
873
- // Методы-помощники для реактивности (Фаза 1)
874
- // ============================================================================
875
- /**
876
- * Подписка на изменения значения поля
877
- * Автоматически отслеживает изменения через @preact/signals effect
878
- *
879
- * @param callback - Функция, вызываемая при изменении значения
880
- * @returns Функция отписки для cleanup
881
- *
882
- * @example
883
- * ```typescript
884
- * const unsubscribe = form.email.watch((value) => {
885
- * console.log('Email changed:', value);
886
- * });
887
- *
888
- * // Cleanup
889
- * useEffect(() => unsubscribe, []);
890
- * ```
891
- */
892
- watch(t) {
893
- const e = c(() => {
894
- const s = this.value.value;
895
- t(s);
896
- }), i = `watch-${Date.now()}-${Math.random()}`;
897
- return this.disposers.add(i, e);
898
- }
899
- /**
900
- * Вычисляемое значение из других полей
901
- * Автоматически обновляет текущее поле при изменении источников
902
- *
903
- * @param sources - Массив ReadonlySignal для отслеживания
904
- * @param computeFn - Функция вычисления нового значения
905
- * @returns Функция отписки для cleanup
906
- *
907
- * @example
908
- * ```typescript
909
- * // Автоматический расчет первоначального взноса (20% от стоимости)
910
- * const dispose = form.initialPayment.computeFrom(
911
- * [form.propertyValue.value],
912
- * (propertyValue) => {
913
- * return propertyValue ? propertyValue * 0.2 : null;
914
- * }
915
- * );
916
- *
917
- * // Cleanup
918
- * useEffect(() => dispose, []);
919
- * ```
920
- */
921
- computeFrom(t, e) {
922
- const i = c(() => {
923
- const a = t.map((o) => o.value), n = e(...a);
924
- this.setValue(n, { emitEvent: !1 });
925
- }), s = `computeFrom-${Date.now()}-${Math.random()}`;
926
- return this.disposers.add(s, i);
927
- }
928
- /**
929
- * Очистить все ресурсы и таймеры
930
- * Должен вызываться при unmount компонента
931
- *
932
- * @example
933
- * ```typescript
934
- * useEffect(() => {
935
- * return () => {
936
- * field.dispose();
937
- * };
938
- * }, []);
939
- * ```
940
- */
941
- dispose() {
942
- this.disposers.dispose(), this.validateDebounceTimer && (clearTimeout(this.validateDebounceTimer), this.validateDebounceTimer = void 0);
943
- }
944
- }
945
- function R() {
946
- return E("");
947
- }
948
- function E(r) {
949
- return new Proxy({}, {
950
- get(t, e) {
951
- if (typeof e == "symbol")
952
- return;
953
- if (e === "__path")
954
- return r || e;
955
- if (e === "__key") {
956
- const a = r.split(".");
957
- return a[a.length - 1] || e;
958
- }
959
- if (e === "then" || e === "catch" || e === "finally" || e === "constructor" || e === "toString" || e === "valueOf" || e === "toJSON")
960
- return;
961
- const i = r ? `${r}.${e}` : e, s = {
962
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
963
- __key: e,
964
- __path: i,
965
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
966
- __formType: void 0,
967
- __fieldType: void 0
968
- };
969
- return new Proxy(s, {
970
- get(a, n) {
971
- if (typeof n != "symbol") {
972
- if (n === "__path") return i;
973
- if (n === "__key") return e;
974
- if (n !== "__formType" && n !== "__fieldType" && !(n === "then" || n === "catch" || n === "finally" || n === "constructor" || n === "toString" || n === "valueOf" || n === "toJSON"))
975
- return E(`${i}.${n}`);
976
- }
977
- }
978
- });
979
- }
980
- });
981
- }
982
- function M(r) {
983
- if (typeof r == "string")
984
- return r;
985
- if (r && typeof r == "object") {
986
- const t = r.__path;
987
- if (typeof t == "string")
988
- return t;
989
- }
990
- throw new Error("Invalid field path node: " + JSON.stringify(r));
991
- }
992
- function K(r) {
993
- const t = M(r);
994
- return E(t);
995
- }
996
- function z(r) {
997
- if (r && typeof r == "object" && "__key" in r)
998
- return r.__key;
999
- if (typeof r == "string") {
1000
- const t = r.split(".");
1001
- return t[t.length - 1];
1002
- }
1003
- throw new Error("Invalid field path node");
1004
- }
1005
- class m extends b {
1006
- // ============================================================================
1007
- // Приватные поля
1008
- // ============================================================================
1009
- items;
1010
- itemSchema;
1011
- initialItems;
1012
- /**
1013
- * Менеджер подписок для централизованного cleanup
1014
- * Использует SubscriptionManager вместо массива для управления подписками
1015
- */
1016
- disposers = new V();
1017
- // ============================================================================
1018
- // Приватные поля для сохранения схем
1019
- // ============================================================================
1020
- validationSchemaFn;
1021
- behaviorSchemaFn;
1022
- // ============================================================================
1023
- // Публичные computed signals
1024
- // ============================================================================
1025
- value;
1026
- valid;
1027
- invalid;
1028
- touched;
1029
- dirty;
1030
- pending;
1031
- errors;
1032
- status;
1033
- length;
1034
- // ============================================================================
1035
- // Конструктор
1036
- // ============================================================================
1037
- constructor(t, e = []) {
1038
- super(), this.itemSchema = t, this.initialItems = e, this.items = d([]);
1039
- for (const i of e)
1040
- this.push(i);
1041
- this.length = l(() => this.items.value.length), this.value = l(() => this.items.value.map((i) => i.value.value)), this.valid = l(() => this.items.value.every((i) => i.valid.value)), this.invalid = l(() => !this.valid.value), this.pending = l(() => this.items.value.some((i) => i.pending.value)), this.touched = l(() => this.items.value.some((i) => i.touched.value)), this.dirty = l(() => this.items.value.some((i) => i.dirty.value)), this.errors = l(() => {
1042
- const i = [];
1043
- return this.items.value.forEach((s) => {
1044
- i.push(...s.errors.value);
1045
- }), i;
1046
- }), this.status = l(() => this.pending.value ? "pending" : this.invalid.value ? "invalid" : "valid");
1047
- }
1048
- // ============================================================================
1049
- // CRUD операции
1050
- // ============================================================================
1051
- /**
1052
- * Добавить элемент в конец массива
1053
- * @param initialValue - Начальные значения для нового элемента
1054
- */
1055
- push(t) {
1056
- const e = this.createItem(t);
1057
- this.items.value = [...this.items.value, e];
1058
- }
1059
- /**
1060
- * Удалить элемент по индексу
1061
- * @param index - Индекс элемента для удаления
1062
- */
1063
- removeAt(t) {
1064
- t < 0 || t >= this.items.value.length || (this.items.value = this.items.value.filter((e, i) => i !== t));
1065
- }
1066
- /**
1067
- * Вставить элемент в массив
1068
- * @param index - Индекс для вставки
1069
- * @param initialValue - Начальные значения для нового элемента
1070
- */
1071
- insert(t, e) {
1072
- if (t < 0 || t > this.items.value.length)
1073
- return;
1074
- const i = this.createItem(e), s = [...this.items.value];
1075
- s.splice(t, 0, i), this.items.value = s;
1076
- }
1077
- /**
1078
- * Удалить все элементы массива
1079
- */
1080
- clear() {
1081
- this.items.value = [];
1082
- }
1083
- /**
1084
- * Получить элемент по индексу
1085
- * @param index - Индекс элемента
1086
- * @returns Типизированный GroupNode или undefined если индекс вне границ
1087
- */
1088
- at(t) {
1089
- return this.items.value[t];
1090
- }
1091
- // ============================================================================
1092
- // Реализация абстрактных методов
1093
- // ============================================================================
1094
- getValue() {
1095
- return this.items.value.map((t) => t.getValue());
1096
- }
1097
- setValue(t, e) {
1098
- this.clear(), t.forEach((i) => this.push(i)), e?.emitEvent !== !1 && this.validate().catch(() => {
1099
- });
1100
- }
1101
- patchValue(t) {
1102
- t.forEach((e, i) => {
1103
- this.items.value[i] && e !== void 0 && this.items.value[i].patchValue(e);
1104
- });
1105
- }
1106
- /**
1107
- * Сбросить массив к указанным значениям (или очистить)
1108
- *
1109
- * @param values - опциональный массив значений для сброса
1110
- *
1111
- * @remarks
1112
- * Очищает текущий массив и заполняет новыми элементами
1113
- *
1114
- * @example
1115
- * ```typescript
1116
- * // Очистить массив
1117
- * arrayNode.reset();
1118
- *
1119
- * // Сбросить к новым значениям
1120
- * arrayNode.reset([{ name: 'Item 1' }, { name: 'Item 2' }]);
1121
- * ```
1122
- */
1123
- reset(t) {
1124
- this.clear(), t && t.forEach((e) => this.push(e));
1125
- }
1126
- /**
1127
- * Сбросить массив к исходным значениям (initialItems)
1128
- *
1129
- * @remarks
1130
- * Восстанавливает массив в состояние, которое было при создании ArrayNode.
1131
- * Более явный способ сброса к начальным значениям по сравнению с reset()
1132
- *
1133
- * Полезно когда:
1134
- * - Пользователь нажал "Cancel" - вернуть массив к исходным элементам
1135
- * - Массив был изменен через reset(newValues), но нужно вернуться к началу
1136
- * - Явное намерение показать "отмена всех изменений"
1137
- *
1138
- * @example
1139
- * ```typescript
1140
- * const arrayNode = new ArrayNode(
1141
- * { name: { value: '', component: Input } },
1142
- * [{ name: 'Initial 1' }, { name: 'Initial 2' }]
1143
- * );
1144
- *
1145
- * arrayNode.push({ name: 'New Item' });
1146
- * arrayNode.reset([{ name: 'Temp' }]);
1147
- * console.log(arrayNode.length.value); // 1
1148
- *
1149
- * arrayNode.resetToInitial();
1150
- * console.log(arrayNode.length.value); // 2
1151
- * console.log(arrayNode.at(0)?.name.value.value); // 'Initial 1'
1152
- * ```
1153
- */
1154
- resetToInitial() {
1155
- this.clear(), this.initialItems.forEach((t) => this.push(t));
1156
- }
1157
- async validate() {
1158
- return (await Promise.all(this.items.value.map((e) => e.validate()))).every(Boolean);
1159
- }
1160
- setErrors(t) {
1161
- }
1162
- clearErrors() {
1163
- this.items.value.forEach((t) => t.clearErrors());
1164
- }
1165
- // ============================================================================
1166
- // Protected hooks (Template Method pattern)
1167
- // ============================================================================
1168
- /**
1169
- * Hook: вызывается после markAsTouched()
1170
- *
1171
- * Для ArrayNode: рекурсивно помечаем все элементы массива как touched
1172
- */
1173
- onMarkAsTouched() {
1174
- this.items.value.forEach((t) => t.markAsTouched());
1175
- }
1176
- /**
1177
- * Hook: вызывается после markAsUntouched()
1178
- *
1179
- * Для ArrayNode: рекурсивно помечаем все элементы массива как untouched
1180
- */
1181
- onMarkAsUntouched() {
1182
- this.items.value.forEach((t) => t.markAsUntouched());
1183
- }
1184
- /**
1185
- * Hook: вызывается после markAsDirty()
1186
- *
1187
- * Для ArrayNode: рекурсивно помечаем все элементы массива как dirty
1188
- */
1189
- onMarkAsDirty() {
1190
- this.items.value.forEach((t) => t.markAsDirty());
1191
- }
1192
- /**
1193
- * Hook: вызывается после markAsPristine()
1194
- *
1195
- * Для ArrayNode: рекурсивно помечаем все элементы массива как pristine
1196
- */
1197
- onMarkAsPristine() {
1198
- this.items.value.forEach((t) => t.markAsPristine());
1199
- }
1200
- // ============================================================================
1201
- // Итерация
1202
- // ============================================================================
1203
- /**
1204
- * Итерировать по элементам массива
1205
- * @param callback - Функция, вызываемая для каждого элемента с типизированным GroupNode
1206
- */
1207
- forEach(t) {
1208
- this.items.value.forEach((e, i) => {
1209
- t(e, i);
1210
- });
1211
- }
1212
- /**
1213
- * Маппинг элементов массива
1214
- * @param callback - Функция преобразования с типизированным GroupNode
1215
- * @returns Новый массив результатов
1216
- */
1217
- map(t) {
1218
- return this.items.value.map((e, i) => t(e, i));
1219
- }
1220
- // ============================================================================
1221
- // Private методы
1222
- // ============================================================================
1223
- /**
1224
- * Создать новый элемент массива на основе схемы
1225
- * @param initialValue - Начальные значения
1226
- */
1227
- createItem(t) {
1228
- if (this.isGroupSchema(this.itemSchema)) {
1229
- const e = new f(this.itemSchema);
1230
- return t && e.patchValue(t), this.validationSchemaFn && "applyValidationSchema" in e && e.applyValidationSchema(this.validationSchemaFn), this.behaviorSchemaFn && "applyBehaviorSchema" in e && e.applyBehaviorSchema(this.behaviorSchemaFn), e;
1231
- }
1232
- throw new Error(
1233
- "ArrayNode поддерживает только GroupNode элементы. Для массива примитивов используйте обычное поле с типом массива."
1234
- );
1235
- }
1236
- /**
1237
- * Проверить, является ли схема групповой (объект полей)
1238
- * @param schema - Схема для проверки
1239
- */
1240
- isGroupSchema(t) {
1241
- return typeof t == "object" && t !== null && !("component" in t) && !Array.isArray(t);
1242
- }
1243
- // ============================================================================
1244
- // Validation Schema
1245
- // ============================================================================
1246
- /**
1247
- * Применить validation schema ко всем элементам массива
1248
- *
1249
- * Validation schema будет применена к:
1250
- * - Всем существующим элементам
1251
- * - Всем новым элементам, добавляемым через push/insert
1252
- *
1253
- * @param schemaFn - Функция валидации для элемента массива
1254
- *
1255
- * @example
1256
- * ```typescript
1257
- * import { propertyValidation } from './validation/property-validation';
1258
- *
1259
- * form.properties.applyValidationSchema(propertyValidation);
1260
- * ```
1261
- */
1262
- applyValidationSchema(t) {
1263
- this.validationSchemaFn = t, this.items.value.forEach((e) => {
1264
- "applyValidationSchema" in e && typeof e.applyValidationSchema == "function" && e.applyValidationSchema(t);
1265
- });
1266
- }
1267
- /**
1268
- * Применить behavior schema ко всем элементам ArrayNode
1269
- *
1270
- * Автоматически применяется к новым элементам при push/insert.
1271
- *
1272
- * @param schemaFn - Behavior schema функция
1273
- *
1274
- * @example
1275
- * ```typescript
1276
- * import { addressBehavior } from './behaviors/address-behavior';
1277
- *
1278
- * form.addresses.applyBehaviorSchema(addressBehavior);
1279
- * ```
1280
- */
1281
- applyBehaviorSchema(t) {
1282
- this.behaviorSchemaFn = t, this.items.value.forEach((e) => {
1283
- "applyBehaviorSchema" in e && typeof e.applyBehaviorSchema == "function" && e.applyBehaviorSchema(t);
1284
- });
1285
- }
1286
- // ============================================================================
1287
- // Методы-помощники для реактивности (Фаза 1)
1288
- // ============================================================================
1289
- /**
1290
- * Подписка на изменения конкретного поля во всех элементах массива
1291
- * Срабатывает при изменении значения поля в любом элементе
1292
- *
1293
- * @param fieldKey - Ключ поля для отслеживания
1294
- * @param callback - Функция, вызываемая при изменении, получает массив всех значений и индекс измененного элемента
1295
- * @returns Функция отписки для cleanup
1296
- *
1297
- * @example
1298
- * ```typescript
1299
- * // Автоматический пересчет общей стоимости при изменении цен
1300
- * const dispose = form.existingLoans.watchItems(
1301
- * 'remainingAmount',
1302
- * (amounts) => {
1303
- * const totalDebt = amounts.reduce((sum, amount) => sum + (amount || 0), 0);
1304
- * form.totalDebt.setValue(totalDebt);
1305
- * }
1306
- * );
1307
- *
1308
- * // При изменении любого remainingAmount → пересчитается totalDebt
1309
- * form.existingLoans.at(0)?.remainingAmount.setValue(500000);
1310
- *
1311
- * // Cleanup
1312
- * useEffect(() => dispose, []);
1313
- * ```
1314
- */
1315
- watchItems(t, e) {
1316
- const i = c(() => {
1317
- const a = this.items.value.map((n) => {
1318
- if (n instanceof f)
1319
- return n.getFieldByPath(t)?.value.value;
1320
- });
1321
- e(a);
1322
- }), s = `watchItems-${Date.now()}-${Math.random()}`;
1323
- return this.disposers.add(s, i);
1324
- }
1325
- /**
1326
- * Подписка на изменение длины массива
1327
- * Срабатывает при добавлении/удалении элементов
1328
- *
1329
- * @param callback - Функция, вызываемая при изменении длины, получает новую длину
1330
- * @returns Функция отписки для cleanup
1331
- *
1332
- * @example
1333
- * ```typescript
1334
- * // Обновление счетчика элементов в UI
1335
- * const dispose = form.properties.watchLength((length) => {
1336
- * console.log(`Количество объектов недвижимости: ${length}`);
1337
- * form.propertyCount.setValue(length);
1338
- * });
1339
- *
1340
- * form.properties.push({ title: 'Квартира', value: 5000000 });
1341
- * // Выведет: "Количество объектов недвижимости: 1"
1342
- *
1343
- * // Cleanup
1344
- * useEffect(() => dispose, []);
1345
- * ```
1346
- */
1347
- watchLength(t) {
1348
- const e = c(() => {
1349
- const s = this.length.value;
1350
- t(s);
1351
- }), i = `watchLength-${Date.now()}-${Math.random()}`;
1352
- return this.disposers.add(i, e);
1353
- }
1354
- /**
1355
- * Очистить все ресурсы узла
1356
- * Рекурсивно очищает все subscriptions и элементы массива
1357
- *
1358
- * @example
1359
- * ```typescript
1360
- * useEffect(() => {
1361
- * return () => {
1362
- * arrayNode.dispose();
1363
- * };
1364
- * }, []);
1365
- * ```
1366
- */
1367
- dispose() {
1368
- this.disposers.dispose(), this.items.value.forEach((t) => {
1369
- "dispose" in t && typeof t.dispose == "function" && t.dispose();
1370
- });
1371
- }
1372
- /**
1373
- * Hook: вызывается после disable()
1374
- *
1375
- * Для ArrayNode: рекурсивно отключаем все элементы массива
1376
- *
1377
- * @example
1378
- * ```typescript
1379
- * // Отключить весь массив полей
1380
- * form.items.disable();
1381
- *
1382
- * // Все элементы становятся disabled
1383
- * form.items.forEach(item => {
1384
- * console.log(item.status.value); // 'disabled'
1385
- * });
1386
- * ```
1387
- */
1388
- onDisable() {
1389
- this.items.value.forEach((t) => {
1390
- t.disable();
1391
- });
1392
- }
1393
- /**
1394
- * Hook: вызывается после enable()
1395
- *
1396
- * Для ArrayNode: рекурсивно включаем все элементы массива
1397
- *
1398
- * @example
1399
- * ```typescript
1400
- * // Включить весь массив полей
1401
- * form.items.enable();
1402
- *
1403
- * // Все элементы становятся enabled
1404
- * form.items.forEach(item => {
1405
- * console.log(item.status.value); // 'valid' или 'invalid'
1406
- * });
1407
- * ```
1408
- */
1409
- onEnable() {
1410
- this.items.value.forEach((t) => {
1411
- t.enable();
1412
- });
1413
- }
1414
- }
1415
- function g(r) {
1416
- return r instanceof F ? [r] : r instanceof f ? Array.from(r.getAllFields()).flatMap(g) : r instanceof m ? r.map((t) => g(t)).flat() : [];
1417
- }
1418
- async function J(r, t) {
1419
- const e = new A();
1420
- e.beginRegistration();
1421
- let i = [], s = !1;
1422
- try {
1423
- const a = R();
1424
- t(a), i = e.getCurrentContext()?.getValidators() || [], e.cancelRegistration(), s = !0, r.clearErrors();
1425
- const o = g(r);
1426
- return await Promise.all(o.map((h) => h.validate())), i.length > 0 && await r.applyContextualValidators(i), r.valid.value;
1427
- } catch (a) {
1428
- throw s || e.cancelRegistration(), a;
1429
- }
1430
- }
1431
- function v(r) {
1432
- return r == null ? !1 : typeof r == "object" && "value" in r && "setValue" in r && "getValue" in r && "validate" in r;
1433
- }
1434
- function _(r) {
1435
- return r == null ? !1 : v(r) && "validators" in r && "asyncValidators" in r && // FieldNode имеет markAsTouched метод
1436
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1437
- typeof r.markAsTouched == "function" && // У FieldNode нет fields или items
1438
- !("fields" in r) && !("items" in r);
1439
- }
1440
- function P(r) {
1441
- return r == null ? !1 : v(r) && "applyValidationSchema" in r && "applyBehaviorSchema" in r && "getFieldByPath" in r && // GroupNode НЕ имеет items/push/removeAt (это ArrayNode)
1442
- !("items" in r) && !("push" in r) && !("removeAt" in r);
1443
- }
1444
- function N(r) {
1445
- return r == null ? !1 : v(r) && "items" in r && "length" in r && "push" in r && "removeAt" in r && "at" in r && // eslint-disable-next-line @typescript-eslint/no-explicit-any
1446
- typeof r.push == "function" && // eslint-disable-next-line @typescript-eslint/no-explicit-any
1447
- typeof r.removeAt == "function";
1448
- }
1449
- function W(r) {
1450
- return _(r) ? "FieldNode" : P(r) ? "GroupNode" : N(r) ? "ArrayNode" : v(r) ? "FormNode" : "Unknown";
1451
- }
1452
- class I {
1453
- _form;
1454
- control;
1455
- /**
1456
- * Форма с типизированным Proxy-доступом к полям
1457
- */
1458
- form;
1459
- constructor(t, e, i) {
1460
- this._form = t, this.control = i, this.form = t._proxyInstance || t.getProxy();
1461
- }
1462
- /**
1463
- * Получить текущее значение поля (внутренний метод для validation-applicator)
1464
- * @internal
1465
- */
1466
- value() {
1467
- return this.control.value.value;
1468
- }
1469
- /**
1470
- * Безопасно установить значение поля по строковому пути
1471
- * Автоматически использует emitEvent: false для предотвращения циклов
1472
- */
1473
- setFieldValue(t, e) {
1474
- const i = this._form.getFieldByPath(t);
1475
- i && v(i) && i.setValue(e, { emitEvent: !1 });
1476
- }
1477
- }
1478
- class T {
1479
- _form;
1480
- /**
1481
- * Форма с типизированным Proxy-доступом к полям
1482
- */
1483
- form;
1484
- constructor(t) {
1485
- this._form = t, this.form = t._proxyInstance || t.getProxy();
1486
- }
1487
- /**
1488
- * Безопасно установить значение поля по строковому пути
1489
- * Автоматически использует emitEvent: false для предотвращения циклов
1490
- */
1491
- setFieldValue(t, e) {
1492
- const i = this._form.getFieldByPath(t);
1493
- i && v(i) && i.setValue(e, { emitEvent: !1 });
1494
- }
1495
- }
1496
- class D {
1497
- form;
1498
- constructor(t) {
1499
- this.form = t;
1500
- }
1501
- /**
1502
- * Применить валидаторы к полям формы
1503
- *
1504
- * Этапы применения:
1505
- * 1. Разделение валидаторов на field и tree
1506
- * 2. Применение field валидаторов (sync/async)
1507
- * 3. Применение tree валидаторов (кросс-полевая валидация)
1508
- *
1509
- * @param validators Зарегистрированные валидаторы
1510
- */
1511
- async apply(t) {
1512
- const { validatorsByField: e, treeValidators: i } = this.groupValidators(t);
1513
- await this.applyFieldValidators(e), this.applyTreeValidators(i);
1514
- }
1515
- /**
1516
- * Группировка валидаторов по типам
1517
- *
1518
- * Разделяет валидаторы на:
1519
- * - Field validators (sync/async) - группируются по fieldPath
1520
- * - Tree validators - применяются ко всей форме
1521
- *
1522
- * @param validators Все зарегистрированные валидаторы
1523
- * @returns Сгруппированные валидаторы
1524
- */
1525
- groupValidators(t) {
1526
- const e = /* @__PURE__ */ new Map(), i = [];
1527
- for (const s of t)
1528
- if (s.type === "tree")
1529
- i.push(s);
1530
- else {
1531
- const a = e.get(s.fieldPath) || [];
1532
- a.push(s), e.set(s.fieldPath, a);
1533
- }
1534
- return { validatorsByField: e, treeValidators: i };
1535
- }
1536
- /**
1537
- * Применение field валидаторов к полям
1538
- *
1539
- * Для каждого поля:
1540
- * 1. Найти FieldNode по пути
1541
- * 2. Проверить условия (condition)
1542
- * 3. Выполнить sync/async валидаторы
1543
- * 4. Установить ошибки на поле
1544
- *
1545
- * @param validatorsByField Валидаторы, сгруппированные по полям
1546
- */
1547
- async applyFieldValidators(t) {
1548
- for (const [e, i] of t) {
1549
- const s = this.form.getFieldByPath(e);
1550
- if (!s) {
1551
- console.warn(`Field ${e} not found in GroupNode`);
1552
- continue;
1553
- }
1554
- if (!_(s)) {
1555
- process.env.NODE_ENV !== "production" && console.warn(`Validation can only run on FieldNode, skipping ${e}`);
1556
- continue;
1557
- }
1558
- const a = [], n = new I(this.form, e, s);
1559
- for (const o of i)
1560
- if (!(o.condition && !this.checkCondition(o.condition)))
1561
- try {
1562
- let h = null;
1563
- const u = n.value(), w = o.validator;
1564
- o.type === "sync" ? h = w(u, n) : o.type === "async" && (h = await w(u, n)), h && a.push(h);
1565
- } catch (h) {
1566
- p.handle(
1567
- h,
1568
- `ValidationApplicator: validator for ${e}`,
1569
- y.LOG
1570
- );
1571
- }
1572
- a.length > 0 ? s.setErrors(a) : s.errors.value.length > 0 && !s.errors.value.some((o) => o.code !== "contextual") && s.clearErrors();
1573
- }
1574
- }
1575
- /**
1576
- * Применение tree валидаторов (кросс-полевая валидация)
1577
- *
1578
- * Tree валидаторы имеют доступ ко всей форме через TreeValidationContext.
1579
- * Ошибки устанавливаются на targetField (если указано).
1580
- *
1581
- * @param treeValidators Список tree валидаторов
1582
- */
1583
- applyTreeValidators(t) {
1584
- for (const e of t) {
1585
- const i = new T(this.form);
1586
- if (!(e.condition && !this.checkCondition(e.condition)))
1587
- try {
1588
- if (e.type !== "tree")
1589
- continue;
1590
- const s = e.validator(i);
1591
- if (s && e.options && "targetField" in e.options) {
1592
- const a = e.options.targetField;
1593
- if (a) {
1594
- const n = this.form.getFieldByPath(String(a));
1595
- if (n && _(n)) {
1596
- const o = n.errors.value;
1597
- n.setErrors([...o, s]);
1598
- }
1599
- }
1600
- }
1601
- } catch (s) {
1602
- p.handle(s, "ValidationApplicator: tree validator", y.LOG);
1603
- }
1604
- }
1605
- }
1606
- /**
1607
- * Проверка условия (condition) для валидатора
1608
- *
1609
- * Условие определяет, должен ли валидатор выполняться.
1610
- * Использует getFieldByPath для поддержки вложенных путей.
1611
- *
1612
- * @param condition Условие валидатора
1613
- * @returns true, если условие выполнено
1614
- */
1615
- checkCondition(t) {
1616
- const e = this.form.getFieldByPath(t.fieldPath);
1617
- if (!e)
1618
- return !1;
1619
- const i = e.value.value;
1620
- return t.conditionFn(i);
1621
- }
1622
- }
1623
- class B {
1624
- form;
1625
- behaviorRegistry;
1626
- constructor(t, e) {
1627
- this.form = t, this.behaviorRegistry = e;
1628
- }
1629
- /**
1630
- * Применить behavior схему к форме
1631
- *
1632
- * Этапы:
1633
- * 1. Начать регистрацию (beginRegistration)
1634
- * 2. Выполнить схему (регистрация behaviors)
1635
- * 3. Завершить регистрацию (endRegistration) - применить behaviors
1636
- * 4. Вернуть функцию cleanup для отписки
1637
- *
1638
- * @param schemaFn Функция-схема behavior
1639
- * @returns Функция отписки от всех behaviors
1640
- *
1641
- * @example
1642
- * ```typescript
1643
- * const cleanup = behaviorApplicator.apply((path) => {
1644
- * copyFrom(path.residenceAddress, path.registrationAddress, {
1645
- * when: (form) => form.sameAsRegistration === true
1646
- * });
1647
- *
1648
- * enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage');
1649
- *
1650
- * computeFrom(
1651
- * path.initialPayment,
1652
- * [path.propertyValue],
1653
- * (propertyValue) => propertyValue ? propertyValue * 0.2 : null
1654
- * );
1655
- * });
1656
- *
1657
- * // Cleanup при unmount
1658
- * useEffect(() => cleanup, []);
1659
- * ```
1660
- */
1661
- apply(t) {
1662
- this.behaviorRegistry.beginRegistration();
1663
- try {
1664
- const e = k();
1665
- t(e);
1666
- const i = this.form.getProxy();
1667
- return this.behaviorRegistry.endRegistration(i).cleanup;
1668
- } catch (e) {
1669
- throw p.handle(e, "BehaviorApplicator", y.THROW), e;
1670
- }
1671
- }
1672
- }
1673
- class C {
1674
- /**
1675
- * Парсит путь в массив сегментов
1676
- *
1677
- * Поддерживаемые форматы:
1678
- * - Простые пути: "name", "email"
1679
- * - Вложенные пути: "address.city", "user.profile.avatar"
1680
- * - Массивы: "items[0]", "items[0].name", "tags[1][0]"
1681
- * - Комбинации: "orders[0].items[1].price"
1682
- *
1683
- * @param path Путь к полю (строка с точками и квадратными скобками)
1684
- * @returns Массив сегментов пути
1685
- *
1686
- * @example
1687
- * ```typescript
1688
- * navigator.parsePath('email');
1689
- * // [{ key: 'email' }]
1690
- *
1691
- * navigator.parsePath('address.city');
1692
- * // [{ key: 'address' }, { key: 'city' }]
1693
- *
1694
- * navigator.parsePath('items[0].name');
1695
- * // [{ key: 'items', index: 0 }, { key: 'name' }]
1696
- * ```
1697
- */
1698
- parsePath(t) {
1699
- const e = [];
1700
- let i = "", s = !1;
1701
- for (let a = 0; a < t.length; a++) {
1702
- const n = t[a];
1703
- n === "[" ? (s = !0, i += n) : n === "]" ? (s = !1, i += n) : n === "." && !s ? i && (this.addSegment(e, i), i = "") : i += n;
1704
- }
1705
- return i && this.addSegment(e, i), e;
1706
- }
1707
- /**
1708
- * Добавляет сегмент в массив, обрабатывая массивы
1709
- * @private
1710
- */
1711
- addSegment(t, e) {
1712
- const i = e.match(/^(.+)\[(\d+)\]$/);
1713
- i ? t.push({
1714
- key: i[1],
1715
- index: parseInt(i[2], 10)
1716
- }) : t.push({ key: e });
1717
- }
1718
- /**
1719
- * Получает значение по пути из объекта
1720
- *
1721
- * Проходит по всем сегментам пути и возвращает конечное значение.
1722
- * Если путь не найден, возвращает undefined.
1723
- *
1724
- * @param obj Объект для навигации
1725
- * @param path Путь к значению
1726
- * @returns Значение или undefined, если путь не найден
1727
- *
1728
- * @example
1729
- * ```typescript
1730
- * const obj = {
1731
- * email: 'test@mail.com',
1732
- * address: { city: 'Moscow' },
1733
- * items: [{ title: 'Item 1' }]
1734
- * };
1735
- *
1736
- * navigator.getValueByPath(obj, 'email');
1737
- * // 'test@mail.com'
1738
- *
1739
- * navigator.getValueByPath(obj, 'address.city');
1740
- * // 'Moscow'
1741
- *
1742
- * navigator.getValueByPath(obj, 'items[0].title');
1743
- * // 'Item 1'
1744
- *
1745
- * navigator.getValueByPath(obj, 'invalid.path');
1746
- * // undefined
1747
- * ```
1748
- */
1749
- getValueByPath(t, e) {
1750
- const i = this.parsePath(e);
1751
- let s = t;
1752
- for (const a of i) {
1753
- if (s == null) return;
1754
- if (s = s[a.key], a.index !== void 0) {
1755
- if (!Array.isArray(s)) return;
1756
- s = s[a.index];
1757
- }
1758
- }
1759
- return s;
1760
- }
1761
- /**
1762
- * Устанавливает значение по пути в объекте (мутирует объект)
1763
- *
1764
- * Создает промежуточные объекты, если они не существуют.
1765
- * Выбрасывает ошибку, если ожидается массив, но его нет.
1766
- *
1767
- * @param obj Объект для модификации
1768
- * @param path Путь к значению
1769
- * @param value Новое значение
1770
- *
1771
- * @throws {Error} Если ожидается массив по пути, но его нет
1772
- *
1773
- * @example
1774
- * ```typescript
1775
- * const obj = { address: { city: '' } };
1776
- * navigator.setValueByPath(obj, 'address.city', 'Moscow');
1777
- * // obj.address.city === 'Moscow'
1778
- *
1779
- * const obj2: UnknownRecord = {};
1780
- * navigator.setValueByPath(obj2, 'address.city', 'Moscow');
1781
- * // Создаст { address: { city: 'Moscow' } }
1782
- *
1783
- * const obj3 = { items: [{ title: 'Old' }] };
1784
- * navigator.setValueByPath(obj3, 'items[0].title', 'New');
1785
- * // obj3.items[0].title === 'New'
1786
- * ```
1787
- */
1788
- setValueByPath(t, e, i) {
1789
- const s = this.parsePath(e);
1790
- if (s.length === 0)
1791
- throw new Error("Cannot set value: empty path");
1792
- let a = t;
1793
- for (let o = 0; o < s.length - 1; o++) {
1794
- const h = s[o];
1795
- let u = a[h.key];
1796
- if (h.index !== void 0) {
1797
- if (!Array.isArray(u))
1798
- throw new Error(`Expected array at path segment: ${h.key}, but got ${typeof u}`);
1799
- a = u[h.index];
1800
- } else
1801
- u == null && (a[h.key] = {}, u = a[h.key]), a = u;
1802
- }
1803
- const n = s[s.length - 1];
1804
- if (n.index !== void 0) {
1805
- const o = a[n.key];
1806
- if (!Array.isArray(o))
1807
- throw new Error(
1808
- `Expected array at path segment: ${n.key}, but got ${typeof o}`
1809
- );
1810
- o[n.index] = i;
1811
- } else
1812
- a[n.key] = i;
1813
- }
1814
- /**
1815
- * Получить значение из FormNode по пути
1816
- *
1817
- * Автоматически извлекает значение из FormNode (через .value.value).
1818
- * Используется в ValidationContext и BehaviorContext для единообразного
1819
- * доступа к значениям полей формы.
1820
- *
1821
- * @param form Корневой узел формы (обычно GroupNode)
1822
- * @param path Путь к полю
1823
- * @returns Значение поля или undefined, если путь не найден
1824
- *
1825
- * @example
1826
- * ```typescript
1827
- * const form = new GroupNode({
1828
- * email: { value: 'test@mail.com', component: Input },
1829
- * address: {
1830
- * city: { value: 'Moscow', component: Input }
1831
- * },
1832
- * items: [{ title: { value: 'Item 1', component: Input } }]
1833
- * });
1834
- *
1835
- * navigator.getFormNodeValue(form, 'email');
1836
- * // 'test@mail.com'
1837
- *
1838
- * navigator.getFormNodeValue(form, 'address.city');
1839
- * // 'Moscow'
1840
- *
1841
- * navigator.getFormNodeValue(form, 'items[0].title');
1842
- * // 'Item 1'
1843
- *
1844
- * navigator.getFormNodeValue(form, 'invalid.path');
1845
- * // undefined
1846
- * ```
1847
- */
1848
- getFormNodeValue(t, e) {
1849
- const i = this.getNodeByPath(t, e);
1850
- if (i != null)
1851
- return this.isFormNode(i) ? i.value.value : i;
1852
- }
1853
- /**
1854
- * Type guard для проверки, является ли объект FormNode
1855
- *
1856
- * Проверяет наличие характерных свойств FormNode:
1857
- * - value (Signal)
1858
- * - value.value (значение Signal)
1859
- *
1860
- * @param obj Объект для проверки
1861
- * @returns true, если объект является FormNode
1862
- * @private
1863
- */
1864
- isFormNode(t) {
1865
- return t != null && typeof t == "object" && "value" in t && typeof t.value == "object" && t.value != null && "value" in t.value;
1866
- }
1867
- /**
1868
- * Получает узел формы по пути
1869
- *
1870
- * Навигирует по структуре FormNode (GroupNode/FieldNode/ArrayNode)
1871
- * и возвращает узел по указанному пути.
1872
- *
1873
- * Поддерживает:
1874
- * - Доступ к полям GroupNode через fields Map
1875
- * - Доступ к элементам ArrayNode через индекс
1876
- * - Proxy-доступ к полям (для обратной совместимости)
1877
- *
1878
- * @param form Корневой узел формы (обычно GroupNode)
1879
- * @param path Путь к узлу
1880
- * @returns Узел формы или null, если путь не найден
1881
- *
1882
- * @example
1883
- * ```typescript
1884
- * const form = new GroupNode({
1885
- * email: { value: '', component: Input },
1886
- * address: {
1887
- * city: { value: '', component: Input }
1888
- * },
1889
- * items: [{ title: { value: '', component: Input } }]
1890
- * });
1891
- *
1892
- * const emailNode = navigator.getNodeByPath(form, 'email');
1893
- * // FieldNode
1894
- *
1895
- * const cityNode = navigator.getNodeByPath(form, 'address.city');
1896
- * // FieldNode
1897
- *
1898
- * const itemNode = navigator.getNodeByPath(form, 'items[0]');
1899
- * // GroupNode
1900
- *
1901
- * const titleNode = navigator.getNodeByPath(form, 'items[0].title');
1902
- * // FieldNode
1903
- *
1904
- * const invalidNode = navigator.getNodeByPath(form, 'invalid.path');
1905
- * // null
1906
- * ```
1907
- */
1908
- getNodeByPath(t, e) {
1909
- const i = this.parsePath(e);
1910
- let s = t;
1911
- for (const a of i) {
1912
- if (s == null) return null;
1913
- const n = s;
1914
- if (n.fields && n.fields instanceof Map) {
1915
- if (s = n.fields.get(a.key), a.index === void 0) {
1916
- if (s == null) return null;
1917
- continue;
1918
- }
1919
- } else if (a.index !== void 0 && n.items) {
1920
- const o = n.items.value || n.items;
1921
- if (!Array.isArray(o) || (s = o[a.index], s == null)) return null;
1922
- continue;
1923
- } else if (a.index === void 0) {
1924
- if (s = n[a.key], s == null) return null;
1925
- continue;
1926
- }
1927
- if (s && a.index !== void 0 && s.items) {
1928
- const o = s.items.value || s.items;
1929
- if (!Array.isArray(o)) return null;
1930
- s = o[a.index];
1931
- } else if (s && a.index !== void 0 && !s.items)
1932
- return null;
1933
- if (s == null) return null;
1934
- }
1935
- return s;
1936
- }
1937
- }
1938
- class O {
1939
- /**
1940
- * Внутреннее хранилище полей
1941
- * Map обеспечивает быструю lookup производительность O(1)
1942
- */
1943
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1944
- fields = /* @__PURE__ */ new Map();
1945
- /**
1946
- * Установить поле в реестр
1947
- *
1948
- * @param key - Ключ поля (имя свойства в типе T)
1949
- * @param node - FormNode для этого поля
1950
- *
1951
- * @example
1952
- * ```typescript
1953
- * registry.set('email', new FieldNode({ value: '' }));
1954
- * ```
1955
- */
1956
- set(t, e) {
1957
- this.fields.set(t, e);
1958
- }
1959
- /**
1960
- * Получить поле из реестра
1961
- *
1962
- * @param key - Ключ поля
1963
- * @returns FormNode или undefined, если поле не найдено
1964
- *
1965
- * @example
1966
- * ```typescript
1967
- * const emailField = registry.get('email');
1968
- * if (emailField) {
1969
- * console.log(emailField.value.value);
1970
- * }
1971
- * ```
1972
- */
1973
- get(t) {
1974
- return this.fields.get(t);
1975
- }
1976
- /**
1977
- * Проверить наличие поля в реестре
1978
- *
1979
- * @param key - Ключ поля
1980
- * @returns true если поле существует
1981
- *
1982
- * @example
1983
- * ```typescript
1984
- * if (registry.has('email')) {
1985
- * console.log('Email field exists');
1986
- * }
1987
- * ```
1988
- */
1989
- has(t) {
1990
- return this.fields.has(t);
1991
- }
1992
- /**
1993
- * Удалить поле из реестра
1994
- *
1995
- * @param key - Ключ поля
1996
- * @returns true если поле было удалено, false если поля не было
1997
- *
1998
- * @example
1999
- * ```typescript
2000
- * registry.delete('email');
2001
- * ```
2002
- */
2003
- delete(t) {
2004
- return this.fields.delete(t);
2005
- }
2006
- /**
2007
- * Перебрать все поля
2008
- *
2009
- * @param callback - Функция обратного вызова для каждого поля
2010
- *
2011
- * @example
2012
- * ```typescript
2013
- * registry.forEach((field, key) => {
2014
- * console.log(`${key}: ${field.value.value}`);
2015
- * });
2016
- * ```
2017
- */
2018
- forEach(t) {
2019
- this.fields.forEach(t);
2020
- }
2021
- /**
2022
- * Получить итератор значений (полей)
2023
- *
2024
- * @returns Итератор по всем полям
2025
- *
2026
- * @example
2027
- * ```typescript
2028
- * for (const field of registry.values()) {
2029
- * await field.validate();
2030
- * }
2031
- * ```
2032
- */
2033
- values() {
2034
- return this.fields.values();
2035
- }
2036
- /**
2037
- * Получить итератор пар [ключ, значение]
2038
- *
2039
- * @returns Итератор по всем записям
2040
- *
2041
- * @example
2042
- * ```typescript
2043
- * for (const [key, field] of registry.entries()) {
2044
- * console.log(key, field.value.value);
2045
- * }
2046
- * ```
2047
- */
2048
- entries() {
2049
- return this.fields.entries();
2050
- }
2051
- /**
2052
- * Получить итератор ключей полей
2053
- *
2054
- * @returns Итератор по всем ключам
2055
- *
2056
- * @example
2057
- * ```typescript
2058
- * const fieldNames = Array.from(registry.keys());
2059
- * // ['email', 'name', 'age']
2060
- * ```
2061
- */
2062
- keys() {
2063
- return this.fields.keys();
2064
- }
2065
- /**
2066
- * Получить количество полей
2067
- *
2068
- * @returns Количество зарегистрированных полей
2069
- *
2070
- * @example
2071
- * ```typescript
2072
- * console.log(`Form has ${registry.size()} fields`);
2073
- * ```
2074
- */
2075
- size() {
2076
- return this.fields.size;
2077
- }
2078
- /**
2079
- * Очистить все поля
2080
- *
2081
- * Удаляет все поля из реестра
2082
- *
2083
- * @example
2084
- * ```typescript
2085
- * registry.clear();
2086
- * console.log(registry.size()); // 0
2087
- * ```
2088
- */
2089
- clear() {
2090
- this.fields.clear();
2091
- }
2092
- /**
2093
- * Получить все поля как массив
2094
- *
2095
- * Полезно для операций, требующих работу с массивом
2096
- *
2097
- * @returns Массив всех полей
2098
- *
2099
- * @example
2100
- * ```typescript
2101
- * const allValid = registry.toArray().every(field => field.valid.value);
2102
- * ```
2103
- */
2104
- toArray() {
2105
- return Array.from(this.fields.values());
2106
- }
2107
- /**
2108
- * Получить Map-представление реестра (readonly)
2109
- *
2110
- * Используйте для совместимости с существующим кодом
2111
- *
2112
- * @returns ReadonlyMap с полями
2113
- * @internal
2114
- *
2115
- * @example
2116
- * ```typescript
2117
- * const mapView = registry.asMap();
2118
- * ```
2119
- */
2120
- asMap() {
2121
- return this.fields;
2122
- }
2123
- }
2124
- class $ {
2125
- /**
2126
- * @param fieldRegistry - Реестр полей для доступа к коллекции
2127
- */
2128
- constructor(t) {
2129
- this.fieldRegistry = t;
2130
- }
2131
- /**
2132
- * Создать Proxy для GroupNode
2133
- *
2134
- * Proxy позволяет обращаться к полям формы напрямую:
2135
- * - form.email вместо form.fields.get('email')
2136
- * - form.address.city вместо form.fields.get('address').fields.get('city')
2137
- *
2138
- * @param target - GroupNode для которого создается Proxy
2139
- * @returns Proxy с типобезопасным доступом к полям
2140
- *
2141
- * @example
2142
- * ```typescript
2143
- * const proxy = proxyBuilder.build(groupNode);
2144
- *
2145
- * // Доступ к полям
2146
- * console.log(proxy.email.value); // Работает!
2147
- * console.log(proxy.name.value); // Работает!
2148
- *
2149
- * // Доступ к методам GroupNode
2150
- * await proxy.validate(); // Работает!
2151
- * proxy.markAsTouched(); // Работает!
2152
- *
2153
- * // Проверка существования
2154
- * if ('email' in proxy) { ... }
2155
- *
2156
- * // Перечисление ключей
2157
- * Object.keys(proxy); // ['email', 'name', ...]
2158
- * ```
2159
- */
2160
- build(t) {
2161
- return new Proxy(t, {
2162
- /**
2163
- * Get trap: Перехват доступа к свойствам
2164
- *
2165
- * Приоритет:
2166
- * 1. Собственные свойства и методы GroupNode (validate, setValue и т.д.)
2167
- * 2. Поля формы из fieldRegistry
2168
- * 3. undefined для несуществующих свойств
2169
- */
2170
- get: (e, i) => {
2171
- if (i in e)
2172
- return e[i];
2173
- if (typeof i == "string" && this.fieldRegistry.has(i))
2174
- return this.fieldRegistry.get(i);
2175
- },
2176
- /**
2177
- * Set trap: Перехват установки свойств
2178
- *
2179
- * Запрещает прямую установку значений полей через form.email = value
2180
- * Пользователь должен использовать form.email.setValue(value) или form.setValue({...})
2181
- */
2182
- set: (e, i, s) => typeof i == "string" && this.fieldRegistry.has(i) ? !1 : (e[i] = s, !0),
2183
- /**
2184
- * Has trap: Перехват оператора 'in'
2185
- *
2186
- * Позволяет проверять существование полей:
2187
- * if ('email' in form) { ... }
2188
- */
2189
- has: (e, i) => typeof i == "string" && this.fieldRegistry.has(i) ? !0 : i in e,
2190
- /**
2191
- * OwnKeys trap: Перехват Object.keys() / Object.getOwnPropertyNames()
2192
- *
2193
- * Возвращает объединенный список:
2194
- * - Ключей самого GroupNode
2195
- * - Ключей полей из fieldRegistry
2196
- */
2197
- ownKeys: (e) => {
2198
- const i = Reflect.ownKeys(e), s = Array.from(this.fieldRegistry.keys());
2199
- return [.../* @__PURE__ */ new Set([...i, ...s])];
2200
- },
2201
- /**
2202
- * GetOwnPropertyDescriptor trap: Перехват Object.getOwnPropertyDescriptor()
2203
- *
2204
- * Возвращает дескриптор свойства для полей и свойств GroupNode
2205
- * Важно для корректной работы Object.keys() и других рефлексивных операций
2206
- */
2207
- getOwnPropertyDescriptor: (e, i) => typeof i == "string" && this.fieldRegistry.has(i) ? {
2208
- enumerable: !0,
2209
- // Поле должно быть перечисляемым
2210
- configurable: !0
2211
- // Поле может быть удалено
2212
- // Не указываем writable, т.к. это accessor property через get/set traps
2213
- } : Reflect.getOwnPropertyDescriptor(e, i)
2214
- });
2215
- }
2216
- }
2217
- class G {
2218
- // ============================================================================
2219
- // Конструктор
2220
- // ============================================================================
2221
- /**
2222
- * Создать менеджер состояния
2223
- *
2224
- * @param fieldRegistry - реестр полей формы
2225
- */
2226
- constructor(t) {
2227
- this.fieldRegistry = t, this._submitting = d(!1), this._disabled = d(!1), this._formErrors = d([]), this.value = l(() => {
2228
- const e = {};
2229
- return this.fieldRegistry.forEach((i, s) => {
2230
- e[s] = i.value.value;
2231
- }), e;
2232
- }), this.valid = l(() => this._formErrors.value.length > 0 ? !1 : Array.from(this.fieldRegistry.values()).every((i) => i.valid.value)), this.invalid = l(() => !this.valid.value), this.pending = l(
2233
- () => Array.from(this.fieldRegistry.values()).some((e) => e.pending.value)
2234
- ), this.touched = l(
2235
- () => Array.from(this.fieldRegistry.values()).some((e) => e.touched.value)
2236
- ), this.dirty = l(
2237
- () => Array.from(this.fieldRegistry.values()).some((e) => e.dirty.value)
2238
- ), this.errors = l(() => {
2239
- const e = [];
2240
- return e.push(...this._formErrors.value), this.fieldRegistry.forEach((i) => {
2241
- e.push(...i.errors.value);
2242
- }), e;
2243
- }), this.status = l(() => this._disabled.value ? "disabled" : this.pending.value ? "pending" : this.invalid.value ? "invalid" : "valid"), this.submitting = l(() => this._submitting.value);
2244
- }
2245
- // ============================================================================
2246
- // Приватные сигналы (мутабельные)
2247
- // ============================================================================
2248
- /**
2249
- * Флаг отправки формы
2250
- * Устанавливается в true во время отправки формы на сервер
2251
- */
2252
- _submitting;
2253
- /**
2254
- * Флаг disabled состояния
2255
- * Если true, форма считается disabled
2256
- */
2257
- _disabled;
2258
- /**
2259
- * Form-level validation errors (не связанные с конкретным полем)
2260
- * Используется для server-side errors или кросс-полевой валидации
2261
- */
2262
- _formErrors;
2263
- // ============================================================================
2264
- // Публичные computed signals (read-only)
2265
- // ============================================================================
2266
- /**
2267
- * Значение формы как объект
2268
- *
2269
- * Computed signal, который автоматически пересчитывается при изменении любого поля.
2270
- * Использует мемоизацию - если зависимости не изменились, вернет закешированный объект.
2271
- *
2272
- * @example
2273
- * ```typescript
2274
- * const form = new GroupNode({ email: { value: 'test@mail.com' } });
2275
- * console.log(form.value.value); // { email: 'test@mail.com' }
2276
- * ```
2277
- */
2278
- value;
2279
- /**
2280
- * Форма валидна?
2281
- *
2282
- * Computed signal. Форма валидна, если:
2283
- * - Нет form-level errors
2284
- * - Все поля валидны
2285
- */
2286
- valid;
2287
- /**
2288
- * Форма невалидна?
2289
- *
2290
- * Computed signal. Инверсия valid.
2291
- */
2292
- invalid;
2293
- /**
2294
- * Хотя бы одно поле touched?
2295
- *
2296
- * Computed signal. Возвращает true, если хотя бы одно поле было touched.
2297
- */
2298
- touched;
2299
- /**
2300
- * Хотя бы одно поле dirty?
2301
- *
2302
- * Computed signal. Возвращает true, если хотя бы одно поле изменилось.
2303
- */
2304
- dirty;
2305
- /**
2306
- * Асинхронная валидация в процессе?
2307
- *
2308
- * Computed signal. Возвращает true, если хотя бы одно поле находится в pending состоянии.
2309
- */
2310
- pending;
2311
- /**
2312
- * Все ошибки валидации
2313
- *
2314
- * Computed signal. Возвращает массив всех ошибок:
2315
- * - Form-level errors
2316
- * - Field-level errors (из всех вложенных полей)
2317
- */
2318
- errors;
2319
- /**
2320
- * Общий статус формы
2321
- *
2322
- * Computed signal. Возможные значения:
2323
- * - 'disabled' - форма disabled
2324
- * - 'pending' - асинхронная валидация в процессе
2325
- * - 'invalid' - форма невалидна
2326
- * - 'valid' - форма валидна
2327
- */
2328
- status;
2329
- /**
2330
- * Форма в процессе отправки?
2331
- *
2332
- * Computed signal (обертка над _submitting для read-only доступа).
2333
- */
2334
- submitting;
2335
- // ============================================================================
2336
- // Публичные методы для управления состоянием
2337
- // ============================================================================
2338
- /**
2339
- * Установить form-level ошибки
2340
- *
2341
- * @param errors - массив ошибок валидации
2342
- *
2343
- * @example
2344
- * ```typescript
2345
- * // Server-side ошибки
2346
- * stateManager.setFormErrors([
2347
- * { code: 'server_error', message: 'Пользователь с таким email уже существует' }
2348
- * ]);
2349
- * ```
2350
- */
2351
- setFormErrors(t) {
2352
- this._formErrors.value = t;
2353
- }
2354
- /**
2355
- * Очистить form-level ошибки
2356
- */
2357
- clearFormErrors() {
2358
- this._formErrors.value = [];
2359
- }
2360
- /**
2361
- * Получить form-level ошибки
2362
- */
2363
- getFormErrors() {
2364
- return this._formErrors.value;
2365
- }
2366
- /**
2367
- * Установить флаг submitting
2368
- *
2369
- * @param value - true если форма отправляется, false если нет
2370
- *
2371
- * @example
2372
- * ```typescript
2373
- * stateManager.setSubmitting(true);
2374
- * await api.submitForm(form.getValue());
2375
- * stateManager.setSubmitting(false);
2376
- * ```
2377
- */
2378
- setSubmitting(t) {
2379
- this._submitting.value = t;
2380
- }
2381
- /**
2382
- * Установить флаг disabled
2383
- *
2384
- * @param value - true если форма disabled, false если нет
2385
- */
2386
- setDisabled(t) {
2387
- this._disabled.value = t;
2388
- }
2389
- /**
2390
- * Получить флаг disabled
2391
- */
2392
- isDisabled() {
2393
- return this._disabled.value;
2394
- }
2395
- }
2396
- class f extends b {
2397
- // ============================================================================
2398
- // Приватные поля
2399
- // ============================================================================
2400
- id = S();
2401
- /**
2402
- * Реестр полей формы
2403
- * Использует FieldRegistry для инкапсуляции логики управления коллекцией полей
2404
- */
2405
- fieldRegistry;
2406
- /**
2407
- * Строитель Proxy для типобезопасного доступа к полям
2408
- * Использует ProxyBuilder для создания Proxy с расширенной функциональностью
2409
- */
2410
- proxyBuilder;
2411
- /**
2412
- * Менеджер состояния формы
2413
- * Инкапсулирует всю логику создания и управления сигналами состояния
2414
- * Извлечен из GroupNode для соблюдения SRP
2415
- */
2416
- stateManager;
2417
- /**
2418
- * Менеджер подписок для централизованного cleanup
2419
- * Использует SubscriptionManager вместо массива для управления подписками
2420
- */
2421
- disposers = new V();
2422
- /**
2423
- * Ссылка на Proxy-инстанс для использования в BehaviorContext
2424
- * Устанавливается в конструкторе до применения behavior schema
2425
- */
2426
- _proxyInstance;
2427
- /**
2428
- * Навигатор для работы с путями к полям
2429
- * Использует композицию вместо дублирования логики парсинга путей
2430
- */
2431
- pathNavigator = new C();
2432
- /**
2433
- * Фабрика для создания узлов формы
2434
- * Использует композицию для централизованного создания FieldNode/GroupNode/ArrayNode
2435
- */
2436
- nodeFactory = new U();
2437
- /**
2438
- * Реестр валидаторов для этой формы
2439
- * Использует композицию вместо глобального Singleton
2440
- * Обеспечивает полную изоляцию форм друг от друга
2441
- */
2442
- validationRegistry = new A();
2443
- /**
2444
- * Реестр behaviors для этой формы
2445
- * Использует композицию вместо глобального Singleton
2446
- * Обеспечивает полную изоляцию форм друг от друга
2447
- */
2448
- behaviorRegistry = new x();
2449
- /**
2450
- * Аппликатор для применения валидаторов к форме
2451
- * Извлечен из GroupNode для соблюдения SRP
2452
- * Использует композицию для управления процессом валидации
2453
- */
2454
- validationApplicator = new D(this);
2455
- /**
2456
- * Аппликатор для применения behavior схемы к форме
2457
- * Извлечен из GroupNode для соблюдения SRP
2458
- * Использует композицию для управления процессом применения behaviors
2459
- */
2460
- behaviorApplicator = new B(this, this.behaviorRegistry);
2461
- // ============================================================================
2462
- // Публичные computed signals (делегированы в StateManager)
2463
- // ============================================================================
2464
- value;
2465
- valid;
2466
- invalid;
2467
- touched;
2468
- dirty;
2469
- pending;
2470
- errors;
2471
- status;
2472
- submitting;
2473
- constructor(t) {
2474
- super(), this.fieldRegistry = new O(), this.proxyBuilder = new $(this.fieldRegistry);
2475
- const e = "form" in t, i = e ? t.form : t, s = e ? t.behavior : void 0, a = e ? t.validation : void 0;
2476
- for (const [o, h] of Object.entries(i)) {
2477
- const u = this.createNode(h);
2478
- this.fieldRegistry.set(o, u);
2479
- }
2480
- this.stateManager = new G(this.fieldRegistry), this.value = this.stateManager.value, this.valid = this.stateManager.valid, this.invalid = this.stateManager.invalid, this.touched = this.stateManager.touched, this.dirty = this.stateManager.dirty, this.pending = this.stateManager.pending, this.errors = this.stateManager.errors, this.status = this.stateManager.status, this.submitting = this.stateManager.submitting;
2481
- const n = this.proxyBuilder.build(this);
2482
- return this._proxyInstance = n, s && this.applyBehaviorSchema(s), a && this.applyValidationSchema(a), n;
2483
- }
2484
- // ============================================================================
2485
- // Реализация абстрактных методов FormNode
2486
- // ============================================================================
2487
- getValue() {
2488
- const t = {};
2489
- return this.fieldRegistry.forEach((e, i) => {
2490
- t[i] = e.getValue();
2491
- }), t;
2492
- }
2493
- setValue(t, e) {
2494
- for (const [i, s] of Object.entries(t)) {
2495
- const a = this.fieldRegistry.get(i);
2496
- a && a.setValue(s, e);
2497
- }
2498
- }
2499
- patchValue(t) {
2500
- for (const [e, i] of Object.entries(t)) {
2501
- const s = this.fieldRegistry.get(e);
2502
- s && i !== void 0 && s.setValue(i);
2503
- }
2504
- }
2505
- /**
2506
- * Сбросить форму к указанным значениям (или к initialValues)
2507
- *
2508
- * @param value - опциональный объект со значениями для сброса
2509
- *
2510
- * @remarks
2511
- * Рекурсивно вызывает reset() для всех полей формы
2512
- *
2513
- * @example
2514
- * ```typescript
2515
- * // Сброс к initialValues
2516
- * form.reset();
2517
- *
2518
- * // Сброс к новым значениям
2519
- * form.reset({ email: 'new@mail.com', password: '' });
2520
- * ```
2521
- */
2522
- reset(t) {
2523
- this.fieldRegistry.forEach((e, i) => {
2524
- const s = t?.[i];
2525
- e.reset(s);
2526
- });
2527
- }
2528
- /**
2529
- * Сбросить форму к исходным значениям (initialValues)
2530
- *
2531
- * @remarks
2532
- * Рекурсивно вызывает resetToInitial() для всех полей формы.
2533
- * Более явный способ сброса к начальным значениям по сравнению с reset()
2534
- *
2535
- * Полезно когда:
2536
- * - Пользователь нажал "Cancel" - полная отмена изменений
2537
- * - Форма была изменена через reset(newValues), но нужно вернуться к самому началу
2538
- * - Явное намерение показать "отмена всех изменений"
2539
- *
2540
- * @example
2541
- * ```typescript
2542
- * const form = new GroupNode({
2543
- * email: { value: 'initial@mail.com', component: Input },
2544
- * name: { value: 'John', component: Input }
2545
- * });
2546
- *
2547
- * form.email.setValue('changed@mail.com');
2548
- * form.reset({ email: 'temp@mail.com', name: 'Jane' });
2549
- * console.log(form.getValue()); // { email: 'temp@mail.com', name: 'Jane' }
2550
- *
2551
- * form.resetToInitial();
2552
- * console.log(form.getValue()); // { email: 'initial@mail.com', name: 'John' }
2553
- * ```
2554
- */
2555
- resetToInitial() {
2556
- this.fieldRegistry.forEach((t) => {
2557
- "resetToInitial" in t && typeof t.resetToInitial == "function" ? t.resetToInitial() : t.reset();
2558
- });
2559
- }
2560
- async validate() {
2561
- this.clearErrors(), await Promise.all(Array.from(this.fieldRegistry.values()).map((e) => e.validate()));
2562
- const t = this.validationRegistry.getValidators();
2563
- return t && t.length > 0 && await this.applyContextualValidators(t), Array.from(this.fieldRegistry.values()).every((e) => e.valid.value);
2564
- }
2565
- /**
2566
- * Установить form-level validation errors
2567
- * Используется для server-side validation или кросс-полевых ошибок
2568
- *
2569
- * @param errors - массив ошибок уровня формы
2570
- *
2571
- * @example
2572
- * ```typescript
2573
- * // Server-side validation после submit
2574
- * try {
2575
- * await api.createUser(form.getValue());
2576
- * } catch (error) {
2577
- * form.setErrors([
2578
- * { code: 'duplicate_email', message: 'Email уже используется' }
2579
- * ]);
2580
- * }
2581
- * ```
2582
- */
2583
- setErrors(t) {
2584
- this.stateManager.setFormErrors(t);
2585
- }
2586
- /**
2587
- * Очистить все errors (form-level + field-level)
2588
- */
2589
- clearErrors() {
2590
- this.stateManager.clearFormErrors(), this.fieldRegistry.forEach((t) => t.clearErrors());
2591
- }
2592
- /**
2593
- * Получить поле по ключу
2594
- *
2595
- * Публичный метод для доступа к полю из fieldRegistry
2596
- *
2597
- * @param key - Ключ поля
2598
- * @returns FormNode или undefined, если поле не найдено
2599
- *
2600
- * @example
2601
- * ```typescript
2602
- * const emailField = form.getField('email');
2603
- * if (emailField) {
2604
- * console.log(emailField.value.value);
2605
- * }
2606
- * ```
2607
- */
2608
- getField(t) {
2609
- return this.fieldRegistry.get(t);
2610
- }
2611
- /**
2612
- * Получить Map всех полей формы
2613
- *
2614
- * Используется в FieldPathNavigator для навигации по полям
2615
- *
2616
- * @returns Map полей формы
2617
- */
2618
- get fields() {
2619
- return this.fieldRegistry;
2620
- }
2621
- /**
2622
- * Получить Proxy-инстанс для прямого доступа к полям
2623
- *
2624
- * Proxy позволяет обращаться к полям формы напрямую через точечную нотацию:
2625
- * - form.email вместо form.fields.get('email')
2626
- * - form.address.city вместо form.fields.get('address').fields.get('city')
2627
- *
2628
- * Используется в:
2629
- * - BehaviorApplicator для доступа к полям в behavior functions
2630
- * - ValidationApplicator для доступа к форме в tree validators
2631
- *
2632
- * @returns Proxy-инстанс с типобезопасным доступом к полям или сама форма, если proxy не доступен
2633
- *
2634
- * @example
2635
- * ```typescript
2636
- * const form = new GroupNode({
2637
- * controls: {
2638
- * email: new FieldNode({ value: '' }),
2639
- * name: new FieldNode({ value: '' })
2640
- * }
2641
- * });
2642
- *
2643
- * const proxy = form.getProxy();
2644
- * console.log(proxy.email.value); // Прямой доступ к полю
2645
- * ```
2646
- */
2647
- getProxy() {
2648
- return this._proxyInstance || this;
2649
- }
2650
- /**
2651
- * Получить все поля формы как итератор
2652
- *
2653
- * Предоставляет доступ к внутренним полям для валидации и других операций
2654
- *
2655
- * @returns Итератор по всем полям формы
2656
- *
2657
- * @example
2658
- * ```typescript
2659
- * // Валидация всех полей
2660
- * await Promise.all(
2661
- * Array.from(form.getAllFields()).map(field => field.validate())
2662
- * );
2663
- * ```
2664
- */
2665
- getAllFields() {
2666
- return this.fieldRegistry.values();
2667
- }
2668
- // ============================================================================
2669
- // Protected hooks (Template Method pattern)
2670
- // ============================================================================
2671
- /**
2672
- * Hook: вызывается после markAsTouched()
2673
- *
2674
- * Для GroupNode: рекурсивно помечаем все дочерние поля как touched
2675
- */
2676
- onMarkAsTouched() {
2677
- this.fieldRegistry.forEach((t) => t.markAsTouched());
2678
- }
2679
- /**
2680
- * Hook: вызывается после markAsUntouched()
2681
- *
2682
- * Для GroupNode: рекурсивно помечаем все дочерние поля как untouched
2683
- */
2684
- onMarkAsUntouched() {
2685
- this.fieldRegistry.forEach((t) => t.markAsUntouched());
2686
- }
2687
- /**
2688
- * Hook: вызывается после markAsDirty()
2689
- *
2690
- * Для GroupNode: рекурсивно помечаем все дочерние поля как dirty
2691
- */
2692
- onMarkAsDirty() {
2693
- this.fieldRegistry.forEach((t) => t.markAsDirty());
2694
- }
2695
- /**
2696
- * Hook: вызывается после markAsPristine()
2697
- *
2698
- * Для GroupNode: рекурсивно помечаем все дочерние поля как pristine
2699
- */
2700
- onMarkAsPristine() {
2701
- this.fieldRegistry.forEach((t) => t.markAsPristine());
2702
- }
2703
- // ============================================================================
2704
- // Дополнительные методы (из FormStore)
2705
- // ============================================================================
2706
- /**
2707
- * Отправить форму
2708
- * Валидирует форму и вызывает onSubmit если форма валидна
2709
- */
2710
- async submit(t) {
2711
- if (this.markAsTouched(), !await this.validate())
2712
- return null;
2713
- this.stateManager.setSubmitting(!0);
2714
- try {
2715
- return await t(this.getValue());
2716
- } finally {
2717
- this.stateManager.setSubmitting(!1);
2718
- }
2719
- }
2720
- /**
2721
- * Применить validation schema к форме
2722
- *
2723
- * Использует локальный реестр валидаторов (this.validationRegistry)
2724
- * вместо глобального Singleton для изоляции форм друг от друга.
2725
- */
2726
- applyValidationSchema(t) {
2727
- this.validationRegistry.beginRegistration();
2728
- try {
2729
- const e = R();
2730
- t(e);
2731
- const i = this.getProxy();
2732
- this.validationRegistry.endRegistration(i);
2733
- } catch (e) {
2734
- throw console.error("Error applying validation schema:", e), e;
2735
- }
2736
- }
2737
- /**
2738
- * Применить behavior schema к форме
2739
- *
2740
- * ✅ РЕФАКТОРИНГ: Делегирование BehaviorApplicator (SRP)
2741
- *
2742
- * Логика применения behavior схемы извлечена в BehaviorApplicator для:
2743
- * - Соблюдения Single Responsibility Principle
2744
- * - Уменьшения размера GroupNode (~50 строк)
2745
- * - Улучшения тестируемости
2746
- * - Консистентности с ValidationApplicator
2747
- *
2748
- * @param schemaFn Функция описания поведения формы
2749
- * @returns Функция cleanup для отписки от всех behaviors
2750
- *
2751
- * @example
2752
- * ```typescript
2753
- * import { copyFrom, enableWhen, computeFrom } from '@/lib/forms/core/behaviors';
2754
- *
2755
- * const behaviorSchema: BehaviorSchemaFn<MyForm> = (path) => {
2756
- * copyFrom(path.residenceAddress, path.registrationAddress, {
2757
- * when: (form) => form.sameAsRegistration === true
2758
- * });
2759
- *
2760
- * enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage');
2761
- *
2762
- * computeFrom(
2763
- * path.initialPayment,
2764
- * [path.propertyValue],
2765
- * (propertyValue) => propertyValue ? propertyValue * 0.2 : null
2766
- * );
2767
- * };
2768
- *
2769
- * const cleanup = form.applyBehaviorSchema(behaviorSchema);
2770
- *
2771
- * // Cleanup при unmount
2772
- * useEffect(() => cleanup, []);
2773
- * ```
2774
- */
2775
- applyBehaviorSchema(t) {
2776
- return this.behaviorApplicator.apply(t);
2777
- }
2778
- /**
2779
- * Получить вложенное поле по пути
2780
- *
2781
- * Поддерживаемые форматы путей:
2782
- * - Simple: "email" - получить поле верхнего уровня
2783
- * - Nested: "address.city" - получить вложенное поле
2784
- * - Array index: "items[0]" - получить элемент массива по индексу
2785
- * - Combined: "items[0].name" - получить поле элемента массива
2786
- *
2787
- * @param path - Путь к полю
2788
- * @returns FormNode если найдено, undefined если путь не существует
2789
- *
2790
- * @example
2791
- * ```typescript
2792
- * const form = new GroupNode({
2793
- * email: { value: '', component: Input },
2794
- * address: {
2795
- * city: { value: '', component: Input }
2796
- * },
2797
- * items: [{ name: { value: '', component: Input } }]
2798
- * });
2799
- *
2800
- * form.getFieldByPath('email'); // FieldNode
2801
- * form.getFieldByPath('address.city'); // FieldNode
2802
- * form.getFieldByPath('items[0]'); // GroupNode
2803
- * form.getFieldByPath('items[0].name'); // FieldNode
2804
- * form.getFieldByPath('invalid.path'); // undefined
2805
- * ```
2806
- */
2807
- getFieldByPath(t) {
2808
- if (t.startsWith(".") || t.endsWith("."))
2809
- return;
2810
- const e = this.pathNavigator.parsePath(t);
2811
- if (e.length === 0)
2812
- return;
2813
- let i = this;
2814
- for (const s of e) {
2815
- if (!(i instanceof f) || (i = i.getField(s.key), !i)) return;
2816
- if (s.index !== void 0)
2817
- if ("at" in i && "length" in i && typeof i.at == "function") {
2818
- const a = i.at(s.index);
2819
- if (!a) return;
2820
- i = a;
2821
- } else
2822
- return;
2823
- }
2824
- return i;
2825
- }
2826
- /**
2827
- * Применить contextual валидаторы к полям
2828
- *
2829
- * ✅ РЕФАКТОРИНГ: Делегирование ValidationApplicator (SRP)
2830
- *
2831
- * Логика применения валидаторов извлечена в ValidationApplicator для:
2832
- * - Соблюдения Single Responsibility Principle
2833
- * - Уменьшения размера GroupNode (~120 строк)
2834
- * - Улучшения тестируемости
2835
- *
2836
- * @param validators Зарегистрированные валидаторы
2837
- */
2838
- async applyContextualValidators(t) {
2839
- await this.validationApplicator.apply(t);
2840
- }
2841
- // ============================================================================
2842
- // Private методы для создания узлов
2843
- // ============================================================================
2844
- /**
2845
- * Создать узел на основе конфигурации
2846
- *
2847
- * ✅ РЕФАКТОРИНГ: Полное делегирование NodeFactory
2848
- *
2849
- * NodeFactory теперь обрабатывает:
2850
- * - Массивы [schema, ...items]
2851
- * - FieldConfig
2852
- * - GroupConfig
2853
- * - ArrayConfig
2854
- *
2855
- * @param config Конфигурация узла
2856
- * @returns Созданный узел формы
2857
- * @private
2858
- */
2859
- createNode(t) {
2860
- return this.nodeFactory.createNode(t);
2861
- }
2862
- // ============================================================================
2863
- // Методы-помощники для реактивности (Фаза 1)
2864
- // ============================================================================
2865
- /**
2866
- * Связывает два поля: при изменении source автоматически обновляется target
2867
- * Поддерживает опциональную трансформацию значения
2868
- *
2869
- * @param sourceKey - Ключ поля-источника
2870
- * @param targetKey - Ключ поля-цели
2871
- * @param transform - Опциональная функция трансформации значения
2872
- * @returns Функция отписки для cleanup
2873
- *
2874
- * @example
2875
- * ```typescript
2876
- * // Автоматический расчет минимального взноса от стоимости недвижимости
2877
- * const dispose = form.linkFields(
2878
- * 'propertyValue',
2879
- * 'initialPayment',
2880
- * (propertyValue) => propertyValue ? propertyValue * 0.2 : null
2881
- * );
2882
- *
2883
- * // При изменении propertyValue → автоматически обновится initialPayment
2884
- * form.propertyValue.setValue(1000000);
2885
- * // initialPayment станет 200000
2886
- *
2887
- * // Cleanup
2888
- * useEffect(() => dispose, []);
2889
- * ```
2890
- */
2891
- linkFields(t, e, i) {
2892
- const s = this.fieldRegistry.get(t), a = this.fieldRegistry.get(e);
2893
- if (!s || !a)
2894
- return () => {
2895
- };
2896
- const n = c(() => {
2897
- const h = s.value.value, u = i ? i(h) : h;
2898
- a.setValue(u, { emitEvent: !1 });
2899
- }), o = `linkFields-${Date.now()}-${Math.random()}`;
2900
- return this.disposers.add(o, n);
2901
- }
2902
- /**
2903
- * Подписка на изменения вложенного поля по строковому пути
2904
- * Поддерживает вложенные пути типа "address.city"
2905
- *
2906
- * @param fieldPath - Строковый путь к полю (например, "address.city")
2907
- * @param callback - Функция, вызываемая при изменении поля
2908
- * @returns Функция отписки для cleanup
2909
- *
2910
- * @example
2911
- * ```typescript
2912
- * // Подписка на изменение страны для загрузки городов
2913
- * const dispose = form.watchField(
2914
- * 'registrationAddress.country',
2915
- * async (countryCode) => {
2916
- * if (countryCode) {
2917
- * const cities = await fetchCitiesByCountry(countryCode);
2918
- * form.registrationAddress.city.updateComponentProps({
2919
- * options: cities
2920
- * });
2921
- * }
2922
- * }
2923
- * );
2924
- *
2925
- * // Cleanup
2926
- * useEffect(() => dispose, []);
2927
- * ```
2928
- */
2929
- watchField(t, e) {
2930
- const i = this.getFieldByPath(t);
2931
- if (!i)
2932
- return () => {
2933
- };
2934
- const s = c(() => {
2935
- const n = i.value.value;
2936
- e(n);
2937
- }), a = `watchField-${Date.now()}-${Math.random()}`;
2938
- return this.disposers.add(a, s);
2939
- }
2940
- /**
2941
- * Hook: вызывается после disable()
2942
- *
2943
- * Для GroupNode: рекурсивно отключаем все дочерние поля
2944
- */
2945
- onDisable() {
2946
- this.stateManager.setDisabled(!0), this.fieldRegistry.forEach((t) => {
2947
- t.disable();
2948
- });
2949
- }
2950
- /**
2951
- * Hook: вызывается после enable()
2952
- *
2953
- * Для GroupNode: рекурсивно включаем все дочерние поля
2954
- */
2955
- onEnable() {
2956
- this.stateManager.setDisabled(!1), this.fieldRegistry.forEach((t) => {
2957
- t.enable();
2958
- });
2959
- }
2960
- /**
2961
- * Очистить все ресурсы узла
2962
- * Рекурсивно очищает все subscriptions и дочерние узлы
2963
- *
2964
- * @example
2965
- * ```typescript
2966
- * useEffect(() => {
2967
- * return () => {
2968
- * form.dispose();
2969
- * };
2970
- * }, []);
2971
- * ```
2972
- */
2973
- dispose() {
2974
- this.disposers.dispose(), this.fieldRegistry.forEach((t) => {
2975
- "dispose" in t && typeof t.dispose == "function" && t.dispose();
2976
- });
2977
- }
2978
- }
2979
- class U {
2980
- /**
2981
- * Создает узел формы на основе конфигурации
2982
- *
2983
- * ✅ ОБНОВЛЕНО: Теперь поддерживает массивы напрямую
2984
- *
2985
- * Автоматически определяет тип узла:
2986
- * - FieldNode: имеет value и component
2987
- * - ArrayNode: массив [schema, ...items] или { schema, initialItems }
2988
- * - GroupNode: объект без value, component, schema
2989
- *
2990
- * @param config Конфигурация узла
2991
- * @returns Экземпляр FieldNode, GroupNode или ArrayNode
2992
- * @throws Error если конфиг не соответствует ни одному типу
2993
- *
2994
- * @example
2995
- * ```typescript
2996
- * const factory = new NodeFactory();
2997
- *
2998
- * // FieldNode
2999
- * const field = factory.createNode({
3000
- * value: 'test@mail.com',
3001
- * component: Input,
3002
- * validators: [required, email]
3003
- * });
3004
- *
3005
- * // GroupNode
3006
- * const group = factory.createNode({
3007
- * email: { value: '', component: Input },
3008
- * password: { value: '', component: Input }
3009
- * });
3010
- *
3011
- * // ArrayNode (объект)
3012
- * const array = factory.createNode({
3013
- * schema: { title: { value: '', component: Input } },
3014
- * initialItems: [{ title: 'Item 1' }]
3015
- * });
3016
- *
3017
- * // ArrayNode (массив) - новый формат
3018
- * const array2 = factory.createNode([
3019
- * { title: { value: '', component: Input } }, // schema
3020
- * { title: 'Item 1' }, // initial item 1
3021
- * { title: 'Item 2' } // initial item 2
3022
- * ]);
3023
- * ```
3024
- */
3025
- createNode(t) {
3026
- if (Array.isArray(t) && t.length >= 1)
3027
- return this.createArrayNodeFromArray(t);
3028
- if (this.isFieldConfig(t))
3029
- return new F(t);
3030
- if (this.isArrayConfig(t)) {
3031
- const e = t;
3032
- return new m(
3033
- e.schema,
3034
- e.initialItems
3035
- );
3036
- }
3037
- if (this.isGroupConfig(t))
3038
- return new f(t);
3039
- throw new Error(
3040
- `NodeFactory: Unknown node config. Expected FieldConfig, GroupConfig, or ArrayConfig, but got: ${JSON.stringify(
3041
- t
3042
- )}`
3043
- );
3044
- }
3045
- /**
3046
- * Создать ArrayNode из массива [schema, ...initialItems]
3047
- *
3048
- * ✅ НОВОЕ: Извлечено из GroupNode для централизации логики
3049
- *
3050
- * Формат: [itemSchema, ...initialItems]
3051
- * - Первый элемент - схема элемента массива
3052
- * - Остальные элементы - начальные значения
3053
- *
3054
- * @param config Массив с схемой и начальными элементами
3055
- * @returns ArrayNode
3056
- *
3057
- * @example
3058
- * ```typescript
3059
- * const factory = new NodeFactory();
3060
- *
3061
- * // Массив с начальными элементами
3062
- * const array = factory.createArrayNodeFromArray([
3063
- * { title: { value: '', component: Input } }, // schema
3064
- * { title: 'Item 1' }, // initial value
3065
- * { title: 'Item 2' } // initial value
3066
- * ]);
3067
- * ```
3068
- * @private
3069
- */
3070
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3071
- createArrayNodeFromArray(t) {
3072
- const [e, ...i] = t, s = [];
3073
- this.isGroupConfig(e) && s.push(this.extractValues(e));
3074
- for (const a of i)
3075
- this.isGroupConfig(a) ? s.push(this.extractValues(a)) : s.push(a);
3076
- return new m(e, s);
3077
- }
3078
- /**
3079
- * Извлечь значения из схемы (рекурсивно)
3080
- *
3081
- * ✅ НОВОЕ: Извлечено из GroupNode для централизации логики
3082
- *
3083
- * Преобразует схему формы в объект со значениями:
3084
- * - `{ name: { value: 'John', component: Input } } → { name: 'John' }`
3085
- * - Поддерживает вложенные группы
3086
- * - Поддерживает массивы
3087
- *
3088
- * @param schema Схема формы
3089
- * @returns Объект со значениями полей
3090
- *
3091
- * @example
3092
- * ```typescript
3093
- * const factory = new NodeFactory();
3094
- *
3095
- * const schema = {
3096
- * name: { value: 'John', component: Input },
3097
- * age: { value: 30, component: Input },
3098
- * address: {
3099
- * city: { value: 'Moscow', component: Input }
3100
- * }
3101
- * };
3102
- *
3103
- * factory.extractValues(schema);
3104
- * // { name: 'John', age: 30, address: { city: 'Moscow' } }
3105
- * ```
3106
- */
3107
- extractValues(t) {
3108
- if (this.isFieldConfig(t))
3109
- return t.value;
3110
- if (Array.isArray(t))
3111
- return t.map((e) => this.extractValues(e));
3112
- if (this.isGroupConfig(t)) {
3113
- const e = {};
3114
- for (const [i, s] of Object.entries(t))
3115
- e[i] = this.extractValues(s);
3116
- return e;
3117
- }
3118
- return t;
3119
- }
3120
- /**
3121
- * Проверяет, является ли конфиг конфигурацией поля (FieldConfig)
3122
- *
3123
- * FieldConfig имеет обязательные свойства:
3124
- * - value: начальное значение поля
3125
- * - component: React-компонент для отображения
3126
- *
3127
- * @param config Проверяемая конфигурация
3128
- * @returns true если config является FieldConfig
3129
- *
3130
- * @example
3131
- * ```typescript
3132
- * const factory = new NodeFactory();
3133
- *
3134
- * factory.isFieldConfig({ value: '', component: Input }); // true
3135
- * factory.isFieldConfig({ email: { value: '' } }); // false
3136
- * factory.isFieldConfig(null); // false
3137
- * ```
3138
- */
3139
- isFieldConfig(t) {
3140
- return t != null && typeof t == "object" && "value" in t && "component" in t;
3141
- }
3142
- /**
3143
- * Проверяет, является ли конфиг конфигурацией массива (ArrayConfig)
3144
- *
3145
- * ArrayConfig имеет обязательное свойство:
3146
- * - schema: схема для элементов массива
3147
- *
3148
- * И НЕ имеет:
3149
- * - value (отличие от FieldConfig)
3150
- *
3151
- * @param config Проверяемая конфигурация
3152
- * @returns true если config является ArrayConfig
3153
- *
3154
- * @example
3155
- * ```typescript
3156
- * const factory = new NodeFactory();
3157
- *
3158
- * factory.isArrayConfig({ schema: {}, initialItems: [] }); // true
3159
- * factory.isArrayConfig({ value: '', component: Input }); // false
3160
- * factory.isArrayConfig({ email: { value: '' } }); // false
3161
- * ```
3162
- */
3163
- isArrayConfig(t) {
3164
- return t != null && typeof t == "object" && "schema" in t && !("value" in t);
3165
- }
3166
- /**
3167
- * Проверяет, является ли конфиг конфигурацией группы (GroupConfig)
3168
- *
3169
- * GroupConfig - это объект, который:
3170
- * - НЕ является FieldConfig (нет value/component)
3171
- * - НЕ является ArrayConfig (нет schema)
3172
- * - Содержит вложенные конфиги полей/групп/массивов
3173
- *
3174
- * @param config Проверяемая конфигурация
3175
- * @returns true если config является GroupConfig
3176
- *
3177
- * @example
3178
- * ```typescript
3179
- * const factory = new NodeFactory();
3180
- *
3181
- * factory.isGroupConfig({
3182
- * email: { value: '', component: Input },
3183
- * password: { value: '', component: Input }
3184
- * }); // true
3185
- *
3186
- * factory.isGroupConfig({ value: '', component: Input }); // false
3187
- * factory.isGroupConfig({ schema: {} }); // false
3188
- * factory.isGroupConfig(null); // false
3189
- * ```
3190
- */
3191
- isGroupConfig(t) {
3192
- return t != null && typeof t == "object" && !this.isFieldConfig(t) && !this.isArrayConfig(t);
3193
- }
3194
- }
3195
- export {
3196
- m as A,
3197
- y as E,
3198
- b as F,
3199
- f as G,
3200
- U as N,
3201
- V as S,
3202
- T,
3203
- I as V,
3204
- F as a,
3205
- C as b,
3206
- _ as c,
3207
- P as d,
3208
- N as e,
3209
- p as f,
3210
- W as g,
3211
- M as h,
3212
- v as i,
3213
- R as j,
3214
- z as k,
3215
- K as t,
3216
- J as v
3217
- };