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