@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,510 +0,0 @@
1
- /**
2
- * FieldNode - узел поля формы
3
- *
4
- * Представляет одно поле формы с валидацией и состоянием
5
- * Наследует от FormNode и реализует все его абстрактные методы
6
- *
7
- * @group Nodes
8
- */
9
- import { signal, computed, effect } from '@preact/signals-core';
10
- import { FormNode } from './form-node';
11
- import { SubscriptionManager } from '../utils/subscription-manager';
12
- import { FormErrorHandler, ErrorStrategy } from '../utils/error-handler';
13
- /**
14
- * FieldNode - узел для отдельного поля формы
15
- *
16
- * @group Nodes
17
- *
18
- * @example
19
- * ```typescript
20
- * const field = new FieldNode({
21
- * value: '',
22
- * component: Input,
23
- * validators: [required, email],
24
- * });
25
- *
26
- * field.setValue('test@mail.com');
27
- * await field.validate();
28
- * console.log(field.valid.value); // true
29
- * ```
30
- */
31
- export class FieldNode extends FormNode {
32
- // ============================================================================
33
- // Приватные сигналы
34
- // ============================================================================
35
- _value;
36
- _errors;
37
- // _touched, _dirty, _status наследуются от FormNode (protected)
38
- _pending;
39
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
- _componentProps;
41
- // ============================================================================
42
- // Публичные computed signals
43
- // ============================================================================
44
- value;
45
- valid;
46
- invalid;
47
- // touched, dirty, status наследуются от FormNode
48
- pending;
49
- errors;
50
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
- componentProps;
52
- /**
53
- * Вычисляемое свойство: нужно ли показывать ошибку
54
- * Ошибка показывается если поле невалидно И (touched ИЛИ dirty)
55
- */
56
- shouldShowError;
57
- // ============================================================================
58
- // Конфигурация
59
- // ============================================================================
60
- validators;
61
- asyncValidators;
62
- updateOn;
63
- initialValue;
64
- currentValidationId = 0;
65
- debounceMs;
66
- validateDebounceTimer;
67
- validateDebounceResolve;
68
- /**
69
- * Менеджер подписок для централизованного cleanup
70
- * Использует SubscriptionManager вместо массива для управления подписками
71
- */
72
- disposers = new SubscriptionManager();
73
- component;
74
- // ============================================================================
75
- // Конструктор
76
- // ============================================================================
77
- constructor(config) {
78
- super();
79
- // Сохраняем конфигурацию
80
- // FieldConfig.value имеет тип T | null, но FieldNode всегда работает с T
81
- // null трактуется как начальное значение типа T
82
- this.initialValue = config.value;
83
- this.validators = config.validators || [];
84
- this.asyncValidators = config.asyncValidators || [];
85
- this.updateOn = config.updateOn || 'blur';
86
- this.debounceMs = config.debounce || 0;
87
- this.component = config.component;
88
- // Инициализация приватных сигналов
89
- this._value = signal(config.value);
90
- this._errors = signal([]);
91
- // _touched, _dirty, _status инициализируются в FormNode
92
- this._pending = signal(false);
93
- this._componentProps = signal(config.componentProps || {});
94
- // Установка начального статуса если поле отключено
95
- if (config.disabled) {
96
- this._status.value = 'disabled';
97
- }
98
- // Создание computed signals
99
- this.value = computed(() => this._value.value);
100
- this.valid = computed(() => this._status.value === 'valid');
101
- this.invalid = computed(() => this._status.value === 'invalid');
102
- // touched, dirty, status создаются в FormNode
103
- this.pending = computed(() => this._pending.value);
104
- this.errors = computed(() => this._errors.value);
105
- this.componentProps = computed(() => this._componentProps.value);
106
- this.shouldShowError = computed(() => this._status.value === 'invalid' && (this._touched.value || this._dirty.value));
107
- }
108
- // ============================================================================
109
- // Реализация абстрактных методов FormNode
110
- // ============================================================================
111
- getValue() {
112
- return this._value.peek();
113
- }
114
- setValue(value, options) {
115
- this._value.value = value;
116
- this._dirty.value = true;
117
- if (options?.emitEvent === false) {
118
- return;
119
- }
120
- const hasOwnValidators = this.validators.length > 0 || this.asyncValidators.length > 0;
121
- const hasErrors = this._errors.value.length > 0;
122
- // 1. Если updateOn === 'change' → всегда валидируем
123
- if (this.updateOn === 'change') {
124
- this.validate();
125
- return;
126
- }
127
- // 2. Если updateOn === 'blur' или 'submit':
128
- // Валидируем только если у поля есть ошибки и собственные валидаторы
129
- // Это позволяет скрывать ошибку при исправлении значения
130
- // Поведение:
131
- // - Если значение некорректно → обновляем/показываем ошибку
132
- // - Если значение корректно → скрываем ошибку
133
- // Но первая валидация произойдет только при blur/submit
134
- if (hasErrors && hasOwnValidators) {
135
- this.validate();
136
- }
137
- }
138
- patchValue(value) {
139
- this.setValue(value);
140
- }
141
- /**
142
- * Сбросить поле к указанному значению (или к initialValue)
143
- *
144
- * @param value - опциональное значение для сброса. Если не указано, используется initialValue
145
- *
146
- * @remarks
147
- * Этот метод:
148
- * - Устанавливает значение в value или initialValue
149
- * - Очищает ошибки валидации
150
- * - Сбрасывает touched/dirty флаги
151
- * - Устанавливает статус в 'valid'
152
- *
153
- * Если вам нужно сбросить к исходному значению, используйте resetToInitial()
154
- *
155
- * @example
156
- * ```typescript
157
- * // Сброс к initialValue
158
- * field.reset();
159
- *
160
- * // Сброс к новому значению
161
- * field.reset('new value');
162
- * ```
163
- */
164
- reset(value) {
165
- this._value.value = value !== undefined ? value : this.initialValue;
166
- this._errors.value = [];
167
- this._touched.value = false;
168
- this._dirty.value = false;
169
- this._status.value = 'valid';
170
- }
171
- /**
172
- * Сбросить поле к исходному значению (initialValue)
173
- *
174
- * @remarks
175
- * Алиас для reset() без параметров, но более явный:
176
- * - resetToInitial() - явно показывает намерение вернуться к начальному значению
177
- * - reset() - может принимать новое значение
178
- *
179
- * Полезно когда:
180
- * - Пользователь нажал "Cancel" - вернуть форму в исходное состояние
181
- * - Форма была изменена через reset(newValue), но нужно вернуться к самому началу
182
- * - Явное намерение показать "отмену всех изменений"
183
- *
184
- * @example
185
- * ```typescript
186
- * const field = new FieldNode({ value: 'initial', component: Input });
187
- *
188
- * field.setValue('changed');
189
- * field.reset('temp value');
190
- * console.log(field.value.value); // 'temp value'
191
- *
192
- * field.resetToInitial();
193
- * console.log(field.value.value); // 'initial'
194
- * ```
195
- */
196
- resetToInitial() {
197
- this.reset(this.initialValue);
198
- }
199
- /**
200
- * Запустить валидацию поля
201
- * @param options - опции валидации
202
- * @returns `Promise<boolean>` - true если поле валидно
203
- *
204
- * @remarks
205
- * Метод защищен от race conditions через validationId.
206
- * При быстром вводе только последняя валидация применяет результаты.
207
- *
208
- * @example
209
- * ```typescript
210
- * // Обычная валидация
211
- * await field.validate();
212
- *
213
- * // С debounce
214
- * await field.validate({ debounce: 300 });
215
- * ```
216
- */
217
- async validate(options) {
218
- const debounce = options?.debounce ?? this.debounceMs;
219
- // Если задан debounce, откладываем валидацию
220
- if (debounce > 0 && this.asyncValidators.length > 0) {
221
- return new Promise((resolve) => {
222
- // Запоминаем текущий validationId перед debounce
223
- const currentId = this.currentValidationId;
224
- // Resolve предыдущий promise (если есть) как cancelled
225
- if (this.validateDebounceResolve) {
226
- this.validateDebounceResolve(false);
227
- }
228
- // Отменяем предыдущий таймер
229
- if (this.validateDebounceTimer) {
230
- clearTimeout(this.validateDebounceTimer);
231
- }
232
- // Сохраняем resolver для возможности отмены
233
- this.validateDebounceResolve = resolve;
234
- this.validateDebounceTimer = setTimeout(async () => {
235
- // Очищаем resolver
236
- this.validateDebounceResolve = undefined;
237
- // Проверяем, не была ли запущена новая валидация во время debounce
238
- // (другой вызов validate увеличил бы currentValidationId в validateImmediate)
239
- if (currentId !== this.currentValidationId) {
240
- // Эта валидация устарела
241
- resolve(false);
242
- return;
243
- }
244
- const result = await this.validateImmediate();
245
- resolve(result);
246
- }, debounce);
247
- });
248
- }
249
- return this.validateImmediate();
250
- }
251
- /**
252
- * Немедленная валидация без debounce
253
- * @private
254
- * @remarks
255
- * Защищена от race conditions:
256
- * - Проверка validationId после синхронной валидации
257
- * - Проверка перед установкой pending
258
- * - Проверка после Promise.all
259
- * - Проверка перед обработкой async результатов
260
- * - Проверка перед очисткой errors
261
- */
262
- async validateImmediate() {
263
- const validationId = ++this.currentValidationId;
264
- // Синхронная валидация
265
- const syncErrors = [];
266
- for (const validator of this.validators) {
267
- const error = validator(this._value.value);
268
- if (error)
269
- syncErrors.push(error);
270
- }
271
- // Проверка #1: после синхронной валидации
272
- if (validationId !== this.currentValidationId) {
273
- return false; // Эта валидация устарела
274
- }
275
- if (syncErrors.length > 0) {
276
- this._errors.value = syncErrors;
277
- this._status.value = 'invalid';
278
- return false;
279
- }
280
- // Асинхронная валидация - ПАРАЛЛЕЛЬНО
281
- if (this.asyncValidators.length > 0) {
282
- // Проверка #2: перед установкой pending
283
- if (validationId !== this.currentValidationId) {
284
- return false;
285
- }
286
- this._pending.value = true;
287
- this._status.value = 'pending';
288
- // Выполняем все async валидаторы параллельно
289
- // Каждый validator обернут в try-catch для обработки исключений
290
- const asyncResults = await Promise.all(this.asyncValidators.map(async (validator) => {
291
- try {
292
- return await validator(this._value.value);
293
- }
294
- catch (error) {
295
- // Используем централизованный обработчик ошибок
296
- return FormErrorHandler.handle(error, 'FieldNode AsyncValidator', ErrorStrategy.CONVERT);
297
- }
298
- }));
299
- // Проверка #3: после Promise.all (основная проверка)
300
- if (validationId !== this.currentValidationId) {
301
- // Не сбрасываем pending, т.к. новая валидация может еще выполняться
302
- return false;
303
- }
304
- this._pending.value = false;
305
- // Проверка #4: перед обработкой async результатов
306
- if (validationId !== this.currentValidationId) {
307
- return false;
308
- }
309
- const asyncErrors = asyncResults.filter(Boolean);
310
- if (asyncErrors.length > 0) {
311
- this._errors.value = asyncErrors;
312
- this._status.value = 'invalid';
313
- return false;
314
- }
315
- }
316
- // Проверка #5: перед очисткой errors (финальная проверка)
317
- if (validationId !== this.currentValidationId) {
318
- return false;
319
- }
320
- // Очищаем ошибки только если у поля есть собственные валидаторы
321
- // Если валидаторов нет, значит используется ValidationSchema на уровне формы
322
- // и ошибки устанавливаются извне через setErrors()
323
- const hasOwnValidators = this.validators.length > 0 || this.asyncValidators.length > 0;
324
- if (hasOwnValidators) {
325
- this._errors.value = [];
326
- this._status.value = 'valid';
327
- }
328
- return this._errors.value.length === 0;
329
- }
330
- setErrors(errors) {
331
- this._errors.value = errors;
332
- this._status.value = errors.length > 0 ? 'invalid' : 'valid';
333
- }
334
- clearErrors() {
335
- this._errors.value = [];
336
- this._status.value = 'valid';
337
- }
338
- // ============================================================================
339
- // Protected hooks (Template Method pattern)
340
- // ============================================================================
341
- /**
342
- * Hook: вызывается после markAsTouched()
343
- *
344
- * Для FieldNode: если updateOn === 'blur', запускаем валидацию
345
- */
346
- onMarkAsTouched() {
347
- if (this.updateOn === 'blur') {
348
- this.validate();
349
- }
350
- }
351
- /**
352
- * Hook: вызывается после disable()
353
- *
354
- * Для FieldNode: очищаем ошибки валидации
355
- */
356
- onDisable() {
357
- this._errors.value = [];
358
- }
359
- /**
360
- * Hook: вызывается после enable()
361
- *
362
- * Для FieldNode: запускаем валидацию
363
- */
364
- onEnable() {
365
- this.validate();
366
- }
367
- /**
368
- * Обновляет свойства компонента (например, опции для Select)
369
- *
370
- * @example
371
- * ```typescript
372
- * // Обновление опций для Select после загрузки справочников
373
- * form.registrationAddress.city.updateComponentProps({
374
- * options: cities
375
- * });
376
- * ```
377
- */
378
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
379
- updateComponentProps(props) {
380
- this._componentProps.value = {
381
- ...this._componentProps.value,
382
- ...props,
383
- };
384
- }
385
- /**
386
- * Динамически изменяет триггер валидации (updateOn)
387
- * Полезно для адаптивной валидации - например, переключиться на instant feedback после первого submit
388
- *
389
- * @param updateOn - новый триггер валидации: 'change' | 'blur' | 'submit'
390
- *
391
- * @example
392
- * ```typescript
393
- * // Сценарий 1: Instant feedback после submit
394
- * const form = new GroupNode({
395
- * email: {
396
- * value: '',
397
- * component: Input,
398
- * updateOn: 'submit', // Изначально валидация только при submit
399
- * validators: [required, email]
400
- * }
401
- * });
402
- *
403
- * await form.submit(async (values) => {
404
- * // После submit переключаем на instant feedback
405
- * form.email.setUpdateOn('change');
406
- * await api.save(values);
407
- * });
408
- *
409
- * // Теперь валидация происходит при каждом изменении
410
- *
411
- * // Сценарий 2: Прогрессивное улучшение
412
- * form.email.setUpdateOn('blur'); // Сначала только при blur
413
- * // ... пользователь начинает вводить ...
414
- * form.email.setUpdateOn('change'); // Переключаем на change для real-time feedback
415
- * ```
416
- */
417
- setUpdateOn(updateOn) {
418
- this.updateOn = updateOn;
419
- }
420
- getUpdateOn() {
421
- return this.updateOn;
422
- }
423
- // ============================================================================
424
- // Методы-помощники для реактивности (Фаза 1)
425
- // ============================================================================
426
- /**
427
- * Подписка на изменения значения поля
428
- * Автоматически отслеживает изменения через @preact/signals effect
429
- *
430
- * @param callback - Функция, вызываемая при изменении значения
431
- * @returns Функция отписки для cleanup
432
- *
433
- * @example
434
- * ```typescript
435
- * const unsubscribe = form.email.watch((value) => {
436
- * console.log('Email changed:', value);
437
- * });
438
- *
439
- * // Cleanup
440
- * useEffect(() => unsubscribe, []);
441
- * ```
442
- */
443
- watch(callback) {
444
- const dispose = effect(() => {
445
- const currentValue = this.value.value; // track changes
446
- callback(currentValue);
447
- });
448
- // Регистрируем через SubscriptionManager и возвращаем unsubscribe
449
- const key = `watch-${Date.now()}-${Math.random()}`;
450
- return this.disposers.add(key, dispose);
451
- }
452
- /**
453
- * Вычисляемое значение из других полей
454
- * Автоматически обновляет текущее поле при изменении источников
455
- *
456
- * @param sources - Массив ReadonlySignal для отслеживания
457
- * @param computeFn - Функция вычисления нового значения
458
- * @returns Функция отписки для cleanup
459
- *
460
- * @example
461
- * ```typescript
462
- * // Автоматический расчет первоначального взноса (20% от стоимости)
463
- * const dispose = form.initialPayment.computeFrom(
464
- * [form.propertyValue.value],
465
- * (propertyValue) => {
466
- * return propertyValue ? propertyValue * 0.2 : null;
467
- * }
468
- * );
469
- *
470
- * // Cleanup
471
- * useEffect(() => dispose, []);
472
- * ```
473
- */
474
- computeFrom(sources, computeFn) {
475
- const dispose = effect(() => {
476
- // Читаем все источники для отслеживания
477
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
478
- const sourceValues = sources.map((source) => source.value);
479
- // Вычисляем новое значение
480
- const newValue = computeFn(...sourceValues);
481
- // Устанавливаем значение без триггера событий (избегаем циклов)
482
- this.setValue(newValue, { emitEvent: false });
483
- });
484
- // Регистрируем через SubscriptionManager и возвращаем unsubscribe
485
- const key = `computeFrom-${Date.now()}-${Math.random()}`;
486
- return this.disposers.add(key, dispose);
487
- }
488
- /**
489
- * Очистить все ресурсы и таймеры
490
- * Должен вызываться при unmount компонента
491
- *
492
- * @example
493
- * ```typescript
494
- * useEffect(() => {
495
- * return () => {
496
- * field.dispose();
497
- * };
498
- * }, []);
499
- * ```
500
- */
501
- dispose() {
502
- // Очищаем все subscriptions через SubscriptionManager
503
- this.disposers.dispose();
504
- // Очищаем debounce таймер если он есть
505
- if (this.validateDebounceTimer) {
506
- clearTimeout(this.validateDebounceTimer);
507
- this.validateDebounceTimer = undefined;
508
- }
509
- }
510
- }