@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.
Files changed (99) hide show
  1. package/dist/behaviors-DzYL8kY_.js +499 -0
  2. package/dist/behaviors.d.ts +6 -2
  3. package/dist/behaviors.js +19 -227
  4. package/dist/core/behavior/behavior-context.d.ts +6 -2
  5. package/dist/core/behavior/create-field-path.d.ts +3 -16
  6. package/dist/core/nodes/group-node.d.ts +14 -193
  7. package/dist/core/types/form-context.d.ts +10 -4
  8. package/dist/core/utils/field-path.d.ts +48 -0
  9. package/dist/core/utils/index.d.ts +1 -0
  10. package/dist/core/validation/core/validate-tree.d.ts +10 -4
  11. package/dist/core/validation/field-path.d.ts +3 -39
  12. package/dist/core/validation/validation-context.d.ts +23 -0
  13. package/dist/hooks/types.d.ts +328 -0
  14. package/dist/hooks/useFormControl.d.ts +13 -37
  15. package/dist/hooks/useFormControlValue.d.ts +167 -0
  16. package/dist/hooks/useSignalSubscription.d.ts +17 -0
  17. package/dist/index.d.ts +6 -1
  18. package/dist/index.js +2886 -8
  19. package/dist/{create-field-path-CdPF3lIK.js → registry-helpers-BRxAr6nG.js} +133 -347
  20. package/dist/validators-gXoHPdqM.js +418 -0
  21. package/dist/validators.d.ts +6 -2
  22. package/dist/validators.js +29 -296
  23. package/llms.txt +1283 -22
  24. package/package.json +8 -4
  25. package/dist/core/behavior/behavior-applicator.d.ts +0 -71
  26. package/dist/core/behavior/behavior-applicator.js +0 -92
  27. package/dist/core/behavior/behavior-context.js +0 -38
  28. package/dist/core/behavior/behavior-registry.js +0 -198
  29. package/dist/core/behavior/behaviors/compute-from.js +0 -84
  30. package/dist/core/behavior/behaviors/copy-from.js +0 -64
  31. package/dist/core/behavior/behaviors/enable-when.js +0 -81
  32. package/dist/core/behavior/behaviors/index.js +0 -11
  33. package/dist/core/behavior/behaviors/reset-when.js +0 -63
  34. package/dist/core/behavior/behaviors/revalidate-when.js +0 -51
  35. package/dist/core/behavior/behaviors/sync-fields.js +0 -66
  36. package/dist/core/behavior/behaviors/transform-value.js +0 -110
  37. package/dist/core/behavior/behaviors/watch-field.js +0 -56
  38. package/dist/core/behavior/compose-behavior.js +0 -166
  39. package/dist/core/behavior/create-field-path.js +0 -69
  40. package/dist/core/behavior/index.js +0 -17
  41. package/dist/core/behavior/types.js +0 -7
  42. package/dist/core/context/form-context-impl.js +0 -37
  43. package/dist/core/factories/index.js +0 -6
  44. package/dist/core/factories/node-factory.js +0 -281
  45. package/dist/core/nodes/array-node.js +0 -534
  46. package/dist/core/nodes/field-node.js +0 -510
  47. package/dist/core/nodes/form-node.js +0 -343
  48. package/dist/core/nodes/group-node/field-registry.d.ts +0 -191
  49. package/dist/core/nodes/group-node/field-registry.js +0 -215
  50. package/dist/core/nodes/group-node/index.d.ts +0 -11
  51. package/dist/core/nodes/group-node/index.js +0 -11
  52. package/dist/core/nodes/group-node/proxy-builder.d.ts +0 -71
  53. package/dist/core/nodes/group-node/proxy-builder.js +0 -161
  54. package/dist/core/nodes/group-node/state-manager.d.ts +0 -184
  55. package/dist/core/nodes/group-node/state-manager.js +0 -265
  56. package/dist/core/nodes/group-node.js +0 -770
  57. package/dist/core/types/deep-schema.js +0 -11
  58. package/dist/core/types/field-path.js +0 -4
  59. package/dist/core/types/form-context.js +0 -25
  60. package/dist/core/types/group-node-proxy.js +0 -31
  61. package/dist/core/types/index.js +0 -4
  62. package/dist/core/types/validation-schema.js +0 -10
  63. package/dist/core/utils/create-form.js +0 -24
  64. package/dist/core/utils/debounce.js +0 -197
  65. package/dist/core/utils/error-handler.js +0 -226
  66. package/dist/core/utils/field-path-navigator.js +0 -374
  67. package/dist/core/utils/index.js +0 -14
  68. package/dist/core/utils/registry-helpers.js +0 -79
  69. package/dist/core/utils/registry-stack.js +0 -86
  70. package/dist/core/utils/resources.js +0 -69
  71. package/dist/core/utils/subscription-manager.js +0 -214
  72. package/dist/core/utils/type-guards.js +0 -169
  73. package/dist/core/validation/core/apply-when.js +0 -41
  74. package/dist/core/validation/core/apply.js +0 -38
  75. package/dist/core/validation/core/index.js +0 -8
  76. package/dist/core/validation/core/validate-async.js +0 -45
  77. package/dist/core/validation/core/validate-tree.js +0 -37
  78. package/dist/core/validation/core/validate.js +0 -38
  79. package/dist/core/validation/field-path.js +0 -147
  80. package/dist/core/validation/index.js +0 -33
  81. package/dist/core/validation/validate-form.js +0 -152
  82. package/dist/core/validation/validation-applicator.js +0 -217
  83. package/dist/core/validation/validation-context.js +0 -75
  84. package/dist/core/validation/validation-registry.js +0 -298
  85. package/dist/core/validation/validators/array-validators.js +0 -86
  86. package/dist/core/validation/validators/date.js +0 -117
  87. package/dist/core/validation/validators/email.js +0 -60
  88. package/dist/core/validation/validators/index.js +0 -14
  89. package/dist/core/validation/validators/max-length.js +0 -60
  90. package/dist/core/validation/validators/max.js +0 -60
  91. package/dist/core/validation/validators/min-length.js +0 -60
  92. package/dist/core/validation/validators/min.js +0 -60
  93. package/dist/core/validation/validators/number.js +0 -90
  94. package/dist/core/validation/validators/pattern.js +0 -62
  95. package/dist/core/validation/validators/phone.js +0 -58
  96. package/dist/core/validation/validators/required.js +0 -69
  97. package/dist/core/validation/validators/url.js +0 -55
  98. package/dist/hooks/useFormControl.js +0 -298
  99. package/dist/node-factory-D7DOnSSN.js +0 -3200
@@ -1,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
- }