@reformer/core 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/behaviors/compute-from.d.ts +2 -0
- package/dist/behaviors/compute-from.js +31 -0
- package/dist/behaviors/copy-from.d.ts +2 -0
- package/dist/behaviors/copy-from.js +29 -0
- package/dist/behaviors/enable-when.d.ts +2 -0
- package/dist/behaviors/enable-when.js +25 -0
- package/dist/behaviors/reset-when.d.ts +2 -0
- package/dist/behaviors/reset-when.js +24 -0
- package/dist/behaviors/revalidate-when.d.ts +2 -0
- package/dist/behaviors/revalidate-when.js +18 -0
- package/dist/behaviors/sync-fields.d.ts +2 -0
- package/dist/behaviors/sync-fields.js +41 -0
- package/dist/behaviors/transform-value.d.ts +2 -0
- package/dist/behaviors/transform-value.js +45 -0
- package/dist/behaviors/watch-field.d.ts +2 -0
- package/dist/behaviors/watch-field.js +21 -0
- package/dist/behaviors.d.ts +6 -0
- package/dist/behaviors.js +29 -0
- package/dist/core/behavior/behavior-context.d.ts +47 -0
- package/dist/core/behavior/behavior-registry.d.ts +85 -0
- package/dist/core/behavior/behaviors/compute-from.d.ts +70 -0
- package/dist/core/behavior/behaviors/copy-from.d.ts +56 -0
- package/dist/core/behavior/behaviors/enable-when.d.ts +118 -0
- package/dist/core/behavior/behaviors/index.d.ts +11 -0
- package/dist/core/behavior/behaviors/reset-when.d.ts +64 -0
- package/dist/core/behavior/behaviors/revalidate-when.d.ts +53 -0
- package/dist/core/behavior/behaviors/sync-fields.d.ts +48 -0
- package/dist/core/behavior/behaviors/transform-value.d.ts +192 -0
- package/dist/core/behavior/behaviors/watch-field.d.ts +80 -0
- package/dist/core/behavior/compose-behavior.d.ts +96 -0
- package/dist/core/behavior/index.d.ts +11 -0
- package/dist/core/behavior/types.d.ts +146 -0
- package/dist/core/factories/index.d.ts +6 -0
- package/dist/core/factories/node-factory.d.ts +186 -0
- package/dist/core/nodes/array-node.d.ts +328 -0
- package/dist/core/nodes/field-node.d.ts +294 -0
- package/dist/core/nodes/form-node.d.ts +340 -0
- package/dist/core/nodes/group-node.d.ts +319 -0
- package/dist/core/types/deep-schema.d.ts +68 -0
- package/dist/core/types/field-path.d.ts +42 -0
- package/dist/core/types/form-context.d.ts +89 -0
- package/dist/core/types/form-proxy.d.ts +105 -0
- package/dist/core/types/index.d.ts +209 -0
- package/dist/core/types/validation-schema.d.ts +95 -0
- package/dist/core/utils/abstract-registry.d.ts +74 -0
- package/dist/core/utils/aggregate-signals.d.ts +71 -0
- package/dist/core/utils/create-form.d.ts +44 -0
- package/dist/core/utils/error-handler.d.ts +163 -0
- package/dist/core/utils/field-path-navigator.d.ts +240 -0
- package/dist/core/utils/field-path.d.ts +60 -0
- package/dist/core/utils/form-observer.d.ts +176 -0
- package/dist/core/utils/form-proxy-builder.d.ts +25 -0
- package/dist/core/utils/form-submitter.d.ts +121 -0
- package/dist/core/utils/index.d.ts +22 -0
- package/dist/core/utils/registry-helpers.d.ts +43 -0
- package/dist/core/utils/registry-stack.d.ts +69 -0
- package/dist/core/utils/safe-effect.d.ts +73 -0
- package/dist/core/utils/status-machine.d.ts +153 -0
- package/dist/core/utils/subscription-manager.d.ts +180 -0
- package/dist/core/utils/type-guards.d.ts +98 -0
- package/dist/core/utils/unique-id.d.ts +53 -0
- package/dist/core/validation/core/apply-when.d.ts +22 -0
- package/dist/core/validation/core/apply.d.ts +52 -0
- package/dist/core/validation/core/index.d.ts +8 -0
- package/dist/core/validation/core/validate-async.d.ts +36 -0
- package/dist/core/validation/core/validate-tree.d.ts +35 -0
- package/dist/core/validation/core/validate.d.ts +26 -0
- package/dist/core/validation/index.d.ts +27 -0
- package/dist/core/validation/validate-form.d.ts +48 -0
- package/dist/core/validation/validation-applicator.d.ts +70 -0
- package/dist/core/validation/validation-context.d.ts +86 -0
- package/dist/core/validation/validation-registry.d.ts +142 -0
- package/dist/core/validation/validators/array-validators.d.ts +53 -0
- package/dist/core/validation/validators/date-utils.d.ts +26 -0
- package/dist/core/validation/validators/email.d.ts +37 -0
- package/dist/core/validation/validators/future-date.d.ts +35 -0
- package/dist/core/validation/validators/index.d.ts +20 -0
- package/dist/core/validation/validators/is-date.d.ts +36 -0
- package/dist/core/validation/validators/max-age.d.ts +36 -0
- package/dist/core/validation/validators/max-date.d.ts +36 -0
- package/dist/core/validation/validators/max-length.d.ts +38 -0
- package/dist/core/validation/validators/max.d.ts +38 -0
- package/dist/core/validation/validators/min-age.d.ts +36 -0
- package/dist/core/validation/validators/min-date.d.ts +36 -0
- package/dist/core/validation/validators/min-length.d.ts +38 -0
- package/dist/core/validation/validators/min.d.ts +38 -0
- package/dist/core/validation/validators/number.d.ts +31 -0
- package/dist/core/validation/validators/past-date.d.ts +35 -0
- package/dist/core/validation/validators/pattern.d.ts +40 -0
- package/dist/core/validation/validators/phone.d.ts +27 -0
- package/dist/core/validation/validators/required.d.ts +41 -0
- package/dist/core/validation/validators/url.d.ts +22 -0
- package/dist/date-utils-xUWFslTj.js +29 -0
- package/dist/field-path-DuKdGcIE.js +66 -0
- package/dist/hooks/types.d.ts +328 -0
- package/dist/hooks/useArrayLength.d.ts +31 -0
- package/dist/hooks/useFormControl.d.ts +24 -0
- package/dist/hooks/useFormControlValue.d.ts +167 -0
- package/dist/hooks/useHiddenCondition.d.ts +25 -0
- package/dist/hooks/useSignalSubscription.d.ts +17 -0
- package/dist/index-D25LsbRm.js +73 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +3271 -0
- package/dist/registry-helpers-Bv_BJ1s-.js +615 -0
- package/dist/safe-effect-Dh8uw81c.js +20 -0
- package/dist/validate-C3XiA_zf.js +10 -0
- package/dist/validators/email.d.ts +2 -0
- package/dist/validators/email.js +13 -0
- package/dist/validators/future-date.d.ts +2 -0
- package/dist/validators/future-date.js +20 -0
- package/dist/validators/is-date.d.ts +2 -0
- package/dist/validators/is-date.js +12 -0
- package/dist/validators/max-age.d.ts +2 -0
- package/dist/validators/max-age.js +20 -0
- package/dist/validators/max-date.d.ts +2 -0
- package/dist/validators/max-date.js +20 -0
- package/dist/validators/max-length.d.ts +2 -0
- package/dist/validators/max-length.js +11 -0
- package/dist/validators/max.d.ts +2 -0
- package/dist/validators/max.js +11 -0
- package/dist/validators/min-age.d.ts +2 -0
- package/dist/validators/min-age.js +20 -0
- package/dist/validators/min-date.d.ts +2 -0
- package/dist/validators/min-date.js +20 -0
- package/dist/validators/min-length.d.ts +2 -0
- package/dist/validators/min-length.js +11 -0
- package/dist/validators/min.d.ts +2 -0
- package/dist/validators/min.js +11 -0
- package/dist/validators/number.d.ts +2 -0
- package/dist/validators/number.js +35 -0
- package/dist/validators/past-date.d.ts +2 -0
- package/dist/validators/past-date.js +20 -0
- package/dist/validators/pattern.d.ts +2 -0
- package/dist/validators/pattern.js +11 -0
- package/dist/validators/phone.d.ts +2 -0
- package/dist/validators/phone.js +35 -0
- package/dist/validators/required.d.ts +2 -0
- package/dist/validators/required.js +15 -0
- package/dist/validators/url.d.ts +2 -0
- package/dist/validators/url.js +19 -0
- package/dist/validators-BGsNOgT1.js +207 -0
- package/dist/validators.d.ts +6 -0
- package/dist/validators.js +56 -0
- package/llms.txt +9351 -0
- package/package.json +166 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { ReadonlySignal } from '@preact/signals-core';
|
|
2
|
+
import { FormNode, SetValueOptions } from './form-node';
|
|
3
|
+
import { FieldStatus, ValidationError, FormFields } from '../types';
|
|
4
|
+
import { FormSchema } from '../types/deep-schema';
|
|
5
|
+
import { FormProxy } from '../types/form-proxy';
|
|
6
|
+
/**
|
|
7
|
+
* ArrayNode - массив форм с реактивным состоянием
|
|
8
|
+
*
|
|
9
|
+
* @group Nodes
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const array = new ArrayNode({
|
|
14
|
+
* title: { value: '', component: Input },
|
|
15
|
+
* price: { value: 0, component: Input },
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* array.push({ title: 'Item 1', price: 100 });
|
|
19
|
+
* array.at(0)?.title.setValue('Updated');
|
|
20
|
+
* console.log(array.length.value); // 1
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class ArrayNode<T extends FormFields> extends FormNode<T[]> {
|
|
24
|
+
private items;
|
|
25
|
+
private itemSchema;
|
|
26
|
+
private initialItems;
|
|
27
|
+
/**
|
|
28
|
+
* Менеджер подписок для централизованного cleanup
|
|
29
|
+
* Использует SubscriptionManager вместо массива для управления подписками
|
|
30
|
+
*/
|
|
31
|
+
private disposers;
|
|
32
|
+
/** Array-level validation errors (e.g., "минимум 1 элемент") */
|
|
33
|
+
private readonly _arrayErrors;
|
|
34
|
+
private validationSchemaFn?;
|
|
35
|
+
private behaviorSchemaFn?;
|
|
36
|
+
readonly value: ReadonlySignal<T[]>;
|
|
37
|
+
readonly valid: ReadonlySignal<boolean>;
|
|
38
|
+
readonly invalid: ReadonlySignal<boolean>;
|
|
39
|
+
readonly touched: ReadonlySignal<boolean>;
|
|
40
|
+
readonly dirty: ReadonlySignal<boolean>;
|
|
41
|
+
readonly pending: ReadonlySignal<boolean>;
|
|
42
|
+
readonly errors: ReadonlySignal<ValidationError[]>;
|
|
43
|
+
readonly status: ReadonlySignal<FieldStatus>;
|
|
44
|
+
readonly length: ReadonlySignal<number>;
|
|
45
|
+
constructor(schema: FormSchema<T>, initialItems?: Partial<T>[]);
|
|
46
|
+
/**
|
|
47
|
+
* Добавить элемент в конец массива
|
|
48
|
+
* @param initialValue - Начальные значения для нового элемента
|
|
49
|
+
*/
|
|
50
|
+
push(initialValue?: Partial<T>): void;
|
|
51
|
+
/**
|
|
52
|
+
* Удалить элемент по индексу
|
|
53
|
+
* @param index - Индекс элемента для удаления
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Вызывает dispose() на удаляемом элементе для очистки подписок
|
|
57
|
+
*/
|
|
58
|
+
removeAt(index: number): void;
|
|
59
|
+
/**
|
|
60
|
+
* Вставить элемент в массив
|
|
61
|
+
* @param index - Индекс для вставки
|
|
62
|
+
* @param initialValue - Начальные значения для нового элемента
|
|
63
|
+
*/
|
|
64
|
+
insert(index: number, initialValue?: Partial<T>): void;
|
|
65
|
+
/**
|
|
66
|
+
* Удалить все элементы массива
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* Вызывает dispose() на всех элементах для очистки подписок
|
|
70
|
+
*/
|
|
71
|
+
clear(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Получить элемент по индексу
|
|
74
|
+
* @param index - Индекс элемента
|
|
75
|
+
* @returns Типизированный GroupNode proxy или undefined если индекс вне границ
|
|
76
|
+
*/
|
|
77
|
+
at(index: number): FormProxy<T> | undefined;
|
|
78
|
+
getValue(): T[];
|
|
79
|
+
setValue(values: T[], options?: SetValueOptions): void;
|
|
80
|
+
patchValue(values: (T | undefined)[]): void;
|
|
81
|
+
/**
|
|
82
|
+
* Сбросить массив к указанным значениям (или очистить)
|
|
83
|
+
*
|
|
84
|
+
* @param values - опциональный массив значений для сброса
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* Очищает текущий массив и заполняет новыми элементами
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* // Очистить массив
|
|
92
|
+
* arrayNode.reset();
|
|
93
|
+
*
|
|
94
|
+
* // Сбросить к новым значениям
|
|
95
|
+
* arrayNode.reset([{ name: 'Item 1' }, { name: 'Item 2' }]);
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
reset(values?: T[]): void;
|
|
99
|
+
/**
|
|
100
|
+
* Сбросить массив к исходным значениям (initialItems)
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* Восстанавливает массив в состояние, которое было при создании ArrayNode.
|
|
104
|
+
* Более явный способ сброса к начальным значениям по сравнению с reset()
|
|
105
|
+
*
|
|
106
|
+
* Полезно когда:
|
|
107
|
+
* - Пользователь нажал "Cancel" - вернуть массив к исходным элементам
|
|
108
|
+
* - Массив был изменен через reset(newValues), но нужно вернуться к началу
|
|
109
|
+
* - Явное намерение показать "отмена всех изменений"
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const arrayNode = new ArrayNode(
|
|
114
|
+
* { name: { value: '', component: Input } },
|
|
115
|
+
* [{ name: 'Initial 1' }, { name: 'Initial 2' }]
|
|
116
|
+
* );
|
|
117
|
+
*
|
|
118
|
+
* arrayNode.push({ name: 'New Item' });
|
|
119
|
+
* arrayNode.reset([{ name: 'Temp' }]);
|
|
120
|
+
* console.log(arrayNode.length.value); // 1
|
|
121
|
+
*
|
|
122
|
+
* arrayNode.resetToInitial();
|
|
123
|
+
* console.log(arrayNode.length.value); // 2
|
|
124
|
+
* console.log(arrayNode.at(0)?.name.value.value); // 'Initial 1'
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
resetToInitial(): void;
|
|
128
|
+
validate(): Promise<boolean>;
|
|
129
|
+
/**
|
|
130
|
+
* Установить array-level validation errors
|
|
131
|
+
*
|
|
132
|
+
* @param errors - Массив ошибок валидации уровня массива
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* arrayNode.setErrors([{
|
|
137
|
+
* code: 'minItems',
|
|
138
|
+
* message: 'Минимум 1 элемент обязателен',
|
|
139
|
+
* }]);
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
setErrors(errors: ValidationError[]): void;
|
|
143
|
+
/**
|
|
144
|
+
* Очистить все errors (array-level + item-level)
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* arrayNode.clearErrors();
|
|
149
|
+
* console.log(arrayNode.errors.value); // []
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
clearErrors(): void;
|
|
153
|
+
/**
|
|
154
|
+
* Hook: вызывается после markAsTouched()
|
|
155
|
+
*
|
|
156
|
+
* Для ArrayNode: рекурсивно помечаем все элементы массива как touched
|
|
157
|
+
*/
|
|
158
|
+
protected onMarkAsTouched(): void;
|
|
159
|
+
/**
|
|
160
|
+
* Hook: вызывается после markAsUntouched()
|
|
161
|
+
*
|
|
162
|
+
* Для ArrayNode: рекурсивно помечаем все элементы массива как untouched
|
|
163
|
+
*/
|
|
164
|
+
protected onMarkAsUntouched(): void;
|
|
165
|
+
/**
|
|
166
|
+
* Hook: вызывается после markAsDirty()
|
|
167
|
+
*
|
|
168
|
+
* Для ArrayNode: рекурсивно помечаем все элементы массива как dirty
|
|
169
|
+
*/
|
|
170
|
+
protected onMarkAsDirty(): void;
|
|
171
|
+
/**
|
|
172
|
+
* Hook: вызывается после markAsPristine()
|
|
173
|
+
*
|
|
174
|
+
* Для ArrayNode: рекурсивно помечаем все элементы массива как pristine
|
|
175
|
+
*/
|
|
176
|
+
protected onMarkAsPristine(): void;
|
|
177
|
+
/**
|
|
178
|
+
* Итерировать по элементам массива
|
|
179
|
+
* @param callback - Функция, вызываемая для каждого элемента с типизированным GroupNode proxy
|
|
180
|
+
*/
|
|
181
|
+
forEach(callback: (item: FormProxy<T>, index: number) => void): void;
|
|
182
|
+
/**
|
|
183
|
+
* Маппинг элементов массива
|
|
184
|
+
* @param callback - Функция преобразования с типизированным GroupNode proxy
|
|
185
|
+
* @returns Новый массив результатов
|
|
186
|
+
*/
|
|
187
|
+
map<R>(callback: (item: FormProxy<T>, index: number) => R): R[];
|
|
188
|
+
/**
|
|
189
|
+
* Создать новый элемент массива на основе схемы
|
|
190
|
+
* @param initialValue - Начальные значения
|
|
191
|
+
*/
|
|
192
|
+
private createItem;
|
|
193
|
+
/**
|
|
194
|
+
* Проверить, является ли схема групповой (объект полей)
|
|
195
|
+
* @param schema - Схема для проверки
|
|
196
|
+
*/
|
|
197
|
+
private isGroupSchema;
|
|
198
|
+
/**
|
|
199
|
+
* Применить validation schema ко всем элементам массива
|
|
200
|
+
*
|
|
201
|
+
* Validation schema будет применена к:
|
|
202
|
+
* - Всем существующим элементам
|
|
203
|
+
* - Всем новым элементам, добавляемым через push/insert
|
|
204
|
+
*
|
|
205
|
+
* @param schemaFn - Функция валидации для элемента массива
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* import { propertyValidation } from './validation/property-validation';
|
|
210
|
+
*
|
|
211
|
+
* form.properties.applyValidationSchema(propertyValidation);
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
applyValidationSchema(schemaFn: unknown): void;
|
|
215
|
+
/**
|
|
216
|
+
* Применить behavior schema ко всем элементам ArrayNode
|
|
217
|
+
*
|
|
218
|
+
* Автоматически применяется к новым элементам при push/insert.
|
|
219
|
+
*
|
|
220
|
+
* @param schemaFn - Behavior schema функция
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* import { addressBehavior } from './behaviors/address-behavior';
|
|
225
|
+
*
|
|
226
|
+
* form.addresses.applyBehaviorSchema(addressBehavior);
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
applyBehaviorSchema(schemaFn: unknown): void;
|
|
230
|
+
/**
|
|
231
|
+
* Подписка на изменения конкретного поля во всех элементах массива
|
|
232
|
+
* Срабатывает при изменении значения поля в любом элементе
|
|
233
|
+
*
|
|
234
|
+
* @param fieldKey - Ключ поля для отслеживания
|
|
235
|
+
* @param callback - Функция, вызываемая при изменении, получает массив всех значений и индекс измененного элемента
|
|
236
|
+
* @returns Функция отписки для cleanup
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* // Автоматический пересчет общей стоимости при изменении цен
|
|
241
|
+
* const dispose = form.existingLoans.watchItems(
|
|
242
|
+
* 'remainingAmount',
|
|
243
|
+
* (amounts) => {
|
|
244
|
+
* const totalDebt = amounts.reduce((sum, amount) => sum + (amount || 0), 0);
|
|
245
|
+
* form.totalDebt.setValue(totalDebt);
|
|
246
|
+
* }
|
|
247
|
+
* );
|
|
248
|
+
*
|
|
249
|
+
* // При изменении любого remainingAmount → пересчитается totalDebt
|
|
250
|
+
* form.existingLoans.at(0)?.remainingAmount.setValue(500000);
|
|
251
|
+
*
|
|
252
|
+
* // Cleanup
|
|
253
|
+
* useEffect(() => dispose, []);
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
watchItems<K extends keyof T>(fieldKey: K, callback: (values: Array<T[K] | undefined>) => void | Promise<void>): () => void;
|
|
257
|
+
/**
|
|
258
|
+
* Подписка на изменение длины массива
|
|
259
|
+
* Срабатывает при добавлении/удалении элементов
|
|
260
|
+
*
|
|
261
|
+
* @param callback - Функция, вызываемая при изменении длины, получает новую длину
|
|
262
|
+
* @returns Функция отписки для cleanup
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* // Обновление счетчика элементов в UI
|
|
267
|
+
* const dispose = form.properties.watchLength((length) => {
|
|
268
|
+
* console.log(`Количество объектов недвижимости: ${length}`);
|
|
269
|
+
* form.propertyCount.setValue(length);
|
|
270
|
+
* });
|
|
271
|
+
*
|
|
272
|
+
* form.properties.push({ title: 'Квартира', value: 5000000 });
|
|
273
|
+
* // Выведет: "Количество объектов недвижимости: 1"
|
|
274
|
+
*
|
|
275
|
+
* // Cleanup
|
|
276
|
+
* useEffect(() => dispose, []);
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
watchLength(callback: (length: number) => void | Promise<void>): () => void;
|
|
280
|
+
/**
|
|
281
|
+
* Очистить все ресурсы узла
|
|
282
|
+
* Рекурсивно очищает все subscriptions и элементы массива
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```typescript
|
|
286
|
+
* useEffect(() => {
|
|
287
|
+
* return () => {
|
|
288
|
+
* arrayNode.dispose();
|
|
289
|
+
* };
|
|
290
|
+
* }, []);
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
dispose(): void;
|
|
294
|
+
/**
|
|
295
|
+
* Hook: вызывается после disable()
|
|
296
|
+
*
|
|
297
|
+
* Для ArrayNode: рекурсивно отключаем все элементы массива
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```typescript
|
|
301
|
+
* // Отключить весь массив полей
|
|
302
|
+
* form.items.disable();
|
|
303
|
+
*
|
|
304
|
+
* // Все элементы становятся disabled
|
|
305
|
+
* form.items.forEach(item => {
|
|
306
|
+
* console.log(item.status.value); // 'disabled'
|
|
307
|
+
* });
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
protected onDisable(): void;
|
|
311
|
+
/**
|
|
312
|
+
* Hook: вызывается после enable()
|
|
313
|
+
*
|
|
314
|
+
* Для ArrayNode: рекурсивно включаем все элементы массива
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```typescript
|
|
318
|
+
* // Включить весь массив полей
|
|
319
|
+
* form.items.enable();
|
|
320
|
+
*
|
|
321
|
+
* // Все элементы становятся enabled
|
|
322
|
+
* form.items.forEach(item => {
|
|
323
|
+
* console.log(item.status.value); // 'valid' или 'invalid'
|
|
324
|
+
* });
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
protected onEnable(): void;
|
|
328
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { ReadonlySignal } from '@preact/signals-core';
|
|
2
|
+
import { FormNode, SetValueOptions } from './form-node';
|
|
3
|
+
import { FieldConfig, FieldStatus, ValidationError } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* FieldNode - узел для отдельного поля формы
|
|
6
|
+
*
|
|
7
|
+
* @group Nodes
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const field = new FieldNode({
|
|
12
|
+
* value: '',
|
|
13
|
+
* component: Input,
|
|
14
|
+
* validators: [required, email],
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* field.setValue('test@mail.com');
|
|
18
|
+
* await field.validate();
|
|
19
|
+
* console.log(field.valid.value); // true
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class FieldNode<T> extends FormNode<T> {
|
|
23
|
+
private _value;
|
|
24
|
+
private _errors;
|
|
25
|
+
private _componentProps;
|
|
26
|
+
/**
|
|
27
|
+
* State machine для управления статусом поля
|
|
28
|
+
* Централизует логику переходов между valid/invalid/pending/disabled
|
|
29
|
+
*/
|
|
30
|
+
private readonly statusMachine;
|
|
31
|
+
readonly value: ReadonlySignal<T>;
|
|
32
|
+
readonly valid: ReadonlySignal<boolean>;
|
|
33
|
+
readonly invalid: ReadonlySignal<boolean>;
|
|
34
|
+
readonly pending: ReadonlySignal<boolean>;
|
|
35
|
+
readonly status: ReadonlySignal<FieldStatus>;
|
|
36
|
+
readonly disabled: ReadonlySignal<boolean>;
|
|
37
|
+
readonly errors: ReadonlySignal<ValidationError[]>;
|
|
38
|
+
readonly componentProps: ReadonlySignal<Record<string, any>>;
|
|
39
|
+
/**
|
|
40
|
+
* Вычисляемое свойство: нужно ли показывать ошибку
|
|
41
|
+
* Ошибка показывается если поле невалидно И (touched ИЛИ dirty)
|
|
42
|
+
*/
|
|
43
|
+
readonly shouldShowError: ReadonlySignal<boolean>;
|
|
44
|
+
private validators;
|
|
45
|
+
private asyncValidators;
|
|
46
|
+
private updateOn;
|
|
47
|
+
private initialValue;
|
|
48
|
+
private currentAbortController?;
|
|
49
|
+
private debounceMs;
|
|
50
|
+
private validateDebounceTimer?;
|
|
51
|
+
/**
|
|
52
|
+
* Pending debounced validation state
|
|
53
|
+
* Contains resolve function and AbortController for cancellation
|
|
54
|
+
*/
|
|
55
|
+
private pendingValidation?;
|
|
56
|
+
/**
|
|
57
|
+
* Менеджер подписок для централизованного cleanup
|
|
58
|
+
* Использует SubscriptionManager вместо массива для управления подписками
|
|
59
|
+
*/
|
|
60
|
+
private disposers;
|
|
61
|
+
readonly component: FieldConfig<T>['component'];
|
|
62
|
+
constructor(config: FieldConfig<T>);
|
|
63
|
+
getValue(): T;
|
|
64
|
+
setValue(value: T, options?: SetValueOptions): void;
|
|
65
|
+
patchValue(value: Partial<T>): void;
|
|
66
|
+
/**
|
|
67
|
+
* Сбросить поле к указанному значению (или к initialValue)
|
|
68
|
+
*
|
|
69
|
+
* @param value - опциональное значение для сброса. Если не указано, используется initialValue
|
|
70
|
+
*
|
|
71
|
+
* @remarks
|
|
72
|
+
* Этот метод:
|
|
73
|
+
* - Устанавливает значение в value или initialValue
|
|
74
|
+
* - Очищает ошибки валидации
|
|
75
|
+
* - Сбрасывает touched/dirty флаги
|
|
76
|
+
* - Устанавливает статус в 'valid'
|
|
77
|
+
*
|
|
78
|
+
* Если вам нужно сбросить к исходному значению, используйте resetToInitial()
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* // Сброс к initialValue
|
|
83
|
+
* field.reset();
|
|
84
|
+
*
|
|
85
|
+
* // Сброс к новому значению
|
|
86
|
+
* field.reset('new value');
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
reset(value?: T): void;
|
|
90
|
+
/**
|
|
91
|
+
* Сбросить поле к исходному значению (initialValue)
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* Алиас для reset() без параметров, но более явный:
|
|
95
|
+
* - resetToInitial() - явно показывает намерение вернуться к начальному значению
|
|
96
|
+
* - reset() - может принимать новое значение
|
|
97
|
+
*
|
|
98
|
+
* Полезно когда:
|
|
99
|
+
* - Пользователь нажал "Cancel" - вернуть форму в исходное состояние
|
|
100
|
+
* - Форма была изменена через reset(newValue), но нужно вернуться к самому началу
|
|
101
|
+
* - Явное намерение показать "отмену всех изменений"
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const field = new FieldNode({ value: 'initial', component: Input });
|
|
106
|
+
*
|
|
107
|
+
* field.setValue('changed');
|
|
108
|
+
* field.reset('temp value');
|
|
109
|
+
* console.log(field.value.value); // 'temp value'
|
|
110
|
+
*
|
|
111
|
+
* field.resetToInitial();
|
|
112
|
+
* console.log(field.value.value); // 'initial'
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
resetToInitial(): void;
|
|
116
|
+
/**
|
|
117
|
+
* Cancel any pending validation (debounced or running)
|
|
118
|
+
* @private
|
|
119
|
+
* @remarks
|
|
120
|
+
* Centralizes all cancellation logic:
|
|
121
|
+
* - Aborts pending debounced validation and resolves its promise
|
|
122
|
+
* - Clears debounce timer
|
|
123
|
+
* - Aborts currently running async validation
|
|
124
|
+
*/
|
|
125
|
+
private cancelPendingValidation;
|
|
126
|
+
/**
|
|
127
|
+
* Запустить валидацию поля
|
|
128
|
+
* @param options - опции валидации
|
|
129
|
+
* @returns `Promise<boolean>` - true если поле валидно
|
|
130
|
+
*
|
|
131
|
+
* @remarks
|
|
132
|
+
* Метод защищен от race conditions через AbortController.
|
|
133
|
+
* При быстром вводе только последняя валидация применяет результаты.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* // Обычная валидация
|
|
138
|
+
* await field.validate();
|
|
139
|
+
*
|
|
140
|
+
* // С debounce
|
|
141
|
+
* await field.validate({ debounce: 300 });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
validate(options?: {
|
|
145
|
+
debounce?: number;
|
|
146
|
+
}): Promise<boolean>;
|
|
147
|
+
/**
|
|
148
|
+
* Немедленная валидация без debounce
|
|
149
|
+
* @private
|
|
150
|
+
* @param providedController - AbortController from debounced validate()
|
|
151
|
+
* @remarks
|
|
152
|
+
* Защищена от race conditions через AbortController:
|
|
153
|
+
* - Отменяет предыдущую валидацию при запуске новой (if no controller provided)
|
|
154
|
+
* - Передаёт AbortSignal в async валидаторы для отмены операций (например, fetch)
|
|
155
|
+
* - Проверяет signal.aborted в ключевых точках
|
|
156
|
+
*/
|
|
157
|
+
private validateImmediate;
|
|
158
|
+
setErrors(errors: ValidationError[]): void;
|
|
159
|
+
clearErrors(): void;
|
|
160
|
+
/**
|
|
161
|
+
* Hook: вызывается после markAsTouched()
|
|
162
|
+
*
|
|
163
|
+
* Для FieldNode: если updateOn === 'blur', запускаем валидацию
|
|
164
|
+
*/
|
|
165
|
+
protected onMarkAsTouched(): void;
|
|
166
|
+
/**
|
|
167
|
+
* Hook: вызывается после disable()
|
|
168
|
+
*
|
|
169
|
+
* Для FieldNode: синхронизируем statusMachine и очищаем ошибки
|
|
170
|
+
*/
|
|
171
|
+
protected onDisable(): void;
|
|
172
|
+
/**
|
|
173
|
+
* Hook: вызывается после enable()
|
|
174
|
+
*
|
|
175
|
+
* Для FieldNode: синхронизируем statusMachine и запускаем валидацию
|
|
176
|
+
*/
|
|
177
|
+
protected onEnable(): void;
|
|
178
|
+
/**
|
|
179
|
+
* Обновляет свойства компонента (например, опции для Select)
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* // Обновление опций для Select после загрузки справочников
|
|
184
|
+
* form.registrationAddress.city.updateComponentProps({
|
|
185
|
+
* options: cities
|
|
186
|
+
* });
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
updateComponentProps(props: Partial<Record<string, any>>): void;
|
|
190
|
+
/**
|
|
191
|
+
* Динамически изменяет триггер валидации (updateOn)
|
|
192
|
+
* Полезно для адаптивной валидации - например, переключиться на instant feedback после первого submit
|
|
193
|
+
*
|
|
194
|
+
* @param updateOn - новый триггер валидации: 'change' | 'blur' | 'submit'
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* // Сценарий 1: Instant feedback после submit
|
|
199
|
+
* const form = new GroupNode({
|
|
200
|
+
* email: {
|
|
201
|
+
* value: '',
|
|
202
|
+
* component: Input,
|
|
203
|
+
* updateOn: 'submit', // Изначально валидация только при submit
|
|
204
|
+
* validators: [required, email]
|
|
205
|
+
* }
|
|
206
|
+
* });
|
|
207
|
+
*
|
|
208
|
+
* await form.submit(async (values) => {
|
|
209
|
+
* // После submit переключаем на instant feedback
|
|
210
|
+
* form.email.setUpdateOn('change');
|
|
211
|
+
* await api.save(values);
|
|
212
|
+
* });
|
|
213
|
+
*
|
|
214
|
+
* // Теперь валидация происходит при каждом изменении
|
|
215
|
+
*
|
|
216
|
+
* // Сценарий 2: Прогрессивное улучшение
|
|
217
|
+
* form.email.setUpdateOn('blur'); // Сначала только при blur
|
|
218
|
+
* // ... пользователь начинает вводить ...
|
|
219
|
+
* form.email.setUpdateOn('change'); // Переключаем на change для real-time feedback
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
setUpdateOn(updateOn: 'change' | 'blur' | 'submit'): void;
|
|
223
|
+
getUpdateOn(): 'change' | 'blur' | 'submit';
|
|
224
|
+
/**
|
|
225
|
+
* Подписка на изменения значения поля
|
|
226
|
+
* Автоматически отслеживает изменения через @preact/signals effect
|
|
227
|
+
*
|
|
228
|
+
* @param callback - Функция, вызываемая при изменении значения.
|
|
229
|
+
* Для async операций передается AbortSignal во втором параметре.
|
|
230
|
+
* @returns Функция отписки для cleanup
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```typescript
|
|
234
|
+
* // Синхронный callback
|
|
235
|
+
* const unsubscribe = form.email.watch((value) => {
|
|
236
|
+
* console.log('Email changed:', value);
|
|
237
|
+
* });
|
|
238
|
+
*
|
|
239
|
+
* // Асинхронный callback с поддержкой отмены
|
|
240
|
+
* const unsubscribe = form.email.watch(async (value, signal) => {
|
|
241
|
+
* const result = await fetch('/api/validate', { signal });
|
|
242
|
+
* // ...
|
|
243
|
+
* });
|
|
244
|
+
*
|
|
245
|
+
* // Cleanup
|
|
246
|
+
* useEffect(() => unsubscribe, []);
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
watch(callback: (value: T, signal: AbortSignal) => void | Promise<void>): () => void;
|
|
250
|
+
/**
|
|
251
|
+
* Вычисляемое значение из других полей
|
|
252
|
+
* Автоматически обновляет текущее поле при изменении источников
|
|
253
|
+
*
|
|
254
|
+
* @param sources - Массив ReadonlySignal для отслеживания
|
|
255
|
+
* @param computeFn - Функция вычисления нового значения
|
|
256
|
+
* @returns Функция отписки для cleanup
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* // Автоматический расчет первоначального взноса (20% от стоимости)
|
|
261
|
+
* const dispose = form.initialPayment.computeFrom(
|
|
262
|
+
* [form.propertyValue.value],
|
|
263
|
+
* (propertyValue) => {
|
|
264
|
+
* return propertyValue ? propertyValue * 0.2 : null;
|
|
265
|
+
* }
|
|
266
|
+
* );
|
|
267
|
+
*
|
|
268
|
+
* // Cleanup
|
|
269
|
+
* useEffect(() => dispose, []);
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
computeFrom<TSource extends readonly unknown[]>(sources: ReadonlySignal<TSource[number]>[], computeFn: (...values: TSource) => T): () => void;
|
|
273
|
+
/**
|
|
274
|
+
* Очистить все ресурсы и таймеры
|
|
275
|
+
* Должен вызываться при unmount компонента
|
|
276
|
+
*
|
|
277
|
+
* @remarks
|
|
278
|
+
* Освобождает все ресурсы:
|
|
279
|
+
* - Отписывает все subscriptions через SubscriptionManager
|
|
280
|
+
* - Отменяет pending/running валидации через cancelPendingValidation()
|
|
281
|
+
*
|
|
282
|
+
* Использует try-finally для гарантированного cleanup даже при ошибках.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```typescript
|
|
286
|
+
* useEffect(() => {
|
|
287
|
+
* return () => {
|
|
288
|
+
* field.dispose();
|
|
289
|
+
* };
|
|
290
|
+
* }, []);
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
dispose(): void;
|
|
294
|
+
}
|