@reformer/core 1.1.0 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/behaviors-DzYL8kY_.js +499 -0
- package/dist/behaviors.d.ts +6 -2
- package/dist/behaviors.js +19 -227
- package/dist/core/behavior/behavior-context.d.ts +6 -2
- package/dist/core/behavior/create-field-path.d.ts +3 -16
- package/dist/core/nodes/group-node.d.ts +14 -193
- package/dist/core/types/form-context.d.ts +10 -4
- package/dist/core/utils/field-path.d.ts +48 -0
- package/dist/core/utils/index.d.ts +1 -0
- package/dist/core/validation/core/validate-tree.d.ts +10 -4
- package/dist/core/validation/field-path.d.ts +3 -39
- package/dist/core/validation/validation-context.d.ts +23 -0
- package/dist/hooks/types.d.ts +328 -0
- package/dist/hooks/useFormControl.d.ts +13 -37
- package/dist/hooks/useFormControlValue.d.ts +167 -0
- package/dist/hooks/useSignalSubscription.d.ts +17 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2886 -8
- package/dist/{create-field-path-CdPF3lIK.js → registry-helpers-BRxAr6nG.js} +133 -347
- package/dist/validators-gXoHPdqM.js +418 -0
- package/dist/validators.d.ts +6 -2
- package/dist/validators.js +29 -296
- package/llms.txt +1283 -22
- package/package.json +8 -4
- package/dist/core/behavior/behavior-applicator.d.ts +0 -71
- package/dist/core/behavior/behavior-applicator.js +0 -92
- package/dist/core/behavior/behavior-context.js +0 -38
- package/dist/core/behavior/behavior-registry.js +0 -198
- package/dist/core/behavior/behaviors/compute-from.js +0 -84
- package/dist/core/behavior/behaviors/copy-from.js +0 -64
- package/dist/core/behavior/behaviors/enable-when.js +0 -81
- package/dist/core/behavior/behaviors/index.js +0 -11
- package/dist/core/behavior/behaviors/reset-when.js +0 -63
- package/dist/core/behavior/behaviors/revalidate-when.js +0 -51
- package/dist/core/behavior/behaviors/sync-fields.js +0 -66
- package/dist/core/behavior/behaviors/transform-value.js +0 -110
- package/dist/core/behavior/behaviors/watch-field.js +0 -56
- package/dist/core/behavior/compose-behavior.js +0 -166
- package/dist/core/behavior/create-field-path.js +0 -69
- package/dist/core/behavior/index.js +0 -17
- package/dist/core/behavior/types.js +0 -7
- package/dist/core/context/form-context-impl.js +0 -37
- package/dist/core/factories/index.js +0 -6
- package/dist/core/factories/node-factory.js +0 -281
- package/dist/core/nodes/array-node.js +0 -534
- package/dist/core/nodes/field-node.js +0 -510
- package/dist/core/nodes/form-node.js +0 -343
- package/dist/core/nodes/group-node/field-registry.d.ts +0 -191
- package/dist/core/nodes/group-node/field-registry.js +0 -215
- package/dist/core/nodes/group-node/index.d.ts +0 -11
- package/dist/core/nodes/group-node/index.js +0 -11
- package/dist/core/nodes/group-node/proxy-builder.d.ts +0 -71
- package/dist/core/nodes/group-node/proxy-builder.js +0 -161
- package/dist/core/nodes/group-node/state-manager.d.ts +0 -184
- package/dist/core/nodes/group-node/state-manager.js +0 -265
- package/dist/core/nodes/group-node.js +0 -770
- package/dist/core/types/deep-schema.js +0 -11
- package/dist/core/types/field-path.js +0 -4
- package/dist/core/types/form-context.js +0 -25
- package/dist/core/types/group-node-proxy.js +0 -31
- package/dist/core/types/index.js +0 -4
- package/dist/core/types/validation-schema.js +0 -10
- package/dist/core/utils/create-form.js +0 -24
- package/dist/core/utils/debounce.js +0 -197
- package/dist/core/utils/error-handler.js +0 -226
- package/dist/core/utils/field-path-navigator.js +0 -374
- package/dist/core/utils/index.js +0 -14
- package/dist/core/utils/registry-helpers.js +0 -79
- package/dist/core/utils/registry-stack.js +0 -86
- package/dist/core/utils/resources.js +0 -69
- package/dist/core/utils/subscription-manager.js +0 -214
- package/dist/core/utils/type-guards.js +0 -169
- package/dist/core/validation/core/apply-when.js +0 -41
- package/dist/core/validation/core/apply.js +0 -38
- package/dist/core/validation/core/index.js +0 -8
- package/dist/core/validation/core/validate-async.js +0 -45
- package/dist/core/validation/core/validate-tree.js +0 -37
- package/dist/core/validation/core/validate.js +0 -38
- package/dist/core/validation/field-path.js +0 -147
- package/dist/core/validation/index.js +0 -33
- package/dist/core/validation/validate-form.js +0 -152
- package/dist/core/validation/validation-applicator.js +0 -217
- package/dist/core/validation/validation-context.js +0 -75
- package/dist/core/validation/validation-registry.js +0 -298
- package/dist/core/validation/validators/array-validators.js +0 -86
- package/dist/core/validation/validators/date.js +0 -117
- package/dist/core/validation/validators/email.js +0 -60
- package/dist/core/validation/validators/index.js +0 -14
- package/dist/core/validation/validators/max-length.js +0 -60
- package/dist/core/validation/validators/max.js +0 -60
- package/dist/core/validation/validators/min-length.js +0 -60
- package/dist/core/validation/validators/min.js +0 -60
- package/dist/core/validation/validators/number.js +0 -90
- package/dist/core/validation/validators/pattern.js +0 -62
- package/dist/core/validation/validators/phone.js +0 -58
- package/dist/core/validation/validators/required.js +0 -69
- package/dist/core/validation/validators/url.js +0 -55
- package/dist/hooks/useFormControl.js +0 -298
- package/dist/node-factory-D7DOnSSN.js +0 -3200
|
@@ -1,770 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GroupNode - узел группы полей формы
|
|
3
|
-
*
|
|
4
|
-
* Представляет группу полей (объект), где каждое поле может быть:
|
|
5
|
-
* - FieldNode (простое поле)
|
|
6
|
-
* - GroupNode (вложенная группа)
|
|
7
|
-
* - ArrayNode (массив форм)
|
|
8
|
-
*
|
|
9
|
-
* Наследует от FormNode и реализует все его абстрактные методы
|
|
10
|
-
*
|
|
11
|
-
* @group Nodes
|
|
12
|
-
*/
|
|
13
|
-
import { effect } from '@preact/signals-core';
|
|
14
|
-
import { FormNode } from './form-node';
|
|
15
|
-
import { createFieldPath } from '../validation';
|
|
16
|
-
import { ValidationApplicator } from '../validation/validation-applicator';
|
|
17
|
-
import { BehaviorRegistry } from '../behavior/behavior-registry';
|
|
18
|
-
import { BehaviorApplicator } from '../behavior/behavior-applicator';
|
|
19
|
-
import { FieldPathNavigator } from '../utils/field-path-navigator';
|
|
20
|
-
import { NodeFactory } from '../factories/node-factory';
|
|
21
|
-
import { SubscriptionManager } from '../utils/subscription-manager';
|
|
22
|
-
import { ValidationRegistry } from '../validation/validation-registry';
|
|
23
|
-
import { FieldRegistry } from './group-node/field-registry';
|
|
24
|
-
import { ProxyBuilder } from './group-node/proxy-builder';
|
|
25
|
-
import { StateManager } from './group-node/state-manager';
|
|
26
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
27
|
-
/**
|
|
28
|
-
* GroupNode - узел для группы полей
|
|
29
|
-
*
|
|
30
|
-
* Поддерживает два API:
|
|
31
|
-
* 1. Старый API (только schema) - обратная совместимость
|
|
32
|
-
* 2. Новый API (config с form, behavior, validation) - автоматическое применение схем
|
|
33
|
-
*
|
|
34
|
-
* @group Nodes
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```typescript
|
|
38
|
-
* // 1. Старый способ (обратная совместимость)
|
|
39
|
-
* const simpleForm = new GroupNode({
|
|
40
|
-
* email: { value: '', component: Input },
|
|
41
|
-
* password: { value: '', component: Input },
|
|
42
|
-
* });
|
|
43
|
-
*
|
|
44
|
-
* // 2. Новый способ (с behavior и validation схемами)
|
|
45
|
-
* const fullForm = new GroupNode({
|
|
46
|
-
* form: {
|
|
47
|
-
* email: { value: '', component: Input },
|
|
48
|
-
* password: { value: '', component: Input },
|
|
49
|
-
* },
|
|
50
|
-
* behavior: (path) => {
|
|
51
|
-
* computeFrom(path.email, [path.email], (values) => values[0]?.trim());
|
|
52
|
-
* },
|
|
53
|
-
* validation: (path) => {
|
|
54
|
-
* required(path.email, { message: 'Email обязателен' });
|
|
55
|
-
* email(path.email);
|
|
56
|
-
* required(path.password);
|
|
57
|
-
* minLength(path.password, 8);
|
|
58
|
-
* },
|
|
59
|
-
* });
|
|
60
|
-
*
|
|
61
|
-
* // Прямой доступ к полям через Proxy
|
|
62
|
-
* fullForm.email.setValue('test@mail.com');
|
|
63
|
-
* await fullForm.validate();
|
|
64
|
-
* console.log(fullForm.valid.value); // true
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
|
-
export class GroupNode extends FormNode {
|
|
68
|
-
// ============================================================================
|
|
69
|
-
// Приватные поля
|
|
70
|
-
// ============================================================================
|
|
71
|
-
id = uuidv4();
|
|
72
|
-
/**
|
|
73
|
-
* Реестр полей формы
|
|
74
|
-
* Использует FieldRegistry для инкапсуляции логики управления коллекцией полей
|
|
75
|
-
*/
|
|
76
|
-
fieldRegistry;
|
|
77
|
-
/**
|
|
78
|
-
* Строитель Proxy для типобезопасного доступа к полям
|
|
79
|
-
* Использует ProxyBuilder для создания Proxy с расширенной функциональностью
|
|
80
|
-
*/
|
|
81
|
-
proxyBuilder;
|
|
82
|
-
/**
|
|
83
|
-
* Менеджер состояния формы
|
|
84
|
-
* Инкапсулирует всю логику создания и управления сигналами состояния
|
|
85
|
-
* Извлечен из GroupNode для соблюдения SRP
|
|
86
|
-
*/
|
|
87
|
-
stateManager;
|
|
88
|
-
/**
|
|
89
|
-
* Менеджер подписок для централизованного cleanup
|
|
90
|
-
* Использует SubscriptionManager вместо массива для управления подписками
|
|
91
|
-
*/
|
|
92
|
-
disposers = new SubscriptionManager();
|
|
93
|
-
/**
|
|
94
|
-
* Ссылка на Proxy-инстанс для использования в BehaviorContext
|
|
95
|
-
* Устанавливается в конструкторе до применения behavior schema
|
|
96
|
-
*/
|
|
97
|
-
_proxyInstance;
|
|
98
|
-
/**
|
|
99
|
-
* Навигатор для работы с путями к полям
|
|
100
|
-
* Использует композицию вместо дублирования логики парсинга путей
|
|
101
|
-
*/
|
|
102
|
-
pathNavigator = new FieldPathNavigator();
|
|
103
|
-
/**
|
|
104
|
-
* Фабрика для создания узлов формы
|
|
105
|
-
* Использует композицию для централизованного создания FieldNode/GroupNode/ArrayNode
|
|
106
|
-
*/
|
|
107
|
-
nodeFactory = new NodeFactory();
|
|
108
|
-
/**
|
|
109
|
-
* Реестр валидаторов для этой формы
|
|
110
|
-
* Использует композицию вместо глобального Singleton
|
|
111
|
-
* Обеспечивает полную изоляцию форм друг от друга
|
|
112
|
-
*/
|
|
113
|
-
validationRegistry = new ValidationRegistry();
|
|
114
|
-
/**
|
|
115
|
-
* Реестр behaviors для этой формы
|
|
116
|
-
* Использует композицию вместо глобального Singleton
|
|
117
|
-
* Обеспечивает полную изоляцию форм друг от друга
|
|
118
|
-
*/
|
|
119
|
-
behaviorRegistry = new BehaviorRegistry();
|
|
120
|
-
/**
|
|
121
|
-
* Аппликатор для применения валидаторов к форме
|
|
122
|
-
* Извлечен из GroupNode для соблюдения SRP
|
|
123
|
-
* Использует композицию для управления процессом валидации
|
|
124
|
-
*/
|
|
125
|
-
validationApplicator = new ValidationApplicator(this);
|
|
126
|
-
/**
|
|
127
|
-
* Аппликатор для применения behavior схемы к форме
|
|
128
|
-
* Извлечен из GroupNode для соблюдения SRP
|
|
129
|
-
* Использует композицию для управления процессом применения behaviors
|
|
130
|
-
*/
|
|
131
|
-
behaviorApplicator = new BehaviorApplicator(this, this.behaviorRegistry);
|
|
132
|
-
// ============================================================================
|
|
133
|
-
// Публичные computed signals (делегированы в StateManager)
|
|
134
|
-
// ============================================================================
|
|
135
|
-
value;
|
|
136
|
-
valid;
|
|
137
|
-
invalid;
|
|
138
|
-
touched;
|
|
139
|
-
dirty;
|
|
140
|
-
pending;
|
|
141
|
-
errors;
|
|
142
|
-
status;
|
|
143
|
-
submitting;
|
|
144
|
-
constructor(schemaOrConfig) {
|
|
145
|
-
super();
|
|
146
|
-
// Инициализация модулей для управления полями и прокси
|
|
147
|
-
this.fieldRegistry = new FieldRegistry();
|
|
148
|
-
this.proxyBuilder = new ProxyBuilder(this.fieldRegistry);
|
|
149
|
-
// Определяем, что передано: schema или config
|
|
150
|
-
const isConfig = 'form' in schemaOrConfig;
|
|
151
|
-
const formSchema = isConfig
|
|
152
|
-
? schemaOrConfig.form
|
|
153
|
-
: schemaOrConfig;
|
|
154
|
-
const behaviorSchema = isConfig ? schemaOrConfig.behavior : undefined;
|
|
155
|
-
const validationSchema = isConfig
|
|
156
|
-
? schemaOrConfig.validation
|
|
157
|
-
: undefined;
|
|
158
|
-
// Создать поля из схемы с поддержкой вложенности
|
|
159
|
-
for (const [key, config] of Object.entries(formSchema)) {
|
|
160
|
-
const node = this.createNode(config);
|
|
161
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
162
|
-
this.fieldRegistry.set(key, node);
|
|
163
|
-
}
|
|
164
|
-
// Создать менеджер состояния (инкапсулирует всю логику сигналов)
|
|
165
|
-
// StateManager создает все computed signals на основе fieldRegistry
|
|
166
|
-
this.stateManager = new StateManager(this.fieldRegistry);
|
|
167
|
-
// Делегировать публичные свойства в StateManager
|
|
168
|
-
this.value = this.stateManager.value;
|
|
169
|
-
this.valid = this.stateManager.valid;
|
|
170
|
-
this.invalid = this.stateManager.invalid;
|
|
171
|
-
this.touched = this.stateManager.touched;
|
|
172
|
-
this.dirty = this.stateManager.dirty;
|
|
173
|
-
this.pending = this.stateManager.pending;
|
|
174
|
-
this.errors = this.stateManager.errors;
|
|
175
|
-
this.status = this.stateManager.status;
|
|
176
|
-
this.submitting = this.stateManager.submitting;
|
|
177
|
-
// Создать Proxy для прямого доступа к полям
|
|
178
|
-
// Используем ProxyBuilder для создания Proxy с расширенной функциональностью
|
|
179
|
-
const proxy = this.proxyBuilder.build(this);
|
|
180
|
-
// Сохраняем Proxy-инстанс перед применением схем
|
|
181
|
-
// Это позволяет BehaviorContext получить доступ к прокси через formNode
|
|
182
|
-
this._proxyInstance = proxy;
|
|
183
|
-
// Применяем схемы, если они переданы (новый API)
|
|
184
|
-
if (behaviorSchema) {
|
|
185
|
-
this.applyBehaviorSchema(behaviorSchema);
|
|
186
|
-
}
|
|
187
|
-
if (validationSchema) {
|
|
188
|
-
this.applyValidationSchema(validationSchema);
|
|
189
|
-
}
|
|
190
|
-
// ВАЖНО: Возвращаем Proxy для прямого доступа к полям
|
|
191
|
-
// Это позволяет писать form.email вместо form.controls.email
|
|
192
|
-
// Используем GroupNodeWithControls для правильной типизации вложенных форм и массивов
|
|
193
|
-
return proxy;
|
|
194
|
-
}
|
|
195
|
-
// ============================================================================
|
|
196
|
-
// Реализация абстрактных методов FormNode
|
|
197
|
-
// ============================================================================
|
|
198
|
-
getValue() {
|
|
199
|
-
const result = {};
|
|
200
|
-
this.fieldRegistry.forEach((field, key) => {
|
|
201
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
202
|
-
result[key] = field.getValue();
|
|
203
|
-
});
|
|
204
|
-
return result;
|
|
205
|
-
}
|
|
206
|
-
setValue(value, options) {
|
|
207
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
-
for (const [key, fieldValue] of Object.entries(value)) {
|
|
209
|
-
const field = this.fieldRegistry.get(key);
|
|
210
|
-
if (field) {
|
|
211
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
-
field.setValue(fieldValue, options);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
patchValue(value) {
|
|
217
|
-
for (const [key, fieldValue] of Object.entries(value)) {
|
|
218
|
-
const field = this.fieldRegistry.get(key);
|
|
219
|
-
if (field && fieldValue !== undefined) {
|
|
220
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
-
field.setValue(fieldValue);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Сбросить форму к указанным значениям (или к initialValues)
|
|
227
|
-
*
|
|
228
|
-
* @param value - опциональный объект со значениями для сброса
|
|
229
|
-
*
|
|
230
|
-
* @remarks
|
|
231
|
-
* Рекурсивно вызывает reset() для всех полей формы
|
|
232
|
-
*
|
|
233
|
-
* @example
|
|
234
|
-
* ```typescript
|
|
235
|
-
* // Сброс к initialValues
|
|
236
|
-
* form.reset();
|
|
237
|
-
*
|
|
238
|
-
* // Сброс к новым значениям
|
|
239
|
-
* form.reset({ email: 'new@mail.com', password: '' });
|
|
240
|
-
* ```
|
|
241
|
-
*/
|
|
242
|
-
reset(value) {
|
|
243
|
-
this.fieldRegistry.forEach((field, key) => {
|
|
244
|
-
const resetValue = value?.[key];
|
|
245
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
246
|
-
field.reset(resetValue);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Сбросить форму к исходным значениям (initialValues)
|
|
251
|
-
*
|
|
252
|
-
* @remarks
|
|
253
|
-
* Рекурсивно вызывает resetToInitial() для всех полей формы.
|
|
254
|
-
* Более явный способ сброса к начальным значениям по сравнению с reset()
|
|
255
|
-
*
|
|
256
|
-
* Полезно когда:
|
|
257
|
-
* - Пользователь нажал "Cancel" - полная отмена изменений
|
|
258
|
-
* - Форма была изменена через reset(newValues), но нужно вернуться к самому началу
|
|
259
|
-
* - Явное намерение показать "отмена всех изменений"
|
|
260
|
-
*
|
|
261
|
-
* @example
|
|
262
|
-
* ```typescript
|
|
263
|
-
* const form = new GroupNode({
|
|
264
|
-
* email: { value: 'initial@mail.com', component: Input },
|
|
265
|
-
* name: { value: 'John', component: Input }
|
|
266
|
-
* });
|
|
267
|
-
*
|
|
268
|
-
* form.email.setValue('changed@mail.com');
|
|
269
|
-
* form.reset({ email: 'temp@mail.com', name: 'Jane' });
|
|
270
|
-
* console.log(form.getValue()); // { email: 'temp@mail.com', name: 'Jane' }
|
|
271
|
-
*
|
|
272
|
-
* form.resetToInitial();
|
|
273
|
-
* console.log(form.getValue()); // { email: 'initial@mail.com', name: 'John' }
|
|
274
|
-
* ```
|
|
275
|
-
*/
|
|
276
|
-
resetToInitial() {
|
|
277
|
-
this.fieldRegistry.forEach((field) => {
|
|
278
|
-
if ('resetToInitial' in field && typeof field.resetToInitial === 'function') {
|
|
279
|
-
field.resetToInitial();
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
field.reset();
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
async validate() {
|
|
287
|
-
// Шаг 0: Очищаем ошибки перед валидацией (для корректной работы ValidationSchema)
|
|
288
|
-
this.clearErrors();
|
|
289
|
-
// Шаг 1: Валидация всех полей
|
|
290
|
-
await Promise.all(Array.from(this.fieldRegistry.values()).map((field) => field.validate()));
|
|
291
|
-
// Шаг 2: Применение contextual валидаторов из validation schema
|
|
292
|
-
// Используем локальный реестр вместо глобального
|
|
293
|
-
const validators = this.validationRegistry.getValidators();
|
|
294
|
-
if (validators && validators.length > 0) {
|
|
295
|
-
await this.applyContextualValidators(validators);
|
|
296
|
-
}
|
|
297
|
-
// Проверяем, все ли поля валидны
|
|
298
|
-
return Array.from(this.fieldRegistry.values()).every((field) => field.valid.value);
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Установить form-level validation errors
|
|
302
|
-
* Используется для server-side validation или кросс-полевых ошибок
|
|
303
|
-
*
|
|
304
|
-
* @param errors - массив ошибок уровня формы
|
|
305
|
-
*
|
|
306
|
-
* @example
|
|
307
|
-
* ```typescript
|
|
308
|
-
* // Server-side validation после submit
|
|
309
|
-
* try {
|
|
310
|
-
* await api.createUser(form.getValue());
|
|
311
|
-
* } catch (error) {
|
|
312
|
-
* form.setErrors([
|
|
313
|
-
* { code: 'duplicate_email', message: 'Email уже используется' }
|
|
314
|
-
* ]);
|
|
315
|
-
* }
|
|
316
|
-
* ```
|
|
317
|
-
*/
|
|
318
|
-
setErrors(errors) {
|
|
319
|
-
this.stateManager.setFormErrors(errors);
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Очистить все errors (form-level + field-level)
|
|
323
|
-
*/
|
|
324
|
-
clearErrors() {
|
|
325
|
-
// Очищаем form-level errors
|
|
326
|
-
this.stateManager.clearFormErrors();
|
|
327
|
-
// Очищаем field-level errors
|
|
328
|
-
this.fieldRegistry.forEach((field) => field.clearErrors());
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Получить поле по ключу
|
|
332
|
-
*
|
|
333
|
-
* Публичный метод для доступа к полю из fieldRegistry
|
|
334
|
-
*
|
|
335
|
-
* @param key - Ключ поля
|
|
336
|
-
* @returns FormNode или undefined, если поле не найдено
|
|
337
|
-
*
|
|
338
|
-
* @example
|
|
339
|
-
* ```typescript
|
|
340
|
-
* const emailField = form.getField('email');
|
|
341
|
-
* if (emailField) {
|
|
342
|
-
* console.log(emailField.value.value);
|
|
343
|
-
* }
|
|
344
|
-
* ```
|
|
345
|
-
*/
|
|
346
|
-
getField(key) {
|
|
347
|
-
return this.fieldRegistry.get(key);
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Получить Map всех полей формы
|
|
351
|
-
*
|
|
352
|
-
* Используется в FieldPathNavigator для навигации по полям
|
|
353
|
-
*
|
|
354
|
-
* @returns Map полей формы
|
|
355
|
-
*/
|
|
356
|
-
get fields() {
|
|
357
|
-
return this.fieldRegistry;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Получить Proxy-инстанс для прямого доступа к полям
|
|
361
|
-
*
|
|
362
|
-
* Proxy позволяет обращаться к полям формы напрямую через точечную нотацию:
|
|
363
|
-
* - form.email вместо form.fields.get('email')
|
|
364
|
-
* - form.address.city вместо form.fields.get('address').fields.get('city')
|
|
365
|
-
*
|
|
366
|
-
* Используется в:
|
|
367
|
-
* - BehaviorApplicator для доступа к полям в behavior functions
|
|
368
|
-
* - ValidationApplicator для доступа к форме в tree validators
|
|
369
|
-
*
|
|
370
|
-
* @returns Proxy-инстанс с типобезопасным доступом к полям или сама форма, если proxy не доступен
|
|
371
|
-
*
|
|
372
|
-
* @example
|
|
373
|
-
* ```typescript
|
|
374
|
-
* const form = new GroupNode({
|
|
375
|
-
* controls: {
|
|
376
|
-
* email: new FieldNode({ value: '' }),
|
|
377
|
-
* name: new FieldNode({ value: '' })
|
|
378
|
-
* }
|
|
379
|
-
* });
|
|
380
|
-
*
|
|
381
|
-
* const proxy = form.getProxy();
|
|
382
|
-
* console.log(proxy.email.value); // Прямой доступ к полю
|
|
383
|
-
* ```
|
|
384
|
-
*/
|
|
385
|
-
getProxy() {
|
|
386
|
-
return (this._proxyInstance || this);
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Получить все поля формы как итератор
|
|
390
|
-
*
|
|
391
|
-
* Предоставляет доступ к внутренним полям для валидации и других операций
|
|
392
|
-
*
|
|
393
|
-
* @returns Итератор по всем полям формы
|
|
394
|
-
*
|
|
395
|
-
* @example
|
|
396
|
-
* ```typescript
|
|
397
|
-
* // Валидация всех полей
|
|
398
|
-
* await Promise.all(
|
|
399
|
-
* Array.from(form.getAllFields()).map(field => field.validate())
|
|
400
|
-
* );
|
|
401
|
-
* ```
|
|
402
|
-
*/
|
|
403
|
-
getAllFields() {
|
|
404
|
-
return this.fieldRegistry.values();
|
|
405
|
-
}
|
|
406
|
-
// ============================================================================
|
|
407
|
-
// Protected hooks (Template Method pattern)
|
|
408
|
-
// ============================================================================
|
|
409
|
-
/**
|
|
410
|
-
* Hook: вызывается после markAsTouched()
|
|
411
|
-
*
|
|
412
|
-
* Для GroupNode: рекурсивно помечаем все дочерние поля как touched
|
|
413
|
-
*/
|
|
414
|
-
onMarkAsTouched() {
|
|
415
|
-
this.fieldRegistry.forEach((field) => field.markAsTouched());
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Hook: вызывается после markAsUntouched()
|
|
419
|
-
*
|
|
420
|
-
* Для GroupNode: рекурсивно помечаем все дочерние поля как untouched
|
|
421
|
-
*/
|
|
422
|
-
onMarkAsUntouched() {
|
|
423
|
-
this.fieldRegistry.forEach((field) => field.markAsUntouched());
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Hook: вызывается после markAsDirty()
|
|
427
|
-
*
|
|
428
|
-
* Для GroupNode: рекурсивно помечаем все дочерние поля как dirty
|
|
429
|
-
*/
|
|
430
|
-
onMarkAsDirty() {
|
|
431
|
-
this.fieldRegistry.forEach((field) => field.markAsDirty());
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Hook: вызывается после markAsPristine()
|
|
435
|
-
*
|
|
436
|
-
* Для GroupNode: рекурсивно помечаем все дочерние поля как pristine
|
|
437
|
-
*/
|
|
438
|
-
onMarkAsPristine() {
|
|
439
|
-
this.fieldRegistry.forEach((field) => field.markAsPristine());
|
|
440
|
-
}
|
|
441
|
-
// ============================================================================
|
|
442
|
-
// Дополнительные методы (из FormStore)
|
|
443
|
-
// ============================================================================
|
|
444
|
-
/**
|
|
445
|
-
* Отправить форму
|
|
446
|
-
* Валидирует форму и вызывает onSubmit если форма валидна
|
|
447
|
-
*/
|
|
448
|
-
async submit(onSubmit) {
|
|
449
|
-
this.markAsTouched();
|
|
450
|
-
const isValid = await this.validate();
|
|
451
|
-
if (!isValid) {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
this.stateManager.setSubmitting(true);
|
|
455
|
-
try {
|
|
456
|
-
const result = await onSubmit(this.getValue());
|
|
457
|
-
return result;
|
|
458
|
-
}
|
|
459
|
-
finally {
|
|
460
|
-
this.stateManager.setSubmitting(false);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Применить validation schema к форме
|
|
465
|
-
*
|
|
466
|
-
* Использует локальный реестр валидаторов (this.validationRegistry)
|
|
467
|
-
* вместо глобального Singleton для изоляции форм друг от друга.
|
|
468
|
-
*/
|
|
469
|
-
applyValidationSchema(schemaFn) {
|
|
470
|
-
this.validationRegistry.beginRegistration();
|
|
471
|
-
try {
|
|
472
|
-
const path = createFieldPath();
|
|
473
|
-
schemaFn(path);
|
|
474
|
-
// Используем публичный метод getProxy() для получения proxy-инстанса
|
|
475
|
-
const formToUse = this.getProxy();
|
|
476
|
-
this.validationRegistry.endRegistration(formToUse);
|
|
477
|
-
}
|
|
478
|
-
catch (error) {
|
|
479
|
-
console.error('Error applying validation schema:', error);
|
|
480
|
-
throw error;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Применить behavior schema к форме
|
|
485
|
-
*
|
|
486
|
-
* ✅ РЕФАКТОРИНГ: Делегирование BehaviorApplicator (SRP)
|
|
487
|
-
*
|
|
488
|
-
* Логика применения behavior схемы извлечена в BehaviorApplicator для:
|
|
489
|
-
* - Соблюдения Single Responsibility Principle
|
|
490
|
-
* - Уменьшения размера GroupNode (~50 строк)
|
|
491
|
-
* - Улучшения тестируемости
|
|
492
|
-
* - Консистентности с ValidationApplicator
|
|
493
|
-
*
|
|
494
|
-
* @param schemaFn Функция описания поведения формы
|
|
495
|
-
* @returns Функция cleanup для отписки от всех behaviors
|
|
496
|
-
*
|
|
497
|
-
* @example
|
|
498
|
-
* ```typescript
|
|
499
|
-
* import { copyFrom, enableWhen, computeFrom } from '@/lib/forms/core/behaviors';
|
|
500
|
-
*
|
|
501
|
-
* const behaviorSchema: BehaviorSchemaFn<MyForm> = (path) => {
|
|
502
|
-
* copyFrom(path.residenceAddress, path.registrationAddress, {
|
|
503
|
-
* when: (form) => form.sameAsRegistration === true
|
|
504
|
-
* });
|
|
505
|
-
*
|
|
506
|
-
* enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage');
|
|
507
|
-
*
|
|
508
|
-
* computeFrom(
|
|
509
|
-
* path.initialPayment,
|
|
510
|
-
* [path.propertyValue],
|
|
511
|
-
* (propertyValue) => propertyValue ? propertyValue * 0.2 : null
|
|
512
|
-
* );
|
|
513
|
-
* };
|
|
514
|
-
*
|
|
515
|
-
* const cleanup = form.applyBehaviorSchema(behaviorSchema);
|
|
516
|
-
*
|
|
517
|
-
* // Cleanup при unmount
|
|
518
|
-
* useEffect(() => cleanup, []);
|
|
519
|
-
* ```
|
|
520
|
-
*/
|
|
521
|
-
applyBehaviorSchema(schemaFn) {
|
|
522
|
-
return this.behaviorApplicator.apply(schemaFn);
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Получить вложенное поле по пути
|
|
526
|
-
*
|
|
527
|
-
* Поддерживаемые форматы путей:
|
|
528
|
-
* - Simple: "email" - получить поле верхнего уровня
|
|
529
|
-
* - Nested: "address.city" - получить вложенное поле
|
|
530
|
-
* - Array index: "items[0]" - получить элемент массива по индексу
|
|
531
|
-
* - Combined: "items[0].name" - получить поле элемента массива
|
|
532
|
-
*
|
|
533
|
-
* @param path - Путь к полю
|
|
534
|
-
* @returns FormNode если найдено, undefined если путь не существует
|
|
535
|
-
*
|
|
536
|
-
* @example
|
|
537
|
-
* ```typescript
|
|
538
|
-
* const form = new GroupNode({
|
|
539
|
-
* email: { value: '', component: Input },
|
|
540
|
-
* address: {
|
|
541
|
-
* city: { value: '', component: Input }
|
|
542
|
-
* },
|
|
543
|
-
* items: [{ name: { value: '', component: Input } }]
|
|
544
|
-
* });
|
|
545
|
-
*
|
|
546
|
-
* form.getFieldByPath('email'); // FieldNode
|
|
547
|
-
* form.getFieldByPath('address.city'); // FieldNode
|
|
548
|
-
* form.getFieldByPath('items[0]'); // GroupNode
|
|
549
|
-
* form.getFieldByPath('items[0].name'); // FieldNode
|
|
550
|
-
* form.getFieldByPath('invalid.path'); // undefined
|
|
551
|
-
* ```
|
|
552
|
-
*/
|
|
553
|
-
getFieldByPath(path) {
|
|
554
|
-
// Проверка на некорректные пути (leading/trailing dots)
|
|
555
|
-
if (path.startsWith('.') || path.endsWith('.')) {
|
|
556
|
-
return undefined;
|
|
557
|
-
}
|
|
558
|
-
// Используем FieldPathNavigator вместо ручного парсинга
|
|
559
|
-
const segments = this.pathNavigator.parsePath(path);
|
|
560
|
-
if (segments.length === 0) {
|
|
561
|
-
return undefined;
|
|
562
|
-
}
|
|
563
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
564
|
-
let current = this;
|
|
565
|
-
for (const segment of segments) {
|
|
566
|
-
// Доступ к полю
|
|
567
|
-
if (!(current instanceof GroupNode)) {
|
|
568
|
-
return undefined;
|
|
569
|
-
}
|
|
570
|
-
current = current.getField(segment.key);
|
|
571
|
-
if (!current)
|
|
572
|
-
return undefined;
|
|
573
|
-
// Если есть индекс, получаем элемент массива
|
|
574
|
-
if (segment.index !== undefined) {
|
|
575
|
-
// Используем duck typing вместо instanceof из-за circular dependency
|
|
576
|
-
if ('at' in current &&
|
|
577
|
-
'length' in current &&
|
|
578
|
-
typeof current.at === 'function') {
|
|
579
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
580
|
-
const item = current.at(segment.index);
|
|
581
|
-
if (!item)
|
|
582
|
-
return undefined;
|
|
583
|
-
current = item;
|
|
584
|
-
}
|
|
585
|
-
else {
|
|
586
|
-
return undefined;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
return current;
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Применить contextual валидаторы к полям
|
|
594
|
-
*
|
|
595
|
-
* ✅ РЕФАКТОРИНГ: Делегирование ValidationApplicator (SRP)
|
|
596
|
-
*
|
|
597
|
-
* Логика применения валидаторов извлечена в ValidationApplicator для:
|
|
598
|
-
* - Соблюдения Single Responsibility Principle
|
|
599
|
-
* - Уменьшения размера GroupNode (~120 строк)
|
|
600
|
-
* - Улучшения тестируемости
|
|
601
|
-
*
|
|
602
|
-
* @param validators Зарегистрированные валидаторы
|
|
603
|
-
*/
|
|
604
|
-
async applyContextualValidators(validators) {
|
|
605
|
-
await this.validationApplicator.apply(validators);
|
|
606
|
-
}
|
|
607
|
-
// ============================================================================
|
|
608
|
-
// Private методы для создания узлов
|
|
609
|
-
// ============================================================================
|
|
610
|
-
/**
|
|
611
|
-
* Создать узел на основе конфигурации
|
|
612
|
-
*
|
|
613
|
-
* ✅ РЕФАКТОРИНГ: Полное делегирование NodeFactory
|
|
614
|
-
*
|
|
615
|
-
* NodeFactory теперь обрабатывает:
|
|
616
|
-
* - Массивы [schema, ...items]
|
|
617
|
-
* - FieldConfig
|
|
618
|
-
* - GroupConfig
|
|
619
|
-
* - ArrayConfig
|
|
620
|
-
*
|
|
621
|
-
* @param config Конфигурация узла
|
|
622
|
-
* @returns Созданный узел формы
|
|
623
|
-
* @private
|
|
624
|
-
*/
|
|
625
|
-
createNode(config) {
|
|
626
|
-
// Полное делегирование NodeFactory
|
|
627
|
-
// NodeFactory теперь поддерживает массивы напрямую
|
|
628
|
-
return this.nodeFactory.createNode(config);
|
|
629
|
-
}
|
|
630
|
-
// ============================================================================
|
|
631
|
-
// Методы-помощники для реактивности (Фаза 1)
|
|
632
|
-
// ============================================================================
|
|
633
|
-
/**
|
|
634
|
-
* Связывает два поля: при изменении source автоматически обновляется target
|
|
635
|
-
* Поддерживает опциональную трансформацию значения
|
|
636
|
-
*
|
|
637
|
-
* @param sourceKey - Ключ поля-источника
|
|
638
|
-
* @param targetKey - Ключ поля-цели
|
|
639
|
-
* @param transform - Опциональная функция трансформации значения
|
|
640
|
-
* @returns Функция отписки для cleanup
|
|
641
|
-
*
|
|
642
|
-
* @example
|
|
643
|
-
* ```typescript
|
|
644
|
-
* // Автоматический расчет минимального взноса от стоимости недвижимости
|
|
645
|
-
* const dispose = form.linkFields(
|
|
646
|
-
* 'propertyValue',
|
|
647
|
-
* 'initialPayment',
|
|
648
|
-
* (propertyValue) => propertyValue ? propertyValue * 0.2 : null
|
|
649
|
-
* );
|
|
650
|
-
*
|
|
651
|
-
* // При изменении propertyValue → автоматически обновится initialPayment
|
|
652
|
-
* form.propertyValue.setValue(1000000);
|
|
653
|
-
* // initialPayment станет 200000
|
|
654
|
-
*
|
|
655
|
-
* // Cleanup
|
|
656
|
-
* useEffect(() => dispose, []);
|
|
657
|
-
* ```
|
|
658
|
-
*/
|
|
659
|
-
linkFields(sourceKey, targetKey, transform) {
|
|
660
|
-
const sourceField = this.fieldRegistry.get(sourceKey);
|
|
661
|
-
const targetField = this.fieldRegistry.get(targetKey);
|
|
662
|
-
if (!sourceField || !targetField) {
|
|
663
|
-
if (import.meta.env.DEV) {
|
|
664
|
-
console.warn(`GroupNode.linkFields: field "${String(sourceKey)}" or "${String(targetKey)}" not found`);
|
|
665
|
-
}
|
|
666
|
-
return () => { }; // noop
|
|
667
|
-
}
|
|
668
|
-
const dispose = effect(() => {
|
|
669
|
-
const sourceValue = sourceField.value.value;
|
|
670
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
671
|
-
const transformedValue = transform ? transform(sourceValue) : sourceValue;
|
|
672
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
673
|
-
targetField.setValue(transformedValue, { emitEvent: false });
|
|
674
|
-
});
|
|
675
|
-
// Регистрируем через SubscriptionManager и возвращаем unsubscribe
|
|
676
|
-
const key = `linkFields-${Date.now()}-${Math.random()}`;
|
|
677
|
-
return this.disposers.add(key, dispose);
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Подписка на изменения вложенного поля по строковому пути
|
|
681
|
-
* Поддерживает вложенные пути типа "address.city"
|
|
682
|
-
*
|
|
683
|
-
* @param fieldPath - Строковый путь к полю (например, "address.city")
|
|
684
|
-
* @param callback - Функция, вызываемая при изменении поля
|
|
685
|
-
* @returns Функция отписки для cleanup
|
|
686
|
-
*
|
|
687
|
-
* @example
|
|
688
|
-
* ```typescript
|
|
689
|
-
* // Подписка на изменение страны для загрузки городов
|
|
690
|
-
* const dispose = form.watchField(
|
|
691
|
-
* 'registrationAddress.country',
|
|
692
|
-
* async (countryCode) => {
|
|
693
|
-
* if (countryCode) {
|
|
694
|
-
* const cities = await fetchCitiesByCountry(countryCode);
|
|
695
|
-
* form.registrationAddress.city.updateComponentProps({
|
|
696
|
-
* options: cities
|
|
697
|
-
* });
|
|
698
|
-
* }
|
|
699
|
-
* }
|
|
700
|
-
* );
|
|
701
|
-
*
|
|
702
|
-
* // Cleanup
|
|
703
|
-
* useEffect(() => dispose, []);
|
|
704
|
-
* ```
|
|
705
|
-
*/
|
|
706
|
-
watchField(fieldPath, callback) {
|
|
707
|
-
const field = this.getFieldByPath(fieldPath);
|
|
708
|
-
if (!field) {
|
|
709
|
-
if (import.meta.env.DEV) {
|
|
710
|
-
console.warn(`GroupNode.watchField: field "${fieldPath}" not found`);
|
|
711
|
-
}
|
|
712
|
-
return () => { }; // noop
|
|
713
|
-
}
|
|
714
|
-
const dispose = effect(() => {
|
|
715
|
-
const value = field.value.value;
|
|
716
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
717
|
-
callback(value);
|
|
718
|
-
});
|
|
719
|
-
// Регистрируем через SubscriptionManager и возвращаем unsubscribe
|
|
720
|
-
const key = `watchField-${Date.now()}-${Math.random()}`;
|
|
721
|
-
return this.disposers.add(key, dispose);
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* Hook: вызывается после disable()
|
|
725
|
-
*
|
|
726
|
-
* Для GroupNode: рекурсивно отключаем все дочерние поля
|
|
727
|
-
*/
|
|
728
|
-
onDisable() {
|
|
729
|
-
// Синхронизируем disabled signal через StateManager
|
|
730
|
-
this.stateManager.setDisabled(true);
|
|
731
|
-
this.fieldRegistry.forEach((field) => {
|
|
732
|
-
field.disable();
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Hook: вызывается после enable()
|
|
737
|
-
*
|
|
738
|
-
* Для GroupNode: рекурсивно включаем все дочерние поля
|
|
739
|
-
*/
|
|
740
|
-
onEnable() {
|
|
741
|
-
// Синхронизируем disabled signal через StateManager
|
|
742
|
-
this.stateManager.setDisabled(false);
|
|
743
|
-
this.fieldRegistry.forEach((field) => {
|
|
744
|
-
field.enable();
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Очистить все ресурсы узла
|
|
749
|
-
* Рекурсивно очищает все subscriptions и дочерние узлы
|
|
750
|
-
*
|
|
751
|
-
* @example
|
|
752
|
-
* ```typescript
|
|
753
|
-
* useEffect(() => {
|
|
754
|
-
* return () => {
|
|
755
|
-
* form.dispose();
|
|
756
|
-
* };
|
|
757
|
-
* }, []);
|
|
758
|
-
* ```
|
|
759
|
-
*/
|
|
760
|
-
dispose() {
|
|
761
|
-
// Очищаем все subscriptions через SubscriptionManager
|
|
762
|
-
this.disposers.dispose();
|
|
763
|
-
// Рекурсивно очищаем дочерние узлы
|
|
764
|
-
this.fieldRegistry.forEach((field) => {
|
|
765
|
-
if ('dispose' in field && typeof field.dispose === 'function') {
|
|
766
|
-
field.dispose();
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
}
|