@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,340 @@
|
|
|
1
|
+
import { ReadonlySignal, Signal } from '@preact/signals-core';
|
|
2
|
+
import { FieldStatus, ValidationError, ErrorFilterOptions } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Опции для setValue
|
|
5
|
+
* @group Nodes
|
|
6
|
+
*/
|
|
7
|
+
export interface SetValueOptions {
|
|
8
|
+
/** Не вызывать событие изменения (не триггерить валидацию) */
|
|
9
|
+
emitEvent?: boolean;
|
|
10
|
+
/** Обновить только этот узел, не распространять на родителей */
|
|
11
|
+
onlySelf?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Абстрактный базовый класс для всех узлов формы.
|
|
15
|
+
*
|
|
16
|
+
* Все узлы (поля, группы, массивы) наследуют от этого класса и реализуют
|
|
17
|
+
* единый интерфейс для работы с состоянием и валидацией.
|
|
18
|
+
*
|
|
19
|
+
* Template Method паттерн используется для управления состоянием:
|
|
20
|
+
* общие signals (`_touched`, `_dirty`, `_status`) живут в базовом классе,
|
|
21
|
+
* publi-методы (`markAsTouched`, `disable`, …) реализованы здесь, а protected
|
|
22
|
+
* hooks (`onMarkAsTouched`, `onDisable`, …) переопределяются в наследниках.
|
|
23
|
+
*
|
|
24
|
+
* @group Nodes
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* // FormNode не используется напрямую — экземпляры приходят из getReformerForm.
|
|
29
|
+
* import { getReformerForm, FormNode } from '@reformer/core';
|
|
30
|
+
*
|
|
31
|
+
* const form = getReformerForm({ email: '' });
|
|
32
|
+
* form.email instanceof FormNode; // true
|
|
33
|
+
* form.email.markAsTouched();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare abstract class FormNode<T> {
|
|
37
|
+
/**
|
|
38
|
+
* Пользователь взаимодействовал с узлом (touched)
|
|
39
|
+
* Protected: наследники могут читать/изменять через методы
|
|
40
|
+
*/
|
|
41
|
+
protected _touched: Signal<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Значение узла было изменено (dirty)
|
|
44
|
+
* Protected: наследники могут читать/изменять через методы
|
|
45
|
+
*/
|
|
46
|
+
protected _dirty: Signal<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Текущий статус узла
|
|
49
|
+
* Protected: наследники могут читать/изменять через методы
|
|
50
|
+
*/
|
|
51
|
+
protected _status: Signal<FieldStatus>;
|
|
52
|
+
/**
|
|
53
|
+
* Пользователь взаимодействовал с узлом (touched)
|
|
54
|
+
* Computed из _touched для предоставления readonly интерфейса
|
|
55
|
+
*/
|
|
56
|
+
readonly touched: ReadonlySignal<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Пользователь не взаимодействовал с узлом (untouched)
|
|
59
|
+
*/
|
|
60
|
+
readonly untouched: ReadonlySignal<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Значение узла было изменено (dirty)
|
|
63
|
+
* Computed из _dirty для предоставления readonly интерфейса
|
|
64
|
+
*/
|
|
65
|
+
readonly dirty: ReadonlySignal<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Значение узла не было изменено (pristine)
|
|
68
|
+
*/
|
|
69
|
+
readonly pristine: ReadonlySignal<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* Текущий статус узла
|
|
72
|
+
* Computed из _status для предоставления readonly интерфейса
|
|
73
|
+
*/
|
|
74
|
+
readonly status: ReadonlySignal<FieldStatus>;
|
|
75
|
+
/**
|
|
76
|
+
* Узел отключен (disabled)
|
|
77
|
+
*/
|
|
78
|
+
readonly disabled: ReadonlySignal<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Узел включен (enabled)
|
|
81
|
+
*/
|
|
82
|
+
readonly enabled: ReadonlySignal<boolean>;
|
|
83
|
+
/**
|
|
84
|
+
* Текущее значение узла
|
|
85
|
+
* - Для FieldNode: значение поля
|
|
86
|
+
* - Для GroupNode: объект со значениями всех полей
|
|
87
|
+
* - Для ArrayNode: массив значений элементов
|
|
88
|
+
*/
|
|
89
|
+
abstract readonly value: ReadonlySignal<T>;
|
|
90
|
+
/**
|
|
91
|
+
* Узел валиден (все валидаторы прошли успешно)
|
|
92
|
+
*/
|
|
93
|
+
abstract readonly valid: ReadonlySignal<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* Узел невалиден (есть ошибки валидации)
|
|
96
|
+
*/
|
|
97
|
+
abstract readonly invalid: ReadonlySignal<boolean>;
|
|
98
|
+
/**
|
|
99
|
+
* Выполняется асинхронная валидация
|
|
100
|
+
*/
|
|
101
|
+
abstract readonly pending: ReadonlySignal<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* Массив ошибок валидации
|
|
104
|
+
*/
|
|
105
|
+
abstract readonly errors: ReadonlySignal<ValidationError[]>;
|
|
106
|
+
/**
|
|
107
|
+
* Получить значение узла (non-reactive)
|
|
108
|
+
* Использует .peek() для получения значения без создания зависимости
|
|
109
|
+
*/
|
|
110
|
+
abstract getValue(): T;
|
|
111
|
+
/**
|
|
112
|
+
* Установить значение узла
|
|
113
|
+
* @param value - новое значение
|
|
114
|
+
* @param options - опции установки значения
|
|
115
|
+
*/
|
|
116
|
+
abstract setValue(value: T, options?: SetValueOptions): void;
|
|
117
|
+
/**
|
|
118
|
+
* Частично обновить значение узла
|
|
119
|
+
* Для FieldNode: работает как setValue
|
|
120
|
+
* Для GroupNode: обновляет только указанные поля
|
|
121
|
+
* Для ArrayNode: обновляет только указанные элементы
|
|
122
|
+
*
|
|
123
|
+
* @param value - частичное значение для обновления
|
|
124
|
+
*/
|
|
125
|
+
abstract patchValue(value: Partial<T>): void;
|
|
126
|
+
/**
|
|
127
|
+
* Сбросить узел к начальному состоянию
|
|
128
|
+
* @param value - опциональное новое начальное значение
|
|
129
|
+
*/
|
|
130
|
+
abstract reset(value?: T): void;
|
|
131
|
+
/**
|
|
132
|
+
* Запустить валидацию узла
|
|
133
|
+
* @returns `Promise<boolean>` - true если валидация успешна
|
|
134
|
+
*/
|
|
135
|
+
abstract validate(): Promise<boolean>;
|
|
136
|
+
/**
|
|
137
|
+
* Установить ошибки валидации извне
|
|
138
|
+
* @param errors - массив ошибок
|
|
139
|
+
*/
|
|
140
|
+
abstract setErrors(errors: ValidationError[]): void;
|
|
141
|
+
/**
|
|
142
|
+
* Очистить ошибки валидации
|
|
143
|
+
*/
|
|
144
|
+
abstract clearErrors(): void;
|
|
145
|
+
/**
|
|
146
|
+
* Получить ошибки валидации с фильтрацией
|
|
147
|
+
*
|
|
148
|
+
* Позволяет фильтровать ошибки по различным критериям:
|
|
149
|
+
* - По коду ошибки
|
|
150
|
+
* - По сообщению (частичное совпадение)
|
|
151
|
+
* - По параметрам
|
|
152
|
+
* - Через кастомный предикат
|
|
153
|
+
*
|
|
154
|
+
* Без параметров возвращает все ошибки (эквивалент errors.value)
|
|
155
|
+
*
|
|
156
|
+
* @param options - Опции фильтрации ошибок
|
|
157
|
+
* @returns Отфильтрованный массив ошибок валидации
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* // Все ошибки
|
|
162
|
+
* const allErrors = form.getErrors();
|
|
163
|
+
*
|
|
164
|
+
* // Ошибки с конкретным кодом
|
|
165
|
+
* const requiredErrors = form.getErrors({ code: 'required' });
|
|
166
|
+
*
|
|
167
|
+
* // Ошибки с несколькими кодами
|
|
168
|
+
* const errors = form.getErrors({ code: ['required', 'email'] });
|
|
169
|
+
*
|
|
170
|
+
* // Ошибки по сообщению
|
|
171
|
+
* const passwordErrors = form.getErrors({ message: 'Password' });
|
|
172
|
+
*
|
|
173
|
+
* // Ошибки по параметрам
|
|
174
|
+
* const minLengthErrors = form.getErrors({
|
|
175
|
+
* params: { minLength: 8 }
|
|
176
|
+
* });
|
|
177
|
+
*
|
|
178
|
+
* // Кастомная фильтрация
|
|
179
|
+
* const customErrors = form.getErrors({
|
|
180
|
+
* predicate: (err) => err.code.startsWith('custom_')
|
|
181
|
+
* });
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
getErrors(options?: ErrorFilterOptions): ValidationError[];
|
|
185
|
+
/**
|
|
186
|
+
* Отметить узел как touched (пользователь взаимодействовал)
|
|
187
|
+
*
|
|
188
|
+
* Template Method: обновляет signal в базовом классе,
|
|
189
|
+
* вызывает hook для кастомной логики в наследниках
|
|
190
|
+
*/
|
|
191
|
+
markAsTouched(): void;
|
|
192
|
+
/**
|
|
193
|
+
* Отметить узел как untouched
|
|
194
|
+
*
|
|
195
|
+
* Template Method: обновляет signal в базовом классе,
|
|
196
|
+
* вызывает hook для кастомной логики в наследниках
|
|
197
|
+
*/
|
|
198
|
+
markAsUntouched(): void;
|
|
199
|
+
/**
|
|
200
|
+
* Отметить узел как dirty (значение изменено)
|
|
201
|
+
*
|
|
202
|
+
* Template Method: обновляет signal в базовом классе,
|
|
203
|
+
* вызывает hook для кастомной логики в наследниках
|
|
204
|
+
*/
|
|
205
|
+
markAsDirty(): void;
|
|
206
|
+
/**
|
|
207
|
+
* Отметить узел как pristine (значение не изменено)
|
|
208
|
+
*
|
|
209
|
+
* Template Method: обновляет signal в базовом классе,
|
|
210
|
+
* вызывает hook для кастомной логики в наследниках
|
|
211
|
+
*/
|
|
212
|
+
markAsPristine(): void;
|
|
213
|
+
/**
|
|
214
|
+
* Пометить все поля (включая вложенные) как touched
|
|
215
|
+
* Алиас для markAsTouched(), но более явно показывает намерение
|
|
216
|
+
* пометить ВСЕ поля рекурсивно
|
|
217
|
+
*
|
|
218
|
+
* Полезно для:
|
|
219
|
+
* - Показа всех ошибок валидации перед submit
|
|
220
|
+
* - Принудительного отображения ошибок при нажатии "Validate All"
|
|
221
|
+
* - Отображения невалидных полей в wizard/step form
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* // Показать все ошибки перед submit
|
|
226
|
+
* form.touchAll();
|
|
227
|
+
* const isValid = await form.validate();
|
|
228
|
+
* if (!isValid) {
|
|
229
|
+
* // Все ошибки теперь видны пользователю
|
|
230
|
+
* }
|
|
231
|
+
*
|
|
232
|
+
* // Или использовать submit() который уже вызывает touchAll
|
|
233
|
+
* await form.submit(async (values) => {
|
|
234
|
+
* await api.save(values);
|
|
235
|
+
* });
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
touchAll(): void;
|
|
239
|
+
/**
|
|
240
|
+
* Отключить узел
|
|
241
|
+
*
|
|
242
|
+
* Template Method: обновляет статус в базовом классе,
|
|
243
|
+
* вызывает hook для кастомной логики в наследниках
|
|
244
|
+
*
|
|
245
|
+
* Отключенные узлы не проходят валидацию и не включаются в getValue()
|
|
246
|
+
*/
|
|
247
|
+
disable(): void;
|
|
248
|
+
/**
|
|
249
|
+
* Включить узел
|
|
250
|
+
*
|
|
251
|
+
* Template Method: обновляет статус в базовом классе,
|
|
252
|
+
* вызывает hook для кастомной логики в наследниках
|
|
253
|
+
*/
|
|
254
|
+
enable(): void;
|
|
255
|
+
/**
|
|
256
|
+
* Очистить все ресурсы узла
|
|
257
|
+
* Должен вызываться при unmount компонента для предотвращения memory leaks
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* // React component
|
|
262
|
+
* useEffect(() => {
|
|
263
|
+
* return () => {
|
|
264
|
+
* form.dispose(); // Cleanup при unmount
|
|
265
|
+
* };
|
|
266
|
+
* }, []);
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
dispose?(): void;
|
|
270
|
+
/**
|
|
271
|
+
* Hook: вызывается после markAsTouched()
|
|
272
|
+
*
|
|
273
|
+
* Переопределите в наследниках для дополнительной логики:
|
|
274
|
+
* - GroupNode: пометить все дочерние узлы как touched
|
|
275
|
+
* - ArrayNode: пометить все элементы массива как touched
|
|
276
|
+
* - FieldNode: пустая реализация (нет дочерних узлов)
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```typescript
|
|
280
|
+
* // GroupNode
|
|
281
|
+
* protected onMarkAsTouched(): void {
|
|
282
|
+
* this.fields.forEach(field => field.markAsTouched());
|
|
283
|
+
* }
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
protected onMarkAsTouched(): void;
|
|
287
|
+
/**
|
|
288
|
+
* Hook: вызывается после markAsUntouched()
|
|
289
|
+
*
|
|
290
|
+
* Переопределите в наследниках для дополнительной логики:
|
|
291
|
+
* - GroupNode: пометить все дочерние узлы как untouched
|
|
292
|
+
* - ArrayNode: пометить все элементы массива как untouched
|
|
293
|
+
* - FieldNode: пустая реализация (нет дочерних узлов)
|
|
294
|
+
*/
|
|
295
|
+
protected onMarkAsUntouched(): void;
|
|
296
|
+
/**
|
|
297
|
+
* Hook: вызывается после markAsDirty()
|
|
298
|
+
*
|
|
299
|
+
* Переопределите в наследниках для дополнительной логики:
|
|
300
|
+
* - GroupNode: может обновить родительскую форму
|
|
301
|
+
* - ArrayNode: может обновить родительскую форму
|
|
302
|
+
* - FieldNode: пустая реализация
|
|
303
|
+
*/
|
|
304
|
+
protected onMarkAsDirty(): void;
|
|
305
|
+
/**
|
|
306
|
+
* Hook: вызывается после markAsPristine()
|
|
307
|
+
*
|
|
308
|
+
* Переопределите в наследниках для дополнительной логики:
|
|
309
|
+
* - GroupNode: пометить все дочерние узлы как pristine
|
|
310
|
+
* - ArrayNode: пометить все элементы массива как pristine
|
|
311
|
+
* - FieldNode: пустая реализация
|
|
312
|
+
*/
|
|
313
|
+
protected onMarkAsPristine(): void;
|
|
314
|
+
/**
|
|
315
|
+
* Hook: вызывается после disable()
|
|
316
|
+
*
|
|
317
|
+
* Переопределите в наследниках для дополнительной логики:
|
|
318
|
+
* - GroupNode: отключить все дочерние узлы
|
|
319
|
+
* - ArrayNode: отключить все элементы массива
|
|
320
|
+
* - FieldNode: очистить ошибки валидации
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* // GroupNode
|
|
325
|
+
* protected onDisable(): void {
|
|
326
|
+
* this.fields.forEach(field => field.disable());
|
|
327
|
+
* }
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
protected onDisable(): void;
|
|
331
|
+
/**
|
|
332
|
+
* Hook: вызывается после enable()
|
|
333
|
+
*
|
|
334
|
+
* Переопределите в наследниках для дополнительной логики:
|
|
335
|
+
* - GroupNode: включить все дочерние узлы
|
|
336
|
+
* - ArrayNode: включить все элементы массива
|
|
337
|
+
* - FieldNode: пустая реализация
|
|
338
|
+
*/
|
|
339
|
+
protected onEnable(): void;
|
|
340
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { ReadonlySignal } from '@preact/signals-core';
|
|
2
|
+
import { FormNode, SetValueOptions } from './form-node';
|
|
3
|
+
import { ValidationError, FieldStatus, ValidationSchemaFn, ValidatorRegistration, FormSchema, GroupNodeConfig, FormValue } from '../types';
|
|
4
|
+
import { FormProxy } from '../types/form-proxy';
|
|
5
|
+
import { BehaviorSchemaFn } from '../behavior/types';
|
|
6
|
+
import { SubmitOptions, SubmitResult } from '../utils/form-submitter';
|
|
7
|
+
/**
|
|
8
|
+
* GroupNode - узел для группы полей
|
|
9
|
+
*
|
|
10
|
+
* Поддерживает два API:
|
|
11
|
+
* 1. Старый API (только schema) - обратная совместимость
|
|
12
|
+
* 2. Новый API (config с form, behavior, validation) - автоматическое применение схем
|
|
13
|
+
*
|
|
14
|
+
* @group Nodes
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // 1. Старый способ (обратная совместимость)
|
|
19
|
+
* const simpleForm = new GroupNode({
|
|
20
|
+
* email: { value: '', component: Input },
|
|
21
|
+
* password: { value: '', component: Input },
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // 2. Новый способ (с behavior и validation схемами)
|
|
25
|
+
* const fullForm = new GroupNode({
|
|
26
|
+
* form: {
|
|
27
|
+
* email: { value: '', component: Input },
|
|
28
|
+
* password: { value: '', component: Input },
|
|
29
|
+
* },
|
|
30
|
+
* behavior: (path) => {
|
|
31
|
+
* computeFrom(path.email, [path.email], (values) => values[0]?.trim());
|
|
32
|
+
* },
|
|
33
|
+
* validation: (path) => {
|
|
34
|
+
* required(path.email, { message: 'Email обязателен' });
|
|
35
|
+
* email(path.email);
|
|
36
|
+
* required(path.password);
|
|
37
|
+
* minLength(path.password, 8);
|
|
38
|
+
* },
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Прямой доступ к полям через Proxy
|
|
42
|
+
* fullForm.email.setValue('test@mail.com');
|
|
43
|
+
* await fullForm.validate();
|
|
44
|
+
* console.log(fullForm.valid.value); // true
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare class GroupNode<T> extends FormNode<T> {
|
|
48
|
+
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
49
|
+
/**
|
|
50
|
+
* Коллекция полей формы (упрощённый Map вместо FieldRegistry)
|
|
51
|
+
*/
|
|
52
|
+
private readonly _fields;
|
|
53
|
+
/**
|
|
54
|
+
* Менеджер подписок для централизованного cleanup
|
|
55
|
+
*/
|
|
56
|
+
private disposers;
|
|
57
|
+
/**
|
|
58
|
+
* Ссылка на Proxy-инстанс для использования в BehaviorContext
|
|
59
|
+
*/
|
|
60
|
+
private _proxyInstance?;
|
|
61
|
+
/**
|
|
62
|
+
* Навигатор для работы с путями к полям
|
|
63
|
+
*/
|
|
64
|
+
private readonly pathNavigator;
|
|
65
|
+
/**
|
|
66
|
+
* Фабрика для создания узлов формы
|
|
67
|
+
*/
|
|
68
|
+
private readonly nodeFactory;
|
|
69
|
+
/**
|
|
70
|
+
* Реестр валидаторов для этой формы
|
|
71
|
+
* Может быть инжектирован через config._validationRegistry для тестирования
|
|
72
|
+
*/
|
|
73
|
+
private readonly validationRegistry;
|
|
74
|
+
/**
|
|
75
|
+
* Реестр behaviors для этой формы
|
|
76
|
+
* Может быть инжектирован через config._behaviorRegistry для тестирования
|
|
77
|
+
*/
|
|
78
|
+
private readonly behaviorRegistry;
|
|
79
|
+
/**
|
|
80
|
+
* Аппликатор для применения валидаторов к форме
|
|
81
|
+
*/
|
|
82
|
+
private readonly validationApplicator;
|
|
83
|
+
/** Управление отправкой формы */
|
|
84
|
+
private readonly formSubmitter;
|
|
85
|
+
/** Флаг disabled состояния */
|
|
86
|
+
private readonly _disabled;
|
|
87
|
+
/** Form-level validation errors */
|
|
88
|
+
private readonly _formErrors;
|
|
89
|
+
readonly value: ReadonlySignal<T>;
|
|
90
|
+
readonly valid: ReadonlySignal<boolean>;
|
|
91
|
+
readonly invalid: ReadonlySignal<boolean>;
|
|
92
|
+
readonly touched: ReadonlySignal<boolean>;
|
|
93
|
+
readonly dirty: ReadonlySignal<boolean>;
|
|
94
|
+
readonly pending: ReadonlySignal<boolean>;
|
|
95
|
+
readonly errors: ReadonlySignal<ValidationError[]>;
|
|
96
|
+
readonly status: ReadonlySignal<FieldStatus>;
|
|
97
|
+
readonly submitting: ReadonlySignal<boolean>;
|
|
98
|
+
/**
|
|
99
|
+
* Создать GroupNode только со схемой формы (обратная совместимость)
|
|
100
|
+
*/
|
|
101
|
+
constructor(schema: FormSchema<T>);
|
|
102
|
+
/**
|
|
103
|
+
* Создать GroupNode с полной конфигурацией (form, behavior, validation)
|
|
104
|
+
*/
|
|
105
|
+
constructor(config: GroupNodeConfig<T>);
|
|
106
|
+
/**
|
|
107
|
+
* Создать Proxy для типобезопасного доступа к полям
|
|
108
|
+
* @see buildFormProxy
|
|
109
|
+
*/
|
|
110
|
+
private buildProxy;
|
|
111
|
+
getValue(): T;
|
|
112
|
+
setValue(value: T, options?: SetValueOptions): void;
|
|
113
|
+
patchValue(value: Partial<T>): void;
|
|
114
|
+
/**
|
|
115
|
+
* Сбросить форму к указанным значениям (или к initialValues)
|
|
116
|
+
*
|
|
117
|
+
* @param value - опциональный объект со значениями для сброса
|
|
118
|
+
*
|
|
119
|
+
* @remarks
|
|
120
|
+
* Рекурсивно вызывает reset() для всех полей формы
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* // Сброс к initialValues
|
|
125
|
+
* form.reset();
|
|
126
|
+
*
|
|
127
|
+
* // Сброс к новым значениям
|
|
128
|
+
* form.reset({ email: 'new@mail.com', password: '' });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
reset(value?: T): void;
|
|
132
|
+
/**
|
|
133
|
+
* Сбросить форму к исходным значениям (initialValues)
|
|
134
|
+
*/
|
|
135
|
+
resetToInitial(): void;
|
|
136
|
+
validate(): Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* Установить form-level validation errors
|
|
139
|
+
*/
|
|
140
|
+
setErrors(errors: ValidationError[]): void;
|
|
141
|
+
/**
|
|
142
|
+
* Очистить все errors (form-level + field-level)
|
|
143
|
+
*/
|
|
144
|
+
clearErrors(): void;
|
|
145
|
+
/**
|
|
146
|
+
* Получить поле по ключу
|
|
147
|
+
*/
|
|
148
|
+
getField<K extends keyof T>(key: K): FormNode<T[K]> | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* Получить Map всех полей формы (для совместимости)
|
|
151
|
+
*/
|
|
152
|
+
get fields(): Map<keyof T, FormNode<FormValue>>;
|
|
153
|
+
/**
|
|
154
|
+
* Получить Proxy-инстанс для прямого доступа к полям
|
|
155
|
+
*
|
|
156
|
+
* Proxy позволяет обращаться к полям формы напрямую через точечную нотацию:
|
|
157
|
+
* - form.email вместо form.fields.get('email')
|
|
158
|
+
* - form.address.city вместо form.fields.get('address').fields.get('city')
|
|
159
|
+
*
|
|
160
|
+
* Используется в:
|
|
161
|
+
* - BehaviorApplicator для доступа к полям в behavior functions
|
|
162
|
+
* - ValidationApplicator для доступа к форме в tree validators
|
|
163
|
+
*
|
|
164
|
+
* @returns Proxy-инстанс с типобезопасным доступом к полям или сама форма, если proxy не доступен
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* const form = new GroupNode({
|
|
169
|
+
* controls: {
|
|
170
|
+
* email: new FieldNode({ value: '' }),
|
|
171
|
+
* name: new FieldNode({ value: '' })
|
|
172
|
+
* }
|
|
173
|
+
* });
|
|
174
|
+
*
|
|
175
|
+
* const proxy = form.getProxy();
|
|
176
|
+
* console.log(proxy.email.value); // Прямой доступ к полю
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
getProxy(): FormProxy<T>;
|
|
180
|
+
/**
|
|
181
|
+
* Получить все поля формы как итератор
|
|
182
|
+
*/
|
|
183
|
+
getAllFields(): IterableIterator<FormNode<FormValue>>;
|
|
184
|
+
protected onMarkAsTouched(): void;
|
|
185
|
+
protected onMarkAsUntouched(): void;
|
|
186
|
+
protected onMarkAsDirty(): void;
|
|
187
|
+
protected onMarkAsPristine(): void;
|
|
188
|
+
/**
|
|
189
|
+
* Отправить форму
|
|
190
|
+
*
|
|
191
|
+
* @param onSubmit - Callback для отправки данных
|
|
192
|
+
* @param options - Опции submit (skipValidation, skipTouch)
|
|
193
|
+
* @returns Результат от onSubmit или null если валидация не пройдена
|
|
194
|
+
*/
|
|
195
|
+
submit<R>(onSubmit: (values: T) => Promise<R> | R, options?: SubmitOptions): Promise<R | null>;
|
|
196
|
+
/**
|
|
197
|
+
* Отправить форму с расширенным результатом
|
|
198
|
+
*
|
|
199
|
+
* @param onSubmit - Callback для отправки данных
|
|
200
|
+
* @param options - Опции submit
|
|
201
|
+
* @returns Объект SubmitResult с данными, статусом и возможной ошибкой
|
|
202
|
+
*/
|
|
203
|
+
submitWithResult<R>(onSubmit: (values: T) => Promise<R> | R, options?: SubmitOptions): Promise<SubmitResult<R>>;
|
|
204
|
+
/**
|
|
205
|
+
* Применить validation schema к форме
|
|
206
|
+
*
|
|
207
|
+
* Использует локальный реестр валидаторов (this.validationRegistry)
|
|
208
|
+
* вместо глобального Singleton для изоляции форм друг от друга.
|
|
209
|
+
*/
|
|
210
|
+
applyValidationSchema(schemaFn: ValidationSchemaFn<T>): void;
|
|
211
|
+
/**
|
|
212
|
+
* Применить behavior schema к форме
|
|
213
|
+
* @returns Функция cleanup для отписки от всех behaviors
|
|
214
|
+
*/
|
|
215
|
+
applyBehaviorSchema(schemaFn: BehaviorSchemaFn<T>): () => void;
|
|
216
|
+
/**
|
|
217
|
+
* Получить вложенное поле по пути
|
|
218
|
+
*
|
|
219
|
+
* Поддерживаемые форматы путей:
|
|
220
|
+
* - Simple: "email" - получить поле верхнего уровня
|
|
221
|
+
* - Nested: "address.city" - получить вложенное поле
|
|
222
|
+
* - Array index: "items[0]" - получить элемент массива по индексу
|
|
223
|
+
* - Combined: "items[0].name" - получить поле элемента массива
|
|
224
|
+
*
|
|
225
|
+
* @param path - Путь к полю
|
|
226
|
+
* @returns FormNode если найдено, undefined если путь не существует
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* const form = new GroupNode({
|
|
231
|
+
* email: { value: '', component: Input },
|
|
232
|
+
* address: {
|
|
233
|
+
* city: { value: '', component: Input }
|
|
234
|
+
* },
|
|
235
|
+
* items: [{ name: { value: '', component: Input } }]
|
|
236
|
+
* });
|
|
237
|
+
*
|
|
238
|
+
* form.getFieldByPath('email'); // FieldNode
|
|
239
|
+
* form.getFieldByPath('address.city'); // FieldNode
|
|
240
|
+
* form.getFieldByPath('items[0]'); // GroupNode
|
|
241
|
+
* form.getFieldByPath('items[0].name'); // FieldNode
|
|
242
|
+
* form.getFieldByPath('invalid.path'); // undefined
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
getFieldByPath(path: string): FormNode<FormValue> | undefined;
|
|
246
|
+
/**
|
|
247
|
+
* Применить contextual валидаторы к полям
|
|
248
|
+
*
|
|
249
|
+
* ✅ РЕФАКТОРИНГ: Делегирование ValidationApplicator (SRP)
|
|
250
|
+
*
|
|
251
|
+
* Логика применения валидаторов извлечена в ValidationApplicator для:
|
|
252
|
+
* - Соблюдения Single Responsibility Principle
|
|
253
|
+
* - Уменьшения размера GroupNode (~120 строк)
|
|
254
|
+
* - Улучшения тестируемости
|
|
255
|
+
*
|
|
256
|
+
* @param validators Зарегистрированные валидаторы
|
|
257
|
+
*/
|
|
258
|
+
applyContextualValidators(validators: ValidatorRegistration[]): Promise<void>;
|
|
259
|
+
/**
|
|
260
|
+
* Создать узел на основе конфигурации
|
|
261
|
+
*
|
|
262
|
+
* ✅ РЕФАКТОРИНГ: Полное делегирование NodeFactory
|
|
263
|
+
*
|
|
264
|
+
* NodeFactory теперь обрабатывает:
|
|
265
|
+
* - Массивы [schema, ...items]
|
|
266
|
+
* - FieldConfig
|
|
267
|
+
* - GroupConfig
|
|
268
|
+
* - ArrayConfig
|
|
269
|
+
*
|
|
270
|
+
* @param config Конфигурация узла
|
|
271
|
+
* @returns Созданный узел формы
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
private createNode;
|
|
275
|
+
/**
|
|
276
|
+
* Связывает два поля: при изменении source автоматически обновляется target
|
|
277
|
+
*/
|
|
278
|
+
linkFields<K1 extends keyof T, K2 extends keyof T>(sourceKey: K1, targetKey: K2, transform?: (value: T[K1]) => T[K2]): () => void;
|
|
279
|
+
/**
|
|
280
|
+
* Подписка на изменения вложенного поля по строковому пути
|
|
281
|
+
* Поддерживает вложенные пути типа "address.city"
|
|
282
|
+
*
|
|
283
|
+
* @param fieldPath - Строковый путь к полю (например, "address.city")
|
|
284
|
+
* @param callback - Функция, вызываемая при изменении поля
|
|
285
|
+
* @returns Функция отписки для cleanup
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* // Подписка на изменение страны для загрузки городов
|
|
290
|
+
* const dispose = form.watchField(
|
|
291
|
+
* 'registrationAddress.country',
|
|
292
|
+
* async (countryCode) => {
|
|
293
|
+
* if (countryCode) {
|
|
294
|
+
* const cities = await fetchCitiesByCountry(countryCode);
|
|
295
|
+
* form.registrationAddress.city.updateComponentProps({
|
|
296
|
+
* options: cities
|
|
297
|
+
* });
|
|
298
|
+
* }
|
|
299
|
+
* }
|
|
300
|
+
* );
|
|
301
|
+
*
|
|
302
|
+
* // Cleanup
|
|
303
|
+
* useEffect(() => dispose, []);
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
watchField<K extends keyof T>(fieldPath: K extends string ? K : string, callback: (value: T[K]) => void | Promise<void>): () => void;
|
|
307
|
+
/**
|
|
308
|
+
* Hook: вызывается после disable()
|
|
309
|
+
*/
|
|
310
|
+
protected onDisable(): void;
|
|
311
|
+
/**
|
|
312
|
+
* Hook: вызывается после enable()
|
|
313
|
+
*/
|
|
314
|
+
protected onEnable(): void;
|
|
315
|
+
/**
|
|
316
|
+
* Очистить все ресурсы узла
|
|
317
|
+
*/
|
|
318
|
+
dispose(): void;
|
|
319
|
+
}
|