@reformer/core 1.1.0 → 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/behaviors-DzYL8kY_.js +499 -0
  2. package/dist/behaviors.d.ts +6 -2
  3. package/dist/behaviors.js +19 -227
  4. package/dist/core/behavior/behavior-context.d.ts +6 -2
  5. package/dist/core/behavior/create-field-path.d.ts +3 -16
  6. package/dist/core/nodes/group-node.d.ts +14 -193
  7. package/dist/core/types/form-context.d.ts +10 -4
  8. package/dist/core/utils/field-path.d.ts +48 -0
  9. package/dist/core/utils/index.d.ts +1 -0
  10. package/dist/core/validation/core/validate-tree.d.ts +10 -4
  11. package/dist/core/validation/field-path.d.ts +3 -39
  12. package/dist/core/validation/validation-context.d.ts +23 -0
  13. package/dist/hooks/types.d.ts +328 -0
  14. package/dist/hooks/useFormControl.d.ts +13 -37
  15. package/dist/hooks/useFormControlValue.d.ts +167 -0
  16. package/dist/hooks/useSignalSubscription.d.ts +17 -0
  17. package/dist/index.d.ts +6 -1
  18. package/dist/index.js +2886 -8
  19. package/dist/{create-field-path-CdPF3lIK.js → registry-helpers-BRxAr6nG.js} +133 -347
  20. package/dist/validators-gXoHPdqM.js +418 -0
  21. package/dist/validators.d.ts +6 -2
  22. package/dist/validators.js +29 -296
  23. package/llms.txt +1283 -22
  24. package/package.json +8 -4
  25. package/dist/core/behavior/behavior-applicator.d.ts +0 -71
  26. package/dist/core/behavior/behavior-applicator.js +0 -92
  27. package/dist/core/behavior/behavior-context.js +0 -38
  28. package/dist/core/behavior/behavior-registry.js +0 -198
  29. package/dist/core/behavior/behaviors/compute-from.js +0 -84
  30. package/dist/core/behavior/behaviors/copy-from.js +0 -64
  31. package/dist/core/behavior/behaviors/enable-when.js +0 -81
  32. package/dist/core/behavior/behaviors/index.js +0 -11
  33. package/dist/core/behavior/behaviors/reset-when.js +0 -63
  34. package/dist/core/behavior/behaviors/revalidate-when.js +0 -51
  35. package/dist/core/behavior/behaviors/sync-fields.js +0 -66
  36. package/dist/core/behavior/behaviors/transform-value.js +0 -110
  37. package/dist/core/behavior/behaviors/watch-field.js +0 -56
  38. package/dist/core/behavior/compose-behavior.js +0 -166
  39. package/dist/core/behavior/create-field-path.js +0 -69
  40. package/dist/core/behavior/index.js +0 -17
  41. package/dist/core/behavior/types.js +0 -7
  42. package/dist/core/context/form-context-impl.js +0 -37
  43. package/dist/core/factories/index.js +0 -6
  44. package/dist/core/factories/node-factory.js +0 -281
  45. package/dist/core/nodes/array-node.js +0 -534
  46. package/dist/core/nodes/field-node.js +0 -510
  47. package/dist/core/nodes/form-node.js +0 -343
  48. package/dist/core/nodes/group-node/field-registry.d.ts +0 -191
  49. package/dist/core/nodes/group-node/field-registry.js +0 -215
  50. package/dist/core/nodes/group-node/index.d.ts +0 -11
  51. package/dist/core/nodes/group-node/index.js +0 -11
  52. package/dist/core/nodes/group-node/proxy-builder.d.ts +0 -71
  53. package/dist/core/nodes/group-node/proxy-builder.js +0 -161
  54. package/dist/core/nodes/group-node/state-manager.d.ts +0 -184
  55. package/dist/core/nodes/group-node/state-manager.js +0 -265
  56. package/dist/core/nodes/group-node.js +0 -770
  57. package/dist/core/types/deep-schema.js +0 -11
  58. package/dist/core/types/field-path.js +0 -4
  59. package/dist/core/types/form-context.js +0 -25
  60. package/dist/core/types/group-node-proxy.js +0 -31
  61. package/dist/core/types/index.js +0 -4
  62. package/dist/core/types/validation-schema.js +0 -10
  63. package/dist/core/utils/create-form.js +0 -24
  64. package/dist/core/utils/debounce.js +0 -197
  65. package/dist/core/utils/error-handler.js +0 -226
  66. package/dist/core/utils/field-path-navigator.js +0 -374
  67. package/dist/core/utils/index.js +0 -14
  68. package/dist/core/utils/registry-helpers.js +0 -79
  69. package/dist/core/utils/registry-stack.js +0 -86
  70. package/dist/core/utils/resources.js +0 -69
  71. package/dist/core/utils/subscription-manager.js +0 -214
  72. package/dist/core/utils/type-guards.js +0 -169
  73. package/dist/core/validation/core/apply-when.js +0 -41
  74. package/dist/core/validation/core/apply.js +0 -38
  75. package/dist/core/validation/core/index.js +0 -8
  76. package/dist/core/validation/core/validate-async.js +0 -45
  77. package/dist/core/validation/core/validate-tree.js +0 -37
  78. package/dist/core/validation/core/validate.js +0 -38
  79. package/dist/core/validation/field-path.js +0 -147
  80. package/dist/core/validation/index.js +0 -33
  81. package/dist/core/validation/validate-form.js +0 -152
  82. package/dist/core/validation/validation-applicator.js +0 -217
  83. package/dist/core/validation/validation-context.js +0 -75
  84. package/dist/core/validation/validation-registry.js +0 -298
  85. package/dist/core/validation/validators/array-validators.js +0 -86
  86. package/dist/core/validation/validators/date.js +0 -117
  87. package/dist/core/validation/validators/email.js +0 -60
  88. package/dist/core/validation/validators/index.js +0 -14
  89. package/dist/core/validation/validators/max-length.js +0 -60
  90. package/dist/core/validation/validators/max.js +0 -60
  91. package/dist/core/validation/validators/min-length.js +0 -60
  92. package/dist/core/validation/validators/min.js +0 -60
  93. package/dist/core/validation/validators/number.js +0 -90
  94. package/dist/core/validation/validators/pattern.js +0 -62
  95. package/dist/core/validation/validators/phone.js +0 -58
  96. package/dist/core/validation/validators/required.js +0 -69
  97. package/dist/core/validation/validators/url.js +0 -55
  98. package/dist/hooks/useFormControl.js +0 -298
  99. package/dist/node-factory-D7DOnSSN.js +0 -3200
@@ -1,3200 +0,0 @@
1
- import { d, w as l, E as c, c as w, V as A, B as F } from "./create-field-path-CdPF3lIK.js";
2
- import { v4 as k } from "uuid";
3
- class g {
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 _ {
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 v = /* @__PURE__ */ ((r) => (r.THROW = "throw", r.LOG = "log", r.CONVERT = "convert", r))(v || {});
449
- class m {
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 R extends g {
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 _();
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 m.handle(
768
- o,
769
- "FieldNode AsyncValidator",
770
- v.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 S() {
946
- return b("");
947
- }
948
- function b(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 b(`${i}.${n}`);
976
- }
977
- }
978
- });
979
- }
980
- });
981
- }
982
- function x(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 L(r) {
993
- const t = x(r);
994
- return b(t);
995
- }
996
- function K(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 E extends g {
1006
- // ============================================================================
1007
- // Приватные поля
1008
- // ============================================================================
1009
- items;
1010
- itemSchema;
1011
- initialItems;
1012
- /**
1013
- * Менеджер подписок для централизованного cleanup
1014
- * Использует SubscriptionManager вместо массива для управления подписками
1015
- */
1016
- disposers = new _();
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 y(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 y)
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 f(r) {
1416
- return r == null ? !1 : typeof r == "object" && "value" in r && "setValue" in r && "getValue" in r && "validate" in r;
1417
- }
1418
- function p(r) {
1419
- return r == null ? !1 : f(r) && "validators" in r && "asyncValidators" in r && // FieldNode имеет markAsTouched метод
1420
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1421
- typeof r.markAsTouched == "function" && // У FieldNode нет fields или items
1422
- !("fields" in r) && !("items" in r);
1423
- }
1424
- function M(r) {
1425
- return r == null ? !1 : f(r) && "applyValidationSchema" in r && "applyBehaviorSchema" in r && "getFieldByPath" in r && // GroupNode НЕ имеет items/push/removeAt (это ArrayNode)
1426
- !("items" in r) && !("push" in r) && !("removeAt" in r);
1427
- }
1428
- function P(r) {
1429
- return r == null ? !1 : f(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
1430
- typeof r.push == "function" && // eslint-disable-next-line @typescript-eslint/no-explicit-any
1431
- typeof r.removeAt == "function";
1432
- }
1433
- function z(r) {
1434
- return p(r) ? "FieldNode" : M(r) ? "GroupNode" : P(r) ? "ArrayNode" : f(r) ? "FormNode" : "Unknown";
1435
- }
1436
- class N {
1437
- _form;
1438
- control;
1439
- /**
1440
- * Форма с типизированным Proxy-доступом к полям
1441
- */
1442
- form;
1443
- constructor(t, e, i) {
1444
- this._form = t, this.control = i, this.form = t._proxyInstance || t.getProxy();
1445
- }
1446
- /**
1447
- * Получить текущее значение поля (внутренний метод для validation-applicator)
1448
- * @internal
1449
- */
1450
- value() {
1451
- return this.control.value.value;
1452
- }
1453
- /**
1454
- * Безопасно установить значение поля по строковому пути
1455
- * Автоматически использует emitEvent: false для предотвращения циклов
1456
- */
1457
- setFieldValue(t, e) {
1458
- const i = this._form.getFieldByPath(t);
1459
- i && f(i) && i.setValue(e, { emitEvent: !1 });
1460
- }
1461
- }
1462
- class I {
1463
- _form;
1464
- /**
1465
- * Форма с типизированным Proxy-доступом к полям
1466
- */
1467
- form;
1468
- constructor(t) {
1469
- this._form = t, this.form = t._proxyInstance || t.getProxy();
1470
- }
1471
- /**
1472
- * Безопасно установить значение поля по строковому пути
1473
- * Автоматически использует emitEvent: false для предотвращения циклов
1474
- */
1475
- setFieldValue(t, e) {
1476
- const i = this._form.getFieldByPath(t);
1477
- i && f(i) && i.setValue(e, { emitEvent: !1 });
1478
- }
1479
- }
1480
- class T {
1481
- form;
1482
- constructor(t) {
1483
- this.form = t;
1484
- }
1485
- /**
1486
- * Применить валидаторы к полям формы
1487
- *
1488
- * Этапы применения:
1489
- * 1. Разделение валидаторов на field и tree
1490
- * 2. Применение field валидаторов (sync/async)
1491
- * 3. Применение tree валидаторов (кросс-полевая валидация)
1492
- *
1493
- * @param validators Зарегистрированные валидаторы
1494
- */
1495
- async apply(t) {
1496
- const { validatorsByField: e, treeValidators: i } = this.groupValidators(t);
1497
- await this.applyFieldValidators(e), this.applyTreeValidators(i);
1498
- }
1499
- /**
1500
- * Группировка валидаторов по типам
1501
- *
1502
- * Разделяет валидаторы на:
1503
- * - Field validators (sync/async) - группируются по fieldPath
1504
- * - Tree validators - применяются ко всей форме
1505
- *
1506
- * @param validators Все зарегистрированные валидаторы
1507
- * @returns Сгруппированные валидаторы
1508
- */
1509
- groupValidators(t) {
1510
- const e = /* @__PURE__ */ new Map(), i = [];
1511
- for (const s of t)
1512
- if (s.type === "tree")
1513
- i.push(s);
1514
- else {
1515
- const a = e.get(s.fieldPath) || [];
1516
- a.push(s), e.set(s.fieldPath, a);
1517
- }
1518
- return { validatorsByField: e, treeValidators: i };
1519
- }
1520
- /**
1521
- * Применение field валидаторов к полям
1522
- *
1523
- * Для каждого поля:
1524
- * 1. Найти FieldNode по пути
1525
- * 2. Проверить условия (condition)
1526
- * 3. Выполнить sync/async валидаторы
1527
- * 4. Установить ошибки на поле
1528
- *
1529
- * @param validatorsByField Валидаторы, сгруппированные по полям
1530
- */
1531
- async applyFieldValidators(t) {
1532
- for (const [e, i] of t) {
1533
- const s = this.form.getFieldByPath(e);
1534
- if (!s) {
1535
- console.warn(`Field ${e} not found in GroupNode`);
1536
- continue;
1537
- }
1538
- if (!p(s)) {
1539
- process.env.NODE_ENV !== "production" && console.warn(`Validation can only run on FieldNode, skipping ${e}`);
1540
- continue;
1541
- }
1542
- const a = [], n = new N(this.form, e, s);
1543
- for (const o of i)
1544
- if (!(o.condition && !this.checkCondition(o.condition)))
1545
- try {
1546
- let h = null;
1547
- const u = n.value(), V = o.validator;
1548
- o.type === "sync" ? h = V(u, n) : o.type === "async" && (h = await V(u, n)), h && a.push(h);
1549
- } catch (h) {
1550
- m.handle(
1551
- h,
1552
- `ValidationApplicator: validator for ${e}`,
1553
- v.LOG
1554
- );
1555
- }
1556
- a.length > 0 ? s.setErrors(a) : s.errors.value.length > 0 && !s.errors.value.some((o) => o.code !== "contextual") && s.clearErrors();
1557
- }
1558
- }
1559
- /**
1560
- * Применение tree валидаторов (кросс-полевая валидация)
1561
- *
1562
- * Tree валидаторы имеют доступ ко всей форме через TreeValidationContext.
1563
- * Ошибки устанавливаются на targetField (если указано).
1564
- *
1565
- * @param treeValidators Список tree валидаторов
1566
- */
1567
- applyTreeValidators(t) {
1568
- for (const e of t) {
1569
- const i = new I(this.form);
1570
- if (!(e.condition && !this.checkCondition(e.condition)))
1571
- try {
1572
- if (e.type !== "tree")
1573
- continue;
1574
- const s = e.validator(i);
1575
- if (s && e.options && "targetField" in e.options) {
1576
- const a = e.options.targetField;
1577
- if (a) {
1578
- const n = this.form.getFieldByPath(String(a));
1579
- if (n && p(n)) {
1580
- const o = n.errors.value;
1581
- n.setErrors([...o, s]);
1582
- }
1583
- }
1584
- }
1585
- } catch (s) {
1586
- m.handle(s, "ValidationApplicator: tree validator", v.LOG);
1587
- }
1588
- }
1589
- }
1590
- /**
1591
- * Проверка условия (condition) для валидатора
1592
- *
1593
- * Условие определяет, должен ли валидатор выполняться.
1594
- * Использует getFieldByPath для поддержки вложенных путей.
1595
- *
1596
- * @param condition Условие валидатора
1597
- * @returns true, если условие выполнено
1598
- */
1599
- checkCondition(t) {
1600
- const e = this.form.getFieldByPath(t.fieldPath);
1601
- if (!e)
1602
- return !1;
1603
- const i = e.value.value;
1604
- return t.conditionFn(i);
1605
- }
1606
- }
1607
- class D {
1608
- form;
1609
- behaviorRegistry;
1610
- constructor(t, e) {
1611
- this.form = t, this.behaviorRegistry = e;
1612
- }
1613
- /**
1614
- * Применить behavior схему к форме
1615
- *
1616
- * Этапы:
1617
- * 1. Начать регистрацию (beginRegistration)
1618
- * 2. Выполнить схему (регистрация behaviors)
1619
- * 3. Завершить регистрацию (endRegistration) - применить behaviors
1620
- * 4. Вернуть функцию cleanup для отписки
1621
- *
1622
- * @param schemaFn Функция-схема behavior
1623
- * @returns Функция отписки от всех behaviors
1624
- *
1625
- * @example
1626
- * ```typescript
1627
- * const cleanup = behaviorApplicator.apply((path) => {
1628
- * copyFrom(path.residenceAddress, path.registrationAddress, {
1629
- * when: (form) => form.sameAsRegistration === true
1630
- * });
1631
- *
1632
- * enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage');
1633
- *
1634
- * computeFrom(
1635
- * path.initialPayment,
1636
- * [path.propertyValue],
1637
- * (propertyValue) => propertyValue ? propertyValue * 0.2 : null
1638
- * );
1639
- * });
1640
- *
1641
- * // Cleanup при unmount
1642
- * useEffect(() => cleanup, []);
1643
- * ```
1644
- */
1645
- apply(t) {
1646
- this.behaviorRegistry.beginRegistration();
1647
- try {
1648
- const e = w();
1649
- t(e);
1650
- const i = this.form.getProxy();
1651
- return this.behaviorRegistry.endRegistration(i).cleanup;
1652
- } catch (e) {
1653
- throw m.handle(e, "BehaviorApplicator", v.THROW), e;
1654
- }
1655
- }
1656
- }
1657
- class B {
1658
- /**
1659
- * Парсит путь в массив сегментов
1660
- *
1661
- * Поддерживаемые форматы:
1662
- * - Простые пути: "name", "email"
1663
- * - Вложенные пути: "address.city", "user.profile.avatar"
1664
- * - Массивы: "items[0]", "items[0].name", "tags[1][0]"
1665
- * - Комбинации: "orders[0].items[1].price"
1666
- *
1667
- * @param path Путь к полю (строка с точками и квадратными скобками)
1668
- * @returns Массив сегментов пути
1669
- *
1670
- * @example
1671
- * ```typescript
1672
- * navigator.parsePath('email');
1673
- * // [{ key: 'email' }]
1674
- *
1675
- * navigator.parsePath('address.city');
1676
- * // [{ key: 'address' }, { key: 'city' }]
1677
- *
1678
- * navigator.parsePath('items[0].name');
1679
- * // [{ key: 'items', index: 0 }, { key: 'name' }]
1680
- * ```
1681
- */
1682
- parsePath(t) {
1683
- const e = [];
1684
- let i = "", s = !1;
1685
- for (let a = 0; a < t.length; a++) {
1686
- const n = t[a];
1687
- n === "[" ? (s = !0, i += n) : n === "]" ? (s = !1, i += n) : n === "." && !s ? i && (this.addSegment(e, i), i = "") : i += n;
1688
- }
1689
- return i && this.addSegment(e, i), e;
1690
- }
1691
- /**
1692
- * Добавляет сегмент в массив, обрабатывая массивы
1693
- * @private
1694
- */
1695
- addSegment(t, e) {
1696
- const i = e.match(/^(.+)\[(\d+)\]$/);
1697
- i ? t.push({
1698
- key: i[1],
1699
- index: parseInt(i[2], 10)
1700
- }) : t.push({ key: e });
1701
- }
1702
- /**
1703
- * Получает значение по пути из объекта
1704
- *
1705
- * Проходит по всем сегментам пути и возвращает конечное значение.
1706
- * Если путь не найден, возвращает undefined.
1707
- *
1708
- * @param obj Объект для навигации
1709
- * @param path Путь к значению
1710
- * @returns Значение или undefined, если путь не найден
1711
- *
1712
- * @example
1713
- * ```typescript
1714
- * const obj = {
1715
- * email: 'test@mail.com',
1716
- * address: { city: 'Moscow' },
1717
- * items: [{ title: 'Item 1' }]
1718
- * };
1719
- *
1720
- * navigator.getValueByPath(obj, 'email');
1721
- * // 'test@mail.com'
1722
- *
1723
- * navigator.getValueByPath(obj, 'address.city');
1724
- * // 'Moscow'
1725
- *
1726
- * navigator.getValueByPath(obj, 'items[0].title');
1727
- * // 'Item 1'
1728
- *
1729
- * navigator.getValueByPath(obj, 'invalid.path');
1730
- * // undefined
1731
- * ```
1732
- */
1733
- getValueByPath(t, e) {
1734
- const i = this.parsePath(e);
1735
- let s = t;
1736
- for (const a of i) {
1737
- if (s == null) return;
1738
- if (s = s[a.key], a.index !== void 0) {
1739
- if (!Array.isArray(s)) return;
1740
- s = s[a.index];
1741
- }
1742
- }
1743
- return s;
1744
- }
1745
- /**
1746
- * Устанавливает значение по пути в объекте (мутирует объект)
1747
- *
1748
- * Создает промежуточные объекты, если они не существуют.
1749
- * Выбрасывает ошибку, если ожидается массив, но его нет.
1750
- *
1751
- * @param obj Объект для модификации
1752
- * @param path Путь к значению
1753
- * @param value Новое значение
1754
- *
1755
- * @throws {Error} Если ожидается массив по пути, но его нет
1756
- *
1757
- * @example
1758
- * ```typescript
1759
- * const obj = { address: { city: '' } };
1760
- * navigator.setValueByPath(obj, 'address.city', 'Moscow');
1761
- * // obj.address.city === 'Moscow'
1762
- *
1763
- * const obj2: UnknownRecord = {};
1764
- * navigator.setValueByPath(obj2, 'address.city', 'Moscow');
1765
- * // Создаст { address: { city: 'Moscow' } }
1766
- *
1767
- * const obj3 = { items: [{ title: 'Old' }] };
1768
- * navigator.setValueByPath(obj3, 'items[0].title', 'New');
1769
- * // obj3.items[0].title === 'New'
1770
- * ```
1771
- */
1772
- setValueByPath(t, e, i) {
1773
- const s = this.parsePath(e);
1774
- if (s.length === 0)
1775
- throw new Error("Cannot set value: empty path");
1776
- let a = t;
1777
- for (let o = 0; o < s.length - 1; o++) {
1778
- const h = s[o];
1779
- let u = a[h.key];
1780
- if (h.index !== void 0) {
1781
- if (!Array.isArray(u))
1782
- throw new Error(`Expected array at path segment: ${h.key}, but got ${typeof u}`);
1783
- a = u[h.index];
1784
- } else
1785
- u == null && (a[h.key] = {}, u = a[h.key]), a = u;
1786
- }
1787
- const n = s[s.length - 1];
1788
- if (n.index !== void 0) {
1789
- const o = a[n.key];
1790
- if (!Array.isArray(o))
1791
- throw new Error(
1792
- `Expected array at path segment: ${n.key}, but got ${typeof o}`
1793
- );
1794
- o[n.index] = i;
1795
- } else
1796
- a[n.key] = i;
1797
- }
1798
- /**
1799
- * Получить значение из FormNode по пути
1800
- *
1801
- * Автоматически извлекает значение из FormNode (через .value.value).
1802
- * Используется в ValidationContext и BehaviorContext для единообразного
1803
- * доступа к значениям полей формы.
1804
- *
1805
- * @param form Корневой узел формы (обычно GroupNode)
1806
- * @param path Путь к полю
1807
- * @returns Значение поля или undefined, если путь не найден
1808
- *
1809
- * @example
1810
- * ```typescript
1811
- * const form = new GroupNode({
1812
- * email: { value: 'test@mail.com', component: Input },
1813
- * address: {
1814
- * city: { value: 'Moscow', component: Input }
1815
- * },
1816
- * items: [{ title: { value: 'Item 1', component: Input } }]
1817
- * });
1818
- *
1819
- * navigator.getFormNodeValue(form, 'email');
1820
- * // 'test@mail.com'
1821
- *
1822
- * navigator.getFormNodeValue(form, 'address.city');
1823
- * // 'Moscow'
1824
- *
1825
- * navigator.getFormNodeValue(form, 'items[0].title');
1826
- * // 'Item 1'
1827
- *
1828
- * navigator.getFormNodeValue(form, 'invalid.path');
1829
- * // undefined
1830
- * ```
1831
- */
1832
- getFormNodeValue(t, e) {
1833
- const i = this.getNodeByPath(t, e);
1834
- if (i != null)
1835
- return this.isFormNode(i) ? i.value.value : i;
1836
- }
1837
- /**
1838
- * Type guard для проверки, является ли объект FormNode
1839
- *
1840
- * Проверяет наличие характерных свойств FormNode:
1841
- * - value (Signal)
1842
- * - value.value (значение Signal)
1843
- *
1844
- * @param obj Объект для проверки
1845
- * @returns true, если объект является FormNode
1846
- * @private
1847
- */
1848
- isFormNode(t) {
1849
- return t != null && typeof t == "object" && "value" in t && typeof t.value == "object" && t.value != null && "value" in t.value;
1850
- }
1851
- /**
1852
- * Получает узел формы по пути
1853
- *
1854
- * Навигирует по структуре FormNode (GroupNode/FieldNode/ArrayNode)
1855
- * и возвращает узел по указанному пути.
1856
- *
1857
- * Поддерживает:
1858
- * - Доступ к полям GroupNode через fields Map
1859
- * - Доступ к элементам ArrayNode через индекс
1860
- * - Proxy-доступ к полям (для обратной совместимости)
1861
- *
1862
- * @param form Корневой узел формы (обычно GroupNode)
1863
- * @param path Путь к узлу
1864
- * @returns Узел формы или null, если путь не найден
1865
- *
1866
- * @example
1867
- * ```typescript
1868
- * const form = new GroupNode({
1869
- * email: { value: '', component: Input },
1870
- * address: {
1871
- * city: { value: '', component: Input }
1872
- * },
1873
- * items: [{ title: { value: '', component: Input } }]
1874
- * });
1875
- *
1876
- * const emailNode = navigator.getNodeByPath(form, 'email');
1877
- * // FieldNode
1878
- *
1879
- * const cityNode = navigator.getNodeByPath(form, 'address.city');
1880
- * // FieldNode
1881
- *
1882
- * const itemNode = navigator.getNodeByPath(form, 'items[0]');
1883
- * // GroupNode
1884
- *
1885
- * const titleNode = navigator.getNodeByPath(form, 'items[0].title');
1886
- * // FieldNode
1887
- *
1888
- * const invalidNode = navigator.getNodeByPath(form, 'invalid.path');
1889
- * // null
1890
- * ```
1891
- */
1892
- getNodeByPath(t, e) {
1893
- const i = this.parsePath(e);
1894
- let s = t;
1895
- for (const a of i) {
1896
- if (s == null) return null;
1897
- const n = s;
1898
- if (n.fields && n.fields instanceof Map) {
1899
- if (s = n.fields.get(a.key), a.index === void 0) {
1900
- if (s == null) return null;
1901
- continue;
1902
- }
1903
- } else if (a.index !== void 0 && n.items) {
1904
- const o = n.items.value || n.items;
1905
- if (!Array.isArray(o) || (s = o[a.index], s == null)) return null;
1906
- continue;
1907
- } else if (a.index === void 0) {
1908
- if (s = n[a.key], s == null) return null;
1909
- continue;
1910
- }
1911
- if (s && a.index !== void 0 && s.items) {
1912
- const o = s.items.value || s.items;
1913
- if (!Array.isArray(o)) return null;
1914
- s = o[a.index];
1915
- } else if (s && a.index !== void 0 && !s.items)
1916
- return null;
1917
- if (s == null) return null;
1918
- }
1919
- return s;
1920
- }
1921
- }
1922
- class O {
1923
- /**
1924
- * Внутреннее хранилище полей
1925
- * Map обеспечивает быструю lookup производительность O(1)
1926
- */
1927
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1928
- fields = /* @__PURE__ */ new Map();
1929
- /**
1930
- * Установить поле в реестр
1931
- *
1932
- * @param key - Ключ поля (имя свойства в типе T)
1933
- * @param node - FormNode для этого поля
1934
- *
1935
- * @example
1936
- * ```typescript
1937
- * registry.set('email', new FieldNode({ value: '' }));
1938
- * ```
1939
- */
1940
- set(t, e) {
1941
- this.fields.set(t, e);
1942
- }
1943
- /**
1944
- * Получить поле из реестра
1945
- *
1946
- * @param key - Ключ поля
1947
- * @returns FormNode или undefined, если поле не найдено
1948
- *
1949
- * @example
1950
- * ```typescript
1951
- * const emailField = registry.get('email');
1952
- * if (emailField) {
1953
- * console.log(emailField.value.value);
1954
- * }
1955
- * ```
1956
- */
1957
- get(t) {
1958
- return this.fields.get(t);
1959
- }
1960
- /**
1961
- * Проверить наличие поля в реестре
1962
- *
1963
- * @param key - Ключ поля
1964
- * @returns true если поле существует
1965
- *
1966
- * @example
1967
- * ```typescript
1968
- * if (registry.has('email')) {
1969
- * console.log('Email field exists');
1970
- * }
1971
- * ```
1972
- */
1973
- has(t) {
1974
- return this.fields.has(t);
1975
- }
1976
- /**
1977
- * Удалить поле из реестра
1978
- *
1979
- * @param key - Ключ поля
1980
- * @returns true если поле было удалено, false если поля не было
1981
- *
1982
- * @example
1983
- * ```typescript
1984
- * registry.delete('email');
1985
- * ```
1986
- */
1987
- delete(t) {
1988
- return this.fields.delete(t);
1989
- }
1990
- /**
1991
- * Перебрать все поля
1992
- *
1993
- * @param callback - Функция обратного вызова для каждого поля
1994
- *
1995
- * @example
1996
- * ```typescript
1997
- * registry.forEach((field, key) => {
1998
- * console.log(`${key}: ${field.value.value}`);
1999
- * });
2000
- * ```
2001
- */
2002
- forEach(t) {
2003
- this.fields.forEach(t);
2004
- }
2005
- /**
2006
- * Получить итератор значений (полей)
2007
- *
2008
- * @returns Итератор по всем полям
2009
- *
2010
- * @example
2011
- * ```typescript
2012
- * for (const field of registry.values()) {
2013
- * await field.validate();
2014
- * }
2015
- * ```
2016
- */
2017
- values() {
2018
- return this.fields.values();
2019
- }
2020
- /**
2021
- * Получить итератор пар [ключ, значение]
2022
- *
2023
- * @returns Итератор по всем записям
2024
- *
2025
- * @example
2026
- * ```typescript
2027
- * for (const [key, field] of registry.entries()) {
2028
- * console.log(key, field.value.value);
2029
- * }
2030
- * ```
2031
- */
2032
- entries() {
2033
- return this.fields.entries();
2034
- }
2035
- /**
2036
- * Получить итератор ключей полей
2037
- *
2038
- * @returns Итератор по всем ключам
2039
- *
2040
- * @example
2041
- * ```typescript
2042
- * const fieldNames = Array.from(registry.keys());
2043
- * // ['email', 'name', 'age']
2044
- * ```
2045
- */
2046
- keys() {
2047
- return this.fields.keys();
2048
- }
2049
- /**
2050
- * Получить количество полей
2051
- *
2052
- * @returns Количество зарегистрированных полей
2053
- *
2054
- * @example
2055
- * ```typescript
2056
- * console.log(`Form has ${registry.size()} fields`);
2057
- * ```
2058
- */
2059
- size() {
2060
- return this.fields.size;
2061
- }
2062
- /**
2063
- * Очистить все поля
2064
- *
2065
- * Удаляет все поля из реестра
2066
- *
2067
- * @example
2068
- * ```typescript
2069
- * registry.clear();
2070
- * console.log(registry.size()); // 0
2071
- * ```
2072
- */
2073
- clear() {
2074
- this.fields.clear();
2075
- }
2076
- /**
2077
- * Получить все поля как массив
2078
- *
2079
- * Полезно для операций, требующих работу с массивом
2080
- *
2081
- * @returns Массив всех полей
2082
- *
2083
- * @example
2084
- * ```typescript
2085
- * const allValid = registry.toArray().every(field => field.valid.value);
2086
- * ```
2087
- */
2088
- toArray() {
2089
- return Array.from(this.fields.values());
2090
- }
2091
- /**
2092
- * Получить Map-представление реестра (readonly)
2093
- *
2094
- * Используйте для совместимости с существующим кодом
2095
- *
2096
- * @returns ReadonlyMap с полями
2097
- * @internal
2098
- *
2099
- * @example
2100
- * ```typescript
2101
- * const mapView = registry.asMap();
2102
- * ```
2103
- */
2104
- asMap() {
2105
- return this.fields;
2106
- }
2107
- }
2108
- class C {
2109
- /**
2110
- * @param fieldRegistry - Реестр полей для доступа к коллекции
2111
- */
2112
- constructor(t) {
2113
- this.fieldRegistry = t;
2114
- }
2115
- /**
2116
- * Создать Proxy для GroupNode
2117
- *
2118
- * Proxy позволяет обращаться к полям формы напрямую:
2119
- * - form.email вместо form.fields.get('email')
2120
- * - form.address.city вместо form.fields.get('address').fields.get('city')
2121
- *
2122
- * @param target - GroupNode для которого создается Proxy
2123
- * @returns Proxy с типобезопасным доступом к полям
2124
- *
2125
- * @example
2126
- * ```typescript
2127
- * const proxy = proxyBuilder.build(groupNode);
2128
- *
2129
- * // Доступ к полям
2130
- * console.log(proxy.email.value); // Работает!
2131
- * console.log(proxy.name.value); // Работает!
2132
- *
2133
- * // Доступ к методам GroupNode
2134
- * await proxy.validate(); // Работает!
2135
- * proxy.markAsTouched(); // Работает!
2136
- *
2137
- * // Проверка существования
2138
- * if ('email' in proxy) { ... }
2139
- *
2140
- * // Перечисление ключей
2141
- * Object.keys(proxy); // ['email', 'name', ...]
2142
- * ```
2143
- */
2144
- build(t) {
2145
- return new Proxy(t, {
2146
- /**
2147
- * Get trap: Перехват доступа к свойствам
2148
- *
2149
- * Приоритет:
2150
- * 1. Собственные свойства и методы GroupNode (validate, setValue и т.д.)
2151
- * 2. Поля формы из fieldRegistry
2152
- * 3. undefined для несуществующих свойств
2153
- */
2154
- get: (e, i) => {
2155
- if (i in e)
2156
- return e[i];
2157
- if (typeof i == "string" && this.fieldRegistry.has(i))
2158
- return this.fieldRegistry.get(i);
2159
- },
2160
- /**
2161
- * Set trap: Перехват установки свойств
2162
- *
2163
- * Запрещает прямую установку значений полей через form.email = value
2164
- * Пользователь должен использовать form.email.setValue(value) или form.setValue({...})
2165
- */
2166
- set: (e, i, s) => typeof i == "string" && this.fieldRegistry.has(i) ? !1 : (e[i] = s, !0),
2167
- /**
2168
- * Has trap: Перехват оператора 'in'
2169
- *
2170
- * Позволяет проверять существование полей:
2171
- * if ('email' in form) { ... }
2172
- */
2173
- has: (e, i) => typeof i == "string" && this.fieldRegistry.has(i) ? !0 : i in e,
2174
- /**
2175
- * OwnKeys trap: Перехват Object.keys() / Object.getOwnPropertyNames()
2176
- *
2177
- * Возвращает объединенный список:
2178
- * - Ключей самого GroupNode
2179
- * - Ключей полей из fieldRegistry
2180
- */
2181
- ownKeys: (e) => {
2182
- const i = Reflect.ownKeys(e), s = Array.from(this.fieldRegistry.keys());
2183
- return [.../* @__PURE__ */ new Set([...i, ...s])];
2184
- },
2185
- /**
2186
- * GetOwnPropertyDescriptor trap: Перехват Object.getOwnPropertyDescriptor()
2187
- *
2188
- * Возвращает дескриптор свойства для полей и свойств GroupNode
2189
- * Важно для корректной работы Object.keys() и других рефлексивных операций
2190
- */
2191
- getOwnPropertyDescriptor: (e, i) => typeof i == "string" && this.fieldRegistry.has(i) ? {
2192
- enumerable: !0,
2193
- // Поле должно быть перечисляемым
2194
- configurable: !0
2195
- // Поле может быть удалено
2196
- // Не указываем writable, т.к. это accessor property через get/set traps
2197
- } : Reflect.getOwnPropertyDescriptor(e, i)
2198
- });
2199
- }
2200
- }
2201
- class $ {
2202
- // ============================================================================
2203
- // Конструктор
2204
- // ============================================================================
2205
- /**
2206
- * Создать менеджер состояния
2207
- *
2208
- * @param fieldRegistry - реестр полей формы
2209
- */
2210
- constructor(t) {
2211
- this.fieldRegistry = t, this._submitting = d(!1), this._disabled = d(!1), this._formErrors = d([]), this.value = l(() => {
2212
- const e = {};
2213
- return this.fieldRegistry.forEach((i, s) => {
2214
- e[s] = i.value.value;
2215
- }), e;
2216
- }), 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(
2217
- () => Array.from(this.fieldRegistry.values()).some((e) => e.pending.value)
2218
- ), this.touched = l(
2219
- () => Array.from(this.fieldRegistry.values()).some((e) => e.touched.value)
2220
- ), this.dirty = l(
2221
- () => Array.from(this.fieldRegistry.values()).some((e) => e.dirty.value)
2222
- ), this.errors = l(() => {
2223
- const e = [];
2224
- return e.push(...this._formErrors.value), this.fieldRegistry.forEach((i) => {
2225
- e.push(...i.errors.value);
2226
- }), e;
2227
- }), this.status = l(() => this._disabled.value ? "disabled" : this.pending.value ? "pending" : this.invalid.value ? "invalid" : "valid"), this.submitting = l(() => this._submitting.value);
2228
- }
2229
- // ============================================================================
2230
- // Приватные сигналы (мутабельные)
2231
- // ============================================================================
2232
- /**
2233
- * Флаг отправки формы
2234
- * Устанавливается в true во время отправки формы на сервер
2235
- */
2236
- _submitting;
2237
- /**
2238
- * Флаг disabled состояния
2239
- * Если true, форма считается disabled
2240
- */
2241
- _disabled;
2242
- /**
2243
- * Form-level validation errors (не связанные с конкретным полем)
2244
- * Используется для server-side errors или кросс-полевой валидации
2245
- */
2246
- _formErrors;
2247
- // ============================================================================
2248
- // Публичные computed signals (read-only)
2249
- // ============================================================================
2250
- /**
2251
- * Значение формы как объект
2252
- *
2253
- * Computed signal, который автоматически пересчитывается при изменении любого поля.
2254
- * Использует мемоизацию - если зависимости не изменились, вернет закешированный объект.
2255
- *
2256
- * @example
2257
- * ```typescript
2258
- * const form = new GroupNode({ email: { value: 'test@mail.com' } });
2259
- * console.log(form.value.value); // { email: 'test@mail.com' }
2260
- * ```
2261
- */
2262
- value;
2263
- /**
2264
- * Форма валидна?
2265
- *
2266
- * Computed signal. Форма валидна, если:
2267
- * - Нет form-level errors
2268
- * - Все поля валидны
2269
- */
2270
- valid;
2271
- /**
2272
- * Форма невалидна?
2273
- *
2274
- * Computed signal. Инверсия valid.
2275
- */
2276
- invalid;
2277
- /**
2278
- * Хотя бы одно поле touched?
2279
- *
2280
- * Computed signal. Возвращает true, если хотя бы одно поле было touched.
2281
- */
2282
- touched;
2283
- /**
2284
- * Хотя бы одно поле dirty?
2285
- *
2286
- * Computed signal. Возвращает true, если хотя бы одно поле изменилось.
2287
- */
2288
- dirty;
2289
- /**
2290
- * Асинхронная валидация в процессе?
2291
- *
2292
- * Computed signal. Возвращает true, если хотя бы одно поле находится в pending состоянии.
2293
- */
2294
- pending;
2295
- /**
2296
- * Все ошибки валидации
2297
- *
2298
- * Computed signal. Возвращает массив всех ошибок:
2299
- * - Form-level errors
2300
- * - Field-level errors (из всех вложенных полей)
2301
- */
2302
- errors;
2303
- /**
2304
- * Общий статус формы
2305
- *
2306
- * Computed signal. Возможные значения:
2307
- * - 'disabled' - форма disabled
2308
- * - 'pending' - асинхронная валидация в процессе
2309
- * - 'invalid' - форма невалидна
2310
- * - 'valid' - форма валидна
2311
- */
2312
- status;
2313
- /**
2314
- * Форма в процессе отправки?
2315
- *
2316
- * Computed signal (обертка над _submitting для read-only доступа).
2317
- */
2318
- submitting;
2319
- // ============================================================================
2320
- // Публичные методы для управления состоянием
2321
- // ============================================================================
2322
- /**
2323
- * Установить form-level ошибки
2324
- *
2325
- * @param errors - массив ошибок валидации
2326
- *
2327
- * @example
2328
- * ```typescript
2329
- * // Server-side ошибки
2330
- * stateManager.setFormErrors([
2331
- * { code: 'server_error', message: 'Пользователь с таким email уже существует' }
2332
- * ]);
2333
- * ```
2334
- */
2335
- setFormErrors(t) {
2336
- this._formErrors.value = t;
2337
- }
2338
- /**
2339
- * Очистить form-level ошибки
2340
- */
2341
- clearFormErrors() {
2342
- this._formErrors.value = [];
2343
- }
2344
- /**
2345
- * Получить form-level ошибки
2346
- */
2347
- getFormErrors() {
2348
- return this._formErrors.value;
2349
- }
2350
- /**
2351
- * Установить флаг submitting
2352
- *
2353
- * @param value - true если форма отправляется, false если нет
2354
- *
2355
- * @example
2356
- * ```typescript
2357
- * stateManager.setSubmitting(true);
2358
- * await api.submitForm(form.getValue());
2359
- * stateManager.setSubmitting(false);
2360
- * ```
2361
- */
2362
- setSubmitting(t) {
2363
- this._submitting.value = t;
2364
- }
2365
- /**
2366
- * Установить флаг disabled
2367
- *
2368
- * @param value - true если форма disabled, false если нет
2369
- */
2370
- setDisabled(t) {
2371
- this._disabled.value = t;
2372
- }
2373
- /**
2374
- * Получить флаг disabled
2375
- */
2376
- isDisabled() {
2377
- return this._disabled.value;
2378
- }
2379
- }
2380
- class y extends g {
2381
- // ============================================================================
2382
- // Приватные поля
2383
- // ============================================================================
2384
- id = k();
2385
- /**
2386
- * Реестр полей формы
2387
- * Использует FieldRegistry для инкапсуляции логики управления коллекцией полей
2388
- */
2389
- fieldRegistry;
2390
- /**
2391
- * Строитель Proxy для типобезопасного доступа к полям
2392
- * Использует ProxyBuilder для создания Proxy с расширенной функциональностью
2393
- */
2394
- proxyBuilder;
2395
- /**
2396
- * Менеджер состояния формы
2397
- * Инкапсулирует всю логику создания и управления сигналами состояния
2398
- * Извлечен из GroupNode для соблюдения SRP
2399
- */
2400
- stateManager;
2401
- /**
2402
- * Менеджер подписок для централизованного cleanup
2403
- * Использует SubscriptionManager вместо массива для управления подписками
2404
- */
2405
- disposers = new _();
2406
- /**
2407
- * Ссылка на Proxy-инстанс для использования в BehaviorContext
2408
- * Устанавливается в конструкторе до применения behavior schema
2409
- */
2410
- _proxyInstance;
2411
- /**
2412
- * Навигатор для работы с путями к полям
2413
- * Использует композицию вместо дублирования логики парсинга путей
2414
- */
2415
- pathNavigator = new B();
2416
- /**
2417
- * Фабрика для создания узлов формы
2418
- * Использует композицию для централизованного создания FieldNode/GroupNode/ArrayNode
2419
- */
2420
- nodeFactory = new G();
2421
- /**
2422
- * Реестр валидаторов для этой формы
2423
- * Использует композицию вместо глобального Singleton
2424
- * Обеспечивает полную изоляцию форм друг от друга
2425
- */
2426
- validationRegistry = new A();
2427
- /**
2428
- * Реестр behaviors для этой формы
2429
- * Использует композицию вместо глобального Singleton
2430
- * Обеспечивает полную изоляцию форм друг от друга
2431
- */
2432
- behaviorRegistry = new F();
2433
- /**
2434
- * Аппликатор для применения валидаторов к форме
2435
- * Извлечен из GroupNode для соблюдения SRP
2436
- * Использует композицию для управления процессом валидации
2437
- */
2438
- validationApplicator = new T(this);
2439
- /**
2440
- * Аппликатор для применения behavior схемы к форме
2441
- * Извлечен из GroupNode для соблюдения SRP
2442
- * Использует композицию для управления процессом применения behaviors
2443
- */
2444
- behaviorApplicator = new D(this, this.behaviorRegistry);
2445
- // ============================================================================
2446
- // Публичные computed signals (делегированы в StateManager)
2447
- // ============================================================================
2448
- value;
2449
- valid;
2450
- invalid;
2451
- touched;
2452
- dirty;
2453
- pending;
2454
- errors;
2455
- status;
2456
- submitting;
2457
- constructor(t) {
2458
- super(), this.fieldRegistry = new O(), this.proxyBuilder = new C(this.fieldRegistry);
2459
- const e = "form" in t, i = e ? t.form : t, s = e ? t.behavior : void 0, a = e ? t.validation : void 0;
2460
- for (const [o, h] of Object.entries(i)) {
2461
- const u = this.createNode(h);
2462
- this.fieldRegistry.set(o, u);
2463
- }
2464
- this.stateManager = new $(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;
2465
- const n = this.proxyBuilder.build(this);
2466
- return this._proxyInstance = n, s && this.applyBehaviorSchema(s), a && this.applyValidationSchema(a), n;
2467
- }
2468
- // ============================================================================
2469
- // Реализация абстрактных методов FormNode
2470
- // ============================================================================
2471
- getValue() {
2472
- const t = {};
2473
- return this.fieldRegistry.forEach((e, i) => {
2474
- t[i] = e.getValue();
2475
- }), t;
2476
- }
2477
- setValue(t, e) {
2478
- for (const [i, s] of Object.entries(t)) {
2479
- const a = this.fieldRegistry.get(i);
2480
- a && a.setValue(s, e);
2481
- }
2482
- }
2483
- patchValue(t) {
2484
- for (const [e, i] of Object.entries(t)) {
2485
- const s = this.fieldRegistry.get(e);
2486
- s && i !== void 0 && s.setValue(i);
2487
- }
2488
- }
2489
- /**
2490
- * Сбросить форму к указанным значениям (или к initialValues)
2491
- *
2492
- * @param value - опциональный объект со значениями для сброса
2493
- *
2494
- * @remarks
2495
- * Рекурсивно вызывает reset() для всех полей формы
2496
- *
2497
- * @example
2498
- * ```typescript
2499
- * // Сброс к initialValues
2500
- * form.reset();
2501
- *
2502
- * // Сброс к новым значениям
2503
- * form.reset({ email: 'new@mail.com', password: '' });
2504
- * ```
2505
- */
2506
- reset(t) {
2507
- this.fieldRegistry.forEach((e, i) => {
2508
- const s = t?.[i];
2509
- e.reset(s);
2510
- });
2511
- }
2512
- /**
2513
- * Сбросить форму к исходным значениям (initialValues)
2514
- *
2515
- * @remarks
2516
- * Рекурсивно вызывает resetToInitial() для всех полей формы.
2517
- * Более явный способ сброса к начальным значениям по сравнению с reset()
2518
- *
2519
- * Полезно когда:
2520
- * - Пользователь нажал "Cancel" - полная отмена изменений
2521
- * - Форма была изменена через reset(newValues), но нужно вернуться к самому началу
2522
- * - Явное намерение показать "отмена всех изменений"
2523
- *
2524
- * @example
2525
- * ```typescript
2526
- * const form = new GroupNode({
2527
- * email: { value: 'initial@mail.com', component: Input },
2528
- * name: { value: 'John', component: Input }
2529
- * });
2530
- *
2531
- * form.email.setValue('changed@mail.com');
2532
- * form.reset({ email: 'temp@mail.com', name: 'Jane' });
2533
- * console.log(form.getValue()); // { email: 'temp@mail.com', name: 'Jane' }
2534
- *
2535
- * form.resetToInitial();
2536
- * console.log(form.getValue()); // { email: 'initial@mail.com', name: 'John' }
2537
- * ```
2538
- */
2539
- resetToInitial() {
2540
- this.fieldRegistry.forEach((t) => {
2541
- "resetToInitial" in t && typeof t.resetToInitial == "function" ? t.resetToInitial() : t.reset();
2542
- });
2543
- }
2544
- async validate() {
2545
- this.clearErrors(), await Promise.all(Array.from(this.fieldRegistry.values()).map((e) => e.validate()));
2546
- const t = this.validationRegistry.getValidators();
2547
- return t && t.length > 0 && await this.applyContextualValidators(t), Array.from(this.fieldRegistry.values()).every((e) => e.valid.value);
2548
- }
2549
- /**
2550
- * Установить form-level validation errors
2551
- * Используется для server-side validation или кросс-полевых ошибок
2552
- *
2553
- * @param errors - массив ошибок уровня формы
2554
- *
2555
- * @example
2556
- * ```typescript
2557
- * // Server-side validation после submit
2558
- * try {
2559
- * await api.createUser(form.getValue());
2560
- * } catch (error) {
2561
- * form.setErrors([
2562
- * { code: 'duplicate_email', message: 'Email уже используется' }
2563
- * ]);
2564
- * }
2565
- * ```
2566
- */
2567
- setErrors(t) {
2568
- this.stateManager.setFormErrors(t);
2569
- }
2570
- /**
2571
- * Очистить все errors (form-level + field-level)
2572
- */
2573
- clearErrors() {
2574
- this.stateManager.clearFormErrors(), this.fieldRegistry.forEach((t) => t.clearErrors());
2575
- }
2576
- /**
2577
- * Получить поле по ключу
2578
- *
2579
- * Публичный метод для доступа к полю из fieldRegistry
2580
- *
2581
- * @param key - Ключ поля
2582
- * @returns FormNode или undefined, если поле не найдено
2583
- *
2584
- * @example
2585
- * ```typescript
2586
- * const emailField = form.getField('email');
2587
- * if (emailField) {
2588
- * console.log(emailField.value.value);
2589
- * }
2590
- * ```
2591
- */
2592
- getField(t) {
2593
- return this.fieldRegistry.get(t);
2594
- }
2595
- /**
2596
- * Получить Map всех полей формы
2597
- *
2598
- * Используется в FieldPathNavigator для навигации по полям
2599
- *
2600
- * @returns Map полей формы
2601
- */
2602
- get fields() {
2603
- return this.fieldRegistry;
2604
- }
2605
- /**
2606
- * Получить Proxy-инстанс для прямого доступа к полям
2607
- *
2608
- * Proxy позволяет обращаться к полям формы напрямую через точечную нотацию:
2609
- * - form.email вместо form.fields.get('email')
2610
- * - form.address.city вместо form.fields.get('address').fields.get('city')
2611
- *
2612
- * Используется в:
2613
- * - BehaviorApplicator для доступа к полям в behavior functions
2614
- * - ValidationApplicator для доступа к форме в tree validators
2615
- *
2616
- * @returns Proxy-инстанс с типобезопасным доступом к полям или сама форма, если proxy не доступен
2617
- *
2618
- * @example
2619
- * ```typescript
2620
- * const form = new GroupNode({
2621
- * controls: {
2622
- * email: new FieldNode({ value: '' }),
2623
- * name: new FieldNode({ value: '' })
2624
- * }
2625
- * });
2626
- *
2627
- * const proxy = form.getProxy();
2628
- * console.log(proxy.email.value); // Прямой доступ к полю
2629
- * ```
2630
- */
2631
- getProxy() {
2632
- return this._proxyInstance || this;
2633
- }
2634
- /**
2635
- * Получить все поля формы как итератор
2636
- *
2637
- * Предоставляет доступ к внутренним полям для валидации и других операций
2638
- *
2639
- * @returns Итератор по всем полям формы
2640
- *
2641
- * @example
2642
- * ```typescript
2643
- * // Валидация всех полей
2644
- * await Promise.all(
2645
- * Array.from(form.getAllFields()).map(field => field.validate())
2646
- * );
2647
- * ```
2648
- */
2649
- getAllFields() {
2650
- return this.fieldRegistry.values();
2651
- }
2652
- // ============================================================================
2653
- // Protected hooks (Template Method pattern)
2654
- // ============================================================================
2655
- /**
2656
- * Hook: вызывается после markAsTouched()
2657
- *
2658
- * Для GroupNode: рекурсивно помечаем все дочерние поля как touched
2659
- */
2660
- onMarkAsTouched() {
2661
- this.fieldRegistry.forEach((t) => t.markAsTouched());
2662
- }
2663
- /**
2664
- * Hook: вызывается после markAsUntouched()
2665
- *
2666
- * Для GroupNode: рекурсивно помечаем все дочерние поля как untouched
2667
- */
2668
- onMarkAsUntouched() {
2669
- this.fieldRegistry.forEach((t) => t.markAsUntouched());
2670
- }
2671
- /**
2672
- * Hook: вызывается после markAsDirty()
2673
- *
2674
- * Для GroupNode: рекурсивно помечаем все дочерние поля как dirty
2675
- */
2676
- onMarkAsDirty() {
2677
- this.fieldRegistry.forEach((t) => t.markAsDirty());
2678
- }
2679
- /**
2680
- * Hook: вызывается после markAsPristine()
2681
- *
2682
- * Для GroupNode: рекурсивно помечаем все дочерние поля как pristine
2683
- */
2684
- onMarkAsPristine() {
2685
- this.fieldRegistry.forEach((t) => t.markAsPristine());
2686
- }
2687
- // ============================================================================
2688
- // Дополнительные методы (из FormStore)
2689
- // ============================================================================
2690
- /**
2691
- * Отправить форму
2692
- * Валидирует форму и вызывает onSubmit если форма валидна
2693
- */
2694
- async submit(t) {
2695
- if (this.markAsTouched(), !await this.validate())
2696
- return null;
2697
- this.stateManager.setSubmitting(!0);
2698
- try {
2699
- return await t(this.getValue());
2700
- } finally {
2701
- this.stateManager.setSubmitting(!1);
2702
- }
2703
- }
2704
- /**
2705
- * Применить validation schema к форме
2706
- *
2707
- * Использует локальный реестр валидаторов (this.validationRegistry)
2708
- * вместо глобального Singleton для изоляции форм друг от друга.
2709
- */
2710
- applyValidationSchema(t) {
2711
- this.validationRegistry.beginRegistration();
2712
- try {
2713
- const e = S();
2714
- t(e);
2715
- const i = this.getProxy();
2716
- this.validationRegistry.endRegistration(i);
2717
- } catch (e) {
2718
- throw console.error("Error applying validation schema:", e), e;
2719
- }
2720
- }
2721
- /**
2722
- * Применить behavior schema к форме
2723
- *
2724
- * ✅ РЕФАКТОРИНГ: Делегирование BehaviorApplicator (SRP)
2725
- *
2726
- * Логика применения behavior схемы извлечена в BehaviorApplicator для:
2727
- * - Соблюдения Single Responsibility Principle
2728
- * - Уменьшения размера GroupNode (~50 строк)
2729
- * - Улучшения тестируемости
2730
- * - Консистентности с ValidationApplicator
2731
- *
2732
- * @param schemaFn Функция описания поведения формы
2733
- * @returns Функция cleanup для отписки от всех behaviors
2734
- *
2735
- * @example
2736
- * ```typescript
2737
- * import { copyFrom, enableWhen, computeFrom } from '@/lib/forms/core/behaviors';
2738
- *
2739
- * const behaviorSchema: BehaviorSchemaFn<MyForm> = (path) => {
2740
- * copyFrom(path.residenceAddress, path.registrationAddress, {
2741
- * when: (form) => form.sameAsRegistration === true
2742
- * });
2743
- *
2744
- * enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage');
2745
- *
2746
- * computeFrom(
2747
- * path.initialPayment,
2748
- * [path.propertyValue],
2749
- * (propertyValue) => propertyValue ? propertyValue * 0.2 : null
2750
- * );
2751
- * };
2752
- *
2753
- * const cleanup = form.applyBehaviorSchema(behaviorSchema);
2754
- *
2755
- * // Cleanup при unmount
2756
- * useEffect(() => cleanup, []);
2757
- * ```
2758
- */
2759
- applyBehaviorSchema(t) {
2760
- return this.behaviorApplicator.apply(t);
2761
- }
2762
- /**
2763
- * Получить вложенное поле по пути
2764
- *
2765
- * Поддерживаемые форматы путей:
2766
- * - Simple: "email" - получить поле верхнего уровня
2767
- * - Nested: "address.city" - получить вложенное поле
2768
- * - Array index: "items[0]" - получить элемент массива по индексу
2769
- * - Combined: "items[0].name" - получить поле элемента массива
2770
- *
2771
- * @param path - Путь к полю
2772
- * @returns FormNode если найдено, undefined если путь не существует
2773
- *
2774
- * @example
2775
- * ```typescript
2776
- * const form = new GroupNode({
2777
- * email: { value: '', component: Input },
2778
- * address: {
2779
- * city: { value: '', component: Input }
2780
- * },
2781
- * items: [{ name: { value: '', component: Input } }]
2782
- * });
2783
- *
2784
- * form.getFieldByPath('email'); // FieldNode
2785
- * form.getFieldByPath('address.city'); // FieldNode
2786
- * form.getFieldByPath('items[0]'); // GroupNode
2787
- * form.getFieldByPath('items[0].name'); // FieldNode
2788
- * form.getFieldByPath('invalid.path'); // undefined
2789
- * ```
2790
- */
2791
- getFieldByPath(t) {
2792
- if (t.startsWith(".") || t.endsWith("."))
2793
- return;
2794
- const e = this.pathNavigator.parsePath(t);
2795
- if (e.length === 0)
2796
- return;
2797
- let i = this;
2798
- for (const s of e) {
2799
- if (!(i instanceof y) || (i = i.getField(s.key), !i)) return;
2800
- if (s.index !== void 0)
2801
- if ("at" in i && "length" in i && typeof i.at == "function") {
2802
- const a = i.at(s.index);
2803
- if (!a) return;
2804
- i = a;
2805
- } else
2806
- return;
2807
- }
2808
- return i;
2809
- }
2810
- /**
2811
- * Применить contextual валидаторы к полям
2812
- *
2813
- * ✅ РЕФАКТОРИНГ: Делегирование ValidationApplicator (SRP)
2814
- *
2815
- * Логика применения валидаторов извлечена в ValidationApplicator для:
2816
- * - Соблюдения Single Responsibility Principle
2817
- * - Уменьшения размера GroupNode (~120 строк)
2818
- * - Улучшения тестируемости
2819
- *
2820
- * @param validators Зарегистрированные валидаторы
2821
- */
2822
- async applyContextualValidators(t) {
2823
- await this.validationApplicator.apply(t);
2824
- }
2825
- // ============================================================================
2826
- // Private методы для создания узлов
2827
- // ============================================================================
2828
- /**
2829
- * Создать узел на основе конфигурации
2830
- *
2831
- * ✅ РЕФАКТОРИНГ: Полное делегирование NodeFactory
2832
- *
2833
- * NodeFactory теперь обрабатывает:
2834
- * - Массивы [schema, ...items]
2835
- * - FieldConfig
2836
- * - GroupConfig
2837
- * - ArrayConfig
2838
- *
2839
- * @param config Конфигурация узла
2840
- * @returns Созданный узел формы
2841
- * @private
2842
- */
2843
- createNode(t) {
2844
- return this.nodeFactory.createNode(t);
2845
- }
2846
- // ============================================================================
2847
- // Методы-помощники для реактивности (Фаза 1)
2848
- // ============================================================================
2849
- /**
2850
- * Связывает два поля: при изменении source автоматически обновляется target
2851
- * Поддерживает опциональную трансформацию значения
2852
- *
2853
- * @param sourceKey - Ключ поля-источника
2854
- * @param targetKey - Ключ поля-цели
2855
- * @param transform - Опциональная функция трансформации значения
2856
- * @returns Функция отписки для cleanup
2857
- *
2858
- * @example
2859
- * ```typescript
2860
- * // Автоматический расчет минимального взноса от стоимости недвижимости
2861
- * const dispose = form.linkFields(
2862
- * 'propertyValue',
2863
- * 'initialPayment',
2864
- * (propertyValue) => propertyValue ? propertyValue * 0.2 : null
2865
- * );
2866
- *
2867
- * // При изменении propertyValue → автоматически обновится initialPayment
2868
- * form.propertyValue.setValue(1000000);
2869
- * // initialPayment станет 200000
2870
- *
2871
- * // Cleanup
2872
- * useEffect(() => dispose, []);
2873
- * ```
2874
- */
2875
- linkFields(t, e, i) {
2876
- const s = this.fieldRegistry.get(t), a = this.fieldRegistry.get(e);
2877
- if (!s || !a)
2878
- return () => {
2879
- };
2880
- const n = c(() => {
2881
- const h = s.value.value, u = i ? i(h) : h;
2882
- a.setValue(u, { emitEvent: !1 });
2883
- }), o = `linkFields-${Date.now()}-${Math.random()}`;
2884
- return this.disposers.add(o, n);
2885
- }
2886
- /**
2887
- * Подписка на изменения вложенного поля по строковому пути
2888
- * Поддерживает вложенные пути типа "address.city"
2889
- *
2890
- * @param fieldPath - Строковый путь к полю (например, "address.city")
2891
- * @param callback - Функция, вызываемая при изменении поля
2892
- * @returns Функция отписки для cleanup
2893
- *
2894
- * @example
2895
- * ```typescript
2896
- * // Подписка на изменение страны для загрузки городов
2897
- * const dispose = form.watchField(
2898
- * 'registrationAddress.country',
2899
- * async (countryCode) => {
2900
- * if (countryCode) {
2901
- * const cities = await fetchCitiesByCountry(countryCode);
2902
- * form.registrationAddress.city.updateComponentProps({
2903
- * options: cities
2904
- * });
2905
- * }
2906
- * }
2907
- * );
2908
- *
2909
- * // Cleanup
2910
- * useEffect(() => dispose, []);
2911
- * ```
2912
- */
2913
- watchField(t, e) {
2914
- const i = this.getFieldByPath(t);
2915
- if (!i)
2916
- return () => {
2917
- };
2918
- const s = c(() => {
2919
- const n = i.value.value;
2920
- e(n);
2921
- }), a = `watchField-${Date.now()}-${Math.random()}`;
2922
- return this.disposers.add(a, s);
2923
- }
2924
- /**
2925
- * Hook: вызывается после disable()
2926
- *
2927
- * Для GroupNode: рекурсивно отключаем все дочерние поля
2928
- */
2929
- onDisable() {
2930
- this.stateManager.setDisabled(!0), this.fieldRegistry.forEach((t) => {
2931
- t.disable();
2932
- });
2933
- }
2934
- /**
2935
- * Hook: вызывается после enable()
2936
- *
2937
- * Для GroupNode: рекурсивно включаем все дочерние поля
2938
- */
2939
- onEnable() {
2940
- this.stateManager.setDisabled(!1), this.fieldRegistry.forEach((t) => {
2941
- t.enable();
2942
- });
2943
- }
2944
- /**
2945
- * Очистить все ресурсы узла
2946
- * Рекурсивно очищает все subscriptions и дочерние узлы
2947
- *
2948
- * @example
2949
- * ```typescript
2950
- * useEffect(() => {
2951
- * return () => {
2952
- * form.dispose();
2953
- * };
2954
- * }, []);
2955
- * ```
2956
- */
2957
- dispose() {
2958
- this.disposers.dispose(), this.fieldRegistry.forEach((t) => {
2959
- "dispose" in t && typeof t.dispose == "function" && t.dispose();
2960
- });
2961
- }
2962
- }
2963
- class G {
2964
- /**
2965
- * Создает узел формы на основе конфигурации
2966
- *
2967
- * ✅ ОБНОВЛЕНО: Теперь поддерживает массивы напрямую
2968
- *
2969
- * Автоматически определяет тип узла:
2970
- * - FieldNode: имеет value и component
2971
- * - ArrayNode: массив [schema, ...items] или { schema, initialItems }
2972
- * - GroupNode: объект без value, component, schema
2973
- *
2974
- * @param config Конфигурация узла
2975
- * @returns Экземпляр FieldNode, GroupNode или ArrayNode
2976
- * @throws Error если конфиг не соответствует ни одному типу
2977
- *
2978
- * @example
2979
- * ```typescript
2980
- * const factory = new NodeFactory();
2981
- *
2982
- * // FieldNode
2983
- * const field = factory.createNode({
2984
- * value: 'test@mail.com',
2985
- * component: Input,
2986
- * validators: [required, email]
2987
- * });
2988
- *
2989
- * // GroupNode
2990
- * const group = factory.createNode({
2991
- * email: { value: '', component: Input },
2992
- * password: { value: '', component: Input }
2993
- * });
2994
- *
2995
- * // ArrayNode (объект)
2996
- * const array = factory.createNode({
2997
- * schema: { title: { value: '', component: Input } },
2998
- * initialItems: [{ title: 'Item 1' }]
2999
- * });
3000
- *
3001
- * // ArrayNode (массив) - новый формат
3002
- * const array2 = factory.createNode([
3003
- * { title: { value: '', component: Input } }, // schema
3004
- * { title: 'Item 1' }, // initial item 1
3005
- * { title: 'Item 2' } // initial item 2
3006
- * ]);
3007
- * ```
3008
- */
3009
- createNode(t) {
3010
- if (Array.isArray(t) && t.length >= 1)
3011
- return this.createArrayNodeFromArray(t);
3012
- if (this.isFieldConfig(t))
3013
- return new R(t);
3014
- if (this.isArrayConfig(t)) {
3015
- const e = t;
3016
- return new E(
3017
- e.schema,
3018
- e.initialItems
3019
- );
3020
- }
3021
- if (this.isGroupConfig(t))
3022
- return new y(t);
3023
- throw new Error(
3024
- `NodeFactory: Unknown node config. Expected FieldConfig, GroupConfig, or ArrayConfig, but got: ${JSON.stringify(
3025
- t
3026
- )}`
3027
- );
3028
- }
3029
- /**
3030
- * Создать ArrayNode из массива [schema, ...initialItems]
3031
- *
3032
- * ✅ НОВОЕ: Извлечено из GroupNode для централизации логики
3033
- *
3034
- * Формат: [itemSchema, ...initialItems]
3035
- * - Первый элемент - схема элемента массива
3036
- * - Остальные элементы - начальные значения
3037
- *
3038
- * @param config Массив с схемой и начальными элементами
3039
- * @returns ArrayNode
3040
- *
3041
- * @example
3042
- * ```typescript
3043
- * const factory = new NodeFactory();
3044
- *
3045
- * // Массив с начальными элементами
3046
- * const array = factory.createArrayNodeFromArray([
3047
- * { title: { value: '', component: Input } }, // schema
3048
- * { title: 'Item 1' }, // initial value
3049
- * { title: 'Item 2' } // initial value
3050
- * ]);
3051
- * ```
3052
- * @private
3053
- */
3054
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3055
- createArrayNodeFromArray(t) {
3056
- const [e, ...i] = t, s = [];
3057
- this.isGroupConfig(e) && s.push(this.extractValues(e));
3058
- for (const a of i)
3059
- this.isGroupConfig(a) ? s.push(this.extractValues(a)) : s.push(a);
3060
- return new E(e, s);
3061
- }
3062
- /**
3063
- * Извлечь значения из схемы (рекурсивно)
3064
- *
3065
- * ✅ НОВОЕ: Извлечено из GroupNode для централизации логики
3066
- *
3067
- * Преобразует схему формы в объект со значениями:
3068
- * - `{ name: { value: 'John', component: Input } } → { name: 'John' }`
3069
- * - Поддерживает вложенные группы
3070
- * - Поддерживает массивы
3071
- *
3072
- * @param schema Схема формы
3073
- * @returns Объект со значениями полей
3074
- *
3075
- * @example
3076
- * ```typescript
3077
- * const factory = new NodeFactory();
3078
- *
3079
- * const schema = {
3080
- * name: { value: 'John', component: Input },
3081
- * age: { value: 30, component: Input },
3082
- * address: {
3083
- * city: { value: 'Moscow', component: Input }
3084
- * }
3085
- * };
3086
- *
3087
- * factory.extractValues(schema);
3088
- * // { name: 'John', age: 30, address: { city: 'Moscow' } }
3089
- * ```
3090
- */
3091
- extractValues(t) {
3092
- if (this.isFieldConfig(t))
3093
- return t.value;
3094
- if (Array.isArray(t))
3095
- return t.map((e) => this.extractValues(e));
3096
- if (this.isGroupConfig(t)) {
3097
- const e = {};
3098
- for (const [i, s] of Object.entries(t))
3099
- e[i] = this.extractValues(s);
3100
- return e;
3101
- }
3102
- return t;
3103
- }
3104
- /**
3105
- * Проверяет, является ли конфиг конфигурацией поля (FieldConfig)
3106
- *
3107
- * FieldConfig имеет обязательные свойства:
3108
- * - value: начальное значение поля
3109
- * - component: React-компонент для отображения
3110
- *
3111
- * @param config Проверяемая конфигурация
3112
- * @returns true если config является FieldConfig
3113
- *
3114
- * @example
3115
- * ```typescript
3116
- * const factory = new NodeFactory();
3117
- *
3118
- * factory.isFieldConfig({ value: '', component: Input }); // true
3119
- * factory.isFieldConfig({ email: { value: '' } }); // false
3120
- * factory.isFieldConfig(null); // false
3121
- * ```
3122
- */
3123
- isFieldConfig(t) {
3124
- return t != null && typeof t == "object" && "value" in t && "component" in t;
3125
- }
3126
- /**
3127
- * Проверяет, является ли конфиг конфигурацией массива (ArrayConfig)
3128
- *
3129
- * ArrayConfig имеет обязательное свойство:
3130
- * - schema: схема для элементов массива
3131
- *
3132
- * И НЕ имеет:
3133
- * - value (отличие от FieldConfig)
3134
- *
3135
- * @param config Проверяемая конфигурация
3136
- * @returns true если config является ArrayConfig
3137
- *
3138
- * @example
3139
- * ```typescript
3140
- * const factory = new NodeFactory();
3141
- *
3142
- * factory.isArrayConfig({ schema: {}, initialItems: [] }); // true
3143
- * factory.isArrayConfig({ value: '', component: Input }); // false
3144
- * factory.isArrayConfig({ email: { value: '' } }); // false
3145
- * ```
3146
- */
3147
- isArrayConfig(t) {
3148
- return t != null && typeof t == "object" && "schema" in t && !("value" in t);
3149
- }
3150
- /**
3151
- * Проверяет, является ли конфиг конфигурацией группы (GroupConfig)
3152
- *
3153
- * GroupConfig - это объект, который:
3154
- * - НЕ является FieldConfig (нет value/component)
3155
- * - НЕ является ArrayConfig (нет schema)
3156
- * - Содержит вложенные конфиги полей/групп/массивов
3157
- *
3158
- * @param config Проверяемая конфигурация
3159
- * @returns true если config является GroupConfig
3160
- *
3161
- * @example
3162
- * ```typescript
3163
- * const factory = new NodeFactory();
3164
- *
3165
- * factory.isGroupConfig({
3166
- * email: { value: '', component: Input },
3167
- * password: { value: '', component: Input }
3168
- * }); // true
3169
- *
3170
- * factory.isGroupConfig({ value: '', component: Input }); // false
3171
- * factory.isGroupConfig({ schema: {} }); // false
3172
- * factory.isGroupConfig(null); // false
3173
- * ```
3174
- */
3175
- isGroupConfig(t) {
3176
- return t != null && typeof t == "object" && !this.isFieldConfig(t) && !this.isArrayConfig(t);
3177
- }
3178
- }
3179
- export {
3180
- E as A,
3181
- v as E,
3182
- g as F,
3183
- y as G,
3184
- G as N,
3185
- _ as S,
3186
- I as T,
3187
- N as V,
3188
- R as a,
3189
- B as b,
3190
- p as c,
3191
- M as d,
3192
- P as e,
3193
- m as f,
3194
- z as g,
3195
- x as h,
3196
- f as i,
3197
- S as j,
3198
- K as k,
3199
- L as t
3200
- };