@reformer/core 1.1.0-beta.8 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/behaviors-DzYL8kY_.js +499 -0
- package/dist/behaviors.js +14 -14
- package/dist/core/behavior/create-field-path.d.ts +3 -16
- package/dist/core/nodes/group-node.d.ts +14 -193
- package/dist/core/utils/field-path.d.ts +48 -0
- package/dist/core/utils/index.d.ts +1 -0
- package/dist/core/validation/field-path.d.ts +3 -39
- package/dist/core/validation/validation-context.d.ts +23 -0
- package/dist/hooks/types.d.ts +328 -0
- package/dist/hooks/useFormControl.d.ts +13 -37
- package/dist/hooks/useFormControlValue.d.ts +167 -0
- package/dist/hooks/useSignalSubscription.d.ts +17 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +492 -986
- package/dist/{registry-helpers-BfCZcMkO.js → registry-helpers-BRxAr6nG.js} +136 -72
- package/dist/{validators-DjXtDVoE.js → validators-gXoHPdqM.js} +194 -231
- package/dist/validators.js +25 -25
- package/llms.txt +317 -172
- package/package.json +8 -4
- package/dist/behaviors-BRaiR-UY.js +0 -528
- package/dist/core/behavior/behavior-applicator.d.ts +0 -71
- package/dist/core/nodes/group-node/field-registry.d.ts +0 -191
- package/dist/core/nodes/group-node/index.d.ts +0 -11
- package/dist/core/nodes/group-node/proxy-builder.d.ts +0 -71
- package/dist/core/nodes/group-node/state-manager.d.ts +0 -184
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { ValidationError } from '../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Состояние поля формы, возвращаемое хуком {@link useFormControl} для {@link FieldNode}.
|
|
4
|
+
*
|
|
5
|
+
* Содержит реактивные данные поля: значение, состояние валидации, флаги взаимодействия
|
|
6
|
+
* и пользовательские props для компонентов.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam T - Тип значения поля (string, number, boolean и т.д.)
|
|
9
|
+
*
|
|
10
|
+
* @example Базовое использование
|
|
11
|
+
* ```tsx
|
|
12
|
+
* interface Props {
|
|
13
|
+
* control: FieldNode<string>;
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* function TextField({ control }: Props) {
|
|
17
|
+
* const state = useFormControl(control);
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <div>
|
|
21
|
+
* <input
|
|
22
|
+
* value={state.value}
|
|
23
|
+
* disabled={state.disabled}
|
|
24
|
+
* onChange={e => control.setValue(e.target.value)}
|
|
25
|
+
* />
|
|
26
|
+
* {state.shouldShowError && state.errors[0] && (
|
|
27
|
+
* <span className="error">{state.errors[0].message}</span>
|
|
28
|
+
* )}
|
|
29
|
+
* </div>
|
|
30
|
+
* );
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @see {@link useFormControl} - хук для получения состояния
|
|
35
|
+
* @see {@link ArrayControlState} - состояние для массивов
|
|
36
|
+
*
|
|
37
|
+
* @group Types
|
|
38
|
+
*/
|
|
39
|
+
export interface FieldControlState<T> {
|
|
40
|
+
/**
|
|
41
|
+
* Текущее значение поля.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const { value } = useFormControl(emailField);
|
|
46
|
+
* console.log(value); // "user@example.com"
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
value: T;
|
|
50
|
+
/**
|
|
51
|
+
* Флаг асинхронной валидации или загрузки.
|
|
52
|
+
* `true` когда выполняется асинхронный валидатор.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const { pending } = useFormControl(usernameField);
|
|
57
|
+
*
|
|
58
|
+
* return (
|
|
59
|
+
* <div>
|
|
60
|
+
* <input {...props} />
|
|
61
|
+
* {pending && <Spinner size="small" />}
|
|
62
|
+
* </div>
|
|
63
|
+
* );
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
pending: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Флаг отключения поля.
|
|
69
|
+
* `true` когда поле недоступно для редактирования.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```tsx
|
|
73
|
+
* const { disabled, value } = useFormControl(field);
|
|
74
|
+
*
|
|
75
|
+
* return (
|
|
76
|
+
* <input
|
|
77
|
+
* value={value}
|
|
78
|
+
* disabled={disabled}
|
|
79
|
+
* className={disabled ? 'opacity-50' : ''}
|
|
80
|
+
* />
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
disabled: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Массив ошибок валидации.
|
|
87
|
+
* Пустой массив означает отсутствие ошибок.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* const { errors } = useFormControl(field);
|
|
92
|
+
*
|
|
93
|
+
* return (
|
|
94
|
+
* <ul className="error-list">
|
|
95
|
+
* {errors.map((error, i) => (
|
|
96
|
+
* <li key={i}>{error.message}</li>
|
|
97
|
+
* ))}
|
|
98
|
+
* </ul>
|
|
99
|
+
* );
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
errors: ValidationError[];
|
|
103
|
+
/**
|
|
104
|
+
* Флаг валидности поля.
|
|
105
|
+
* `true` когда поле прошло все валидации (errors.length === 0).
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```tsx
|
|
109
|
+
* const { valid } = useFormControl(field);
|
|
110
|
+
*
|
|
111
|
+
* return (
|
|
112
|
+
* <input className={valid ? 'border-green' : 'border-gray'} />
|
|
113
|
+
* );
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
valid: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Флаг невалидности поля.
|
|
119
|
+
* `true` когда есть ошибки валидации (errors.length > 0).
|
|
120
|
+
* Противоположность {@link valid}.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```tsx
|
|
124
|
+
* const { invalid } = useFormControl(field);
|
|
125
|
+
*
|
|
126
|
+
* return (
|
|
127
|
+
* <input
|
|
128
|
+
* aria-invalid={invalid}
|
|
129
|
+
* className={invalid ? 'border-red' : ''}
|
|
130
|
+
* />
|
|
131
|
+
* );
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
invalid: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Флаг взаимодействия с полем.
|
|
137
|
+
* `true` после того как поле потеряло фокус (blur) хотя бы один раз.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```tsx
|
|
141
|
+
* const { touched, invalid } = useFormControl(field);
|
|
142
|
+
*
|
|
143
|
+
* // Показываем ошибку только после взаимодействия
|
|
144
|
+
* const showError = touched && invalid;
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
touched: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Флаг для отображения ошибки.
|
|
150
|
+
* Комбинация touched && invalid - удобный shortcut для UI.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```tsx
|
|
154
|
+
* const { shouldShowError, errors } = useFormControl(field);
|
|
155
|
+
*
|
|
156
|
+
* return (
|
|
157
|
+
* <div>
|
|
158
|
+
* <input {...props} />
|
|
159
|
+
* {shouldShowError && (
|
|
160
|
+
* <span className="error">{errors[0]?.message}</span>
|
|
161
|
+
* )}
|
|
162
|
+
* </div>
|
|
163
|
+
* );
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
shouldShowError: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Пользовательские props для передачи в UI-компоненты.
|
|
169
|
+
* Устанавливаются через {@link FieldNode.setComponentProps}.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```tsx
|
|
173
|
+
* // Установка props
|
|
174
|
+
* field.setComponentProps({
|
|
175
|
+
* placeholder: 'Enter email...',
|
|
176
|
+
* maxLength: 100,
|
|
177
|
+
* autoComplete: 'email'
|
|
178
|
+
* });
|
|
179
|
+
*
|
|
180
|
+
* // Использование в компоненте
|
|
181
|
+
* const { componentProps, value } = useFormControl(field);
|
|
182
|
+
*
|
|
183
|
+
* return (
|
|
184
|
+
* <input
|
|
185
|
+
* value={value}
|
|
186
|
+
* placeholder={componentProps.placeholder}
|
|
187
|
+
* maxLength={componentProps.maxLength}
|
|
188
|
+
* autoComplete={componentProps.autoComplete}
|
|
189
|
+
* />
|
|
190
|
+
* );
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
componentProps: Record<string, any>;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Состояние массива формы, возвращаемое хуком {@link useFormControl} для {@link ArrayNode}.
|
|
197
|
+
*
|
|
198
|
+
* Содержит реактивные данные массива: значения элементов, длину, состояние валидации
|
|
199
|
+
* и флаги взаимодействия.
|
|
200
|
+
*
|
|
201
|
+
* @typeParam T - Тип элемента массива (обычно объект с полями формы)
|
|
202
|
+
*
|
|
203
|
+
* @example Список с динамическим добавлением
|
|
204
|
+
* ```tsx
|
|
205
|
+
* interface Phone {
|
|
206
|
+
* type: string;
|
|
207
|
+
* number: string;
|
|
208
|
+
* }
|
|
209
|
+
*
|
|
210
|
+
* interface Props {
|
|
211
|
+
* control: ArrayNode<Phone>;
|
|
212
|
+
* }
|
|
213
|
+
*
|
|
214
|
+
* function PhoneList({ control }: Props) {
|
|
215
|
+
* const { length, valid } = useFormControl(control);
|
|
216
|
+
*
|
|
217
|
+
* return (
|
|
218
|
+
* <div>
|
|
219
|
+
* {control.map((item, index) => (
|
|
220
|
+
* <PhoneItem
|
|
221
|
+
* key={item.id}
|
|
222
|
+
* control={item}
|
|
223
|
+
* onRemove={() => control.remove(index)}
|
|
224
|
+
* />
|
|
225
|
+
* ))}
|
|
226
|
+
*
|
|
227
|
+
* {length === 0 && <p>No phones added</p>}
|
|
228
|
+
*
|
|
229
|
+
* <button onClick={() => control.push({ type: 'mobile', number: '' })}>
|
|
230
|
+
* Add Phone
|
|
231
|
+
* </button>
|
|
232
|
+
*
|
|
233
|
+
* {!valid && <p className="error">Please fix phone errors</p>}
|
|
234
|
+
* </div>
|
|
235
|
+
* );
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*
|
|
239
|
+
* @see {@link useFormControl} - хук для получения состояния
|
|
240
|
+
* @see {@link FieldControlState} - состояние для полей
|
|
241
|
+
*
|
|
242
|
+
* @group Types
|
|
243
|
+
*/
|
|
244
|
+
export interface ArrayControlState<T> {
|
|
245
|
+
/**
|
|
246
|
+
* Массив текущих значений всех элементов.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```tsx
|
|
250
|
+
* const { value } = useFormControl(phonesArray);
|
|
251
|
+
* console.log(value);
|
|
252
|
+
* // [{ type: 'mobile', number: '+1234567890' }, { type: 'home', number: '+0987654321' }]
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
value: T[];
|
|
256
|
+
/**
|
|
257
|
+
* Количество элементов в массиве.
|
|
258
|
+
* Эквивалентно value.length, но оптимизировано для реактивности.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```tsx
|
|
262
|
+
* const { length } = useFormControl(itemsArray);
|
|
263
|
+
*
|
|
264
|
+
* return (
|
|
265
|
+
* <div>
|
|
266
|
+
* <span>Items: {length}</span>
|
|
267
|
+
* {length >= 10 && <span>Maximum reached</span>}
|
|
268
|
+
* </div>
|
|
269
|
+
* );
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
length: number;
|
|
273
|
+
/**
|
|
274
|
+
* Флаг асинхронной валидации.
|
|
275
|
+
* `true` когда выполняется асинхронный валидатор массива или любого элемента.
|
|
276
|
+
*/
|
|
277
|
+
pending: boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Массив ошибок валидации уровня массива.
|
|
280
|
+
* Не включает ошибки отдельных элементов.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```tsx
|
|
284
|
+
* // Валидатор массива
|
|
285
|
+
* validators.apply(phonesArray, {
|
|
286
|
+
* validator: (phones) => phones.length >= 1,
|
|
287
|
+
* message: 'At least one phone required'
|
|
288
|
+
* });
|
|
289
|
+
*
|
|
290
|
+
* // В компоненте
|
|
291
|
+
* const { errors } = useFormControl(phonesArray);
|
|
292
|
+
* // errors содержит ошибку "At least one phone required" если массив пуст
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
errors: ValidationError[];
|
|
296
|
+
/**
|
|
297
|
+
* Флаг валидности массива и всех его элементов.
|
|
298
|
+
* `true` только когда массив и все вложенные элементы валидны.
|
|
299
|
+
*/
|
|
300
|
+
valid: boolean;
|
|
301
|
+
/**
|
|
302
|
+
* Флаг невалидности.
|
|
303
|
+
* `true` когда есть ошибки в массиве или любом элементе.
|
|
304
|
+
*/
|
|
305
|
+
invalid: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* Флаг взаимодействия.
|
|
308
|
+
* `true` после взаимодействия с любым элементом массива.
|
|
309
|
+
*/
|
|
310
|
+
touched: boolean;
|
|
311
|
+
/**
|
|
312
|
+
* Флаг изменения.
|
|
313
|
+
* `true` когда значение массива отличается от начального.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```tsx
|
|
317
|
+
* const { dirty } = useFormControl(itemsArray);
|
|
318
|
+
*
|
|
319
|
+
* return (
|
|
320
|
+
* <div>
|
|
321
|
+
* {dirty && <span>* Unsaved changes</span>}
|
|
322
|
+
* <button disabled={!dirty}>Save</button>
|
|
323
|
+
* </div>
|
|
324
|
+
* );
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
dirty: boolean;
|
|
328
|
+
}
|
|
@@ -1,48 +1,24 @@
|
|
|
1
1
|
import type { FieldNode } from '../core/nodes/field-node';
|
|
2
2
|
import type { ArrayNode } from '../core/nodes/array-node';
|
|
3
|
-
import type { FormValue,
|
|
3
|
+
import type { FormValue, FormFields } from '../core/types';
|
|
4
|
+
import type { FieldControlState, ArrayControlState } from './types';
|
|
4
5
|
/**
|
|
5
|
-
* @
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
disabled: boolean;
|
|
11
|
-
errors: ValidationError[];
|
|
12
|
-
valid: boolean;
|
|
13
|
-
invalid: boolean;
|
|
14
|
-
touched: boolean;
|
|
15
|
-
shouldShowError: boolean;
|
|
16
|
-
componentProps: Record<string, any>;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* @internal
|
|
20
|
-
*/
|
|
21
|
-
interface ArrayControlState<T> {
|
|
22
|
-
value: T[];
|
|
23
|
-
length: number;
|
|
24
|
-
pending: boolean;
|
|
25
|
-
errors: ValidationError[];
|
|
26
|
-
valid: boolean;
|
|
27
|
-
invalid: boolean;
|
|
28
|
-
touched: boolean;
|
|
29
|
-
dirty: boolean;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Хук для получения только значения поля без подписки на errors, valid и т.д.
|
|
33
|
-
* Используйте когда нужно только значение для условного рендеринга.
|
|
6
|
+
* React-хук для подписки на состояние {@link ArrayNode}.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam T - Тип элемента массива
|
|
9
|
+
* @param control - ArrayNode или undefined
|
|
10
|
+
* @returns Состояние массива {@link ArrayControlState}
|
|
34
11
|
*
|
|
35
|
-
* @group React Hooks
|
|
36
|
-
*/
|
|
37
|
-
export declare function useFormControlValue<T extends FormValue>(control: FieldNode<T>): T;
|
|
38
|
-
/**
|
|
39
|
-
* Хук для работы с ArrayNode - возвращает состояние массива с подписками на сигналы
|
|
40
12
|
* @group React Hooks
|
|
41
13
|
*/
|
|
42
14
|
export declare function useFormControl<T extends FormFields>(control: ArrayNode<T> | undefined): ArrayControlState<T>;
|
|
43
15
|
/**
|
|
44
|
-
*
|
|
16
|
+
* React-хук для подписки на состояние {@link FieldNode}.
|
|
17
|
+
*
|
|
18
|
+
* @typeParam T - Тип значения поля
|
|
19
|
+
* @param control - FieldNode для подписки
|
|
20
|
+
* @returns Состояние поля {@link FieldControlState}
|
|
21
|
+
*
|
|
45
22
|
* @group React Hooks
|
|
46
23
|
*/
|
|
47
24
|
export declare function useFormControl<T extends FormValue>(control: FieldNode<T>): FieldControlState<T>;
|
|
48
|
-
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { FieldNode } from '../core/nodes/field-node';
|
|
2
|
+
import type { FormValue } from '../core/types';
|
|
3
|
+
/**
|
|
4
|
+
* React-хук для подписки только на значение поля.
|
|
5
|
+
*
|
|
6
|
+
* Оптимизированная версия {@link useFormControl}, которая подписывается
|
|
7
|
+
* только на сигнал `value`. Компонент не будет ре-рендериться при изменении
|
|
8
|
+
* `errors`, `touched`, `valid` и других свойств состояния.
|
|
9
|
+
*
|
|
10
|
+
* ## Когда использовать
|
|
11
|
+
*
|
|
12
|
+
* - **Условный рендеринг** на основе значения другого поля
|
|
13
|
+
* - **Вычисляемые значения** зависящие от значения поля
|
|
14
|
+
* - **Read-only отображение** значения без интерактивности
|
|
15
|
+
* - **Оптимизация производительности** когда не нужны другие свойства состояния
|
|
16
|
+
*
|
|
17
|
+
* ## Когда НЕ использовать
|
|
18
|
+
*
|
|
19
|
+
* Если компоненту нужны `errors`, `touched`, `disabled` или другие свойства -
|
|
20
|
+
* используйте {@link useFormControl}. Множественные подписки на один контрол
|
|
21
|
+
* через разные хуки менее эффективны, чем одна подписка через `useFormControl`.
|
|
22
|
+
*
|
|
23
|
+
* @typeParam T - Тип значения поля
|
|
24
|
+
* @param control - FieldNode для подписки на значение
|
|
25
|
+
* @returns Текущее значение поля
|
|
26
|
+
*
|
|
27
|
+
* @example Условный рендеринг секции
|
|
28
|
+
* ```tsx
|
|
29
|
+
* import { useFormControlValue } from '@reformer/core';
|
|
30
|
+
*
|
|
31
|
+
* interface FormFields {
|
|
32
|
+
* hasShipping: FieldNode<boolean>;
|
|
33
|
+
* shippingAddress: GroupNode<AddressFields>;
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* function ShippingSection({ form }: { form: FormFields }) {
|
|
37
|
+
* // Подписка только на значение checkbox
|
|
38
|
+
* const hasShipping = useFormControlValue(form.hasShipping);
|
|
39
|
+
*
|
|
40
|
+
* if (!hasShipping) {
|
|
41
|
+
* return null;
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* return (
|
|
45
|
+
* <div className="shipping-section">
|
|
46
|
+
* <h3>Shipping Address</h3>
|
|
47
|
+
* <AddressForm control={form.shippingAddress} />
|
|
48
|
+
* </div>
|
|
49
|
+
* );
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example Динамические опции на основе другого поля
|
|
54
|
+
* ```tsx
|
|
55
|
+
* interface FormFields {
|
|
56
|
+
* country: FieldNode<string>;
|
|
57
|
+
* city: FieldNode<string>;
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* function CitySelect({ form }: { form: FormFields }) {
|
|
61
|
+
* const country = useFormControlValue(form.country);
|
|
62
|
+
* const { value, disabled } = useFormControl(form.city);
|
|
63
|
+
*
|
|
64
|
+
* // Получаем города для выбранной страны
|
|
65
|
+
* const cities = useMemo(() => getCitiesForCountry(country), [country]);
|
|
66
|
+
*
|
|
67
|
+
* // Сбрасываем город при смене страны
|
|
68
|
+
* useEffect(() => {
|
|
69
|
+
* form.city.setValue('');
|
|
70
|
+
* }, [country, form.city]);
|
|
71
|
+
*
|
|
72
|
+
* return (
|
|
73
|
+
* <select
|
|
74
|
+
* value={value}
|
|
75
|
+
* disabled={disabled || !country}
|
|
76
|
+
* onChange={e => form.city.setValue(e.target.value)}
|
|
77
|
+
* >
|
|
78
|
+
* <option value="">Select city...</option>
|
|
79
|
+
* {cities.map(city => (
|
|
80
|
+
* <option key={city.id} value={city.id}>{city.name}</option>
|
|
81
|
+
* ))}
|
|
82
|
+
* </select>
|
|
83
|
+
* );
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @example Отображение суммы в реальном времени
|
|
88
|
+
* ```tsx
|
|
89
|
+
* interface OrderItem {
|
|
90
|
+
* quantity: FieldNode<number>;
|
|
91
|
+
* price: FieldNode<number>;
|
|
92
|
+
* }
|
|
93
|
+
*
|
|
94
|
+
* function OrderTotal({ items }: { items: ArrayNode<OrderItem> }) {
|
|
95
|
+
* // Для каждого элемента получаем только значения
|
|
96
|
+
* const quantities = items.map(item => useFormControlValue(item.controls.quantity));
|
|
97
|
+
* const prices = items.map(item => useFormControlValue(item.controls.price));
|
|
98
|
+
*
|
|
99
|
+
* const total = quantities.reduce((sum, qty, i) => sum + qty * prices[i], 0);
|
|
100
|
+
*
|
|
101
|
+
* return (
|
|
102
|
+
* <div className="order-total">
|
|
103
|
+
* <strong>Total: ${total.toFixed(2)}</strong>
|
|
104
|
+
* </div>
|
|
105
|
+
* );
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @example Preview значения
|
|
110
|
+
* ```tsx
|
|
111
|
+
* interface MarkdownEditorProps {
|
|
112
|
+
* control: FieldNode<string>;
|
|
113
|
+
* }
|
|
114
|
+
*
|
|
115
|
+
* function MarkdownPreview({ control }: MarkdownEditorProps) {
|
|
116
|
+
* // Подписка только на значение для preview
|
|
117
|
+
* const markdown = useFormControlValue(control);
|
|
118
|
+
*
|
|
119
|
+
* const html = useMemo(() => marked(markdown), [markdown]);
|
|
120
|
+
*
|
|
121
|
+
* return (
|
|
122
|
+
* <div
|
|
123
|
+
* className="markdown-preview"
|
|
124
|
+
* dangerouslySetInnerHTML={{ __html: html }}
|
|
125
|
+
* />
|
|
126
|
+
* );
|
|
127
|
+
* }
|
|
128
|
+
*
|
|
129
|
+
* // Основной редактор использует useFormControl для полного состояния
|
|
130
|
+
* function MarkdownEditor({ control }: MarkdownEditorProps) {
|
|
131
|
+
* const { value, shouldShowError, errors } = useFormControl(control);
|
|
132
|
+
*
|
|
133
|
+
* return (
|
|
134
|
+
* <div className="editor-container">
|
|
135
|
+
* <textarea
|
|
136
|
+
* value={value}
|
|
137
|
+
* onChange={e => control.setValue(e.target.value)}
|
|
138
|
+
* />
|
|
139
|
+
* {shouldShowError && <span className="error">{errors[0]?.message}</span>}
|
|
140
|
+
*
|
|
141
|
+
* {/* Preview обновляется только при изменении value *}
|
|
142
|
+
* <MarkdownPreview control={control} />
|
|
143
|
+
* </div>
|
|
144
|
+
* );
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @example Счётчик символов
|
|
149
|
+
* ```tsx
|
|
150
|
+
* function CharacterCounter({ control, max }: { control: FieldNode<string>; max: number }) {
|
|
151
|
+
* const value = useFormControlValue(control);
|
|
152
|
+
* const remaining = max - value.length;
|
|
153
|
+
*
|
|
154
|
+
* return (
|
|
155
|
+
* <span className={remaining < 20 ? 'warning' : ''}>
|
|
156
|
+
* {remaining} characters remaining
|
|
157
|
+
* </span>
|
|
158
|
+
* );
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* @see {@link useFormControl} - для полного состояния поля
|
|
163
|
+
* @see {@link FieldNode} - тип контрола поля
|
|
164
|
+
*
|
|
165
|
+
* @group React Hooks
|
|
166
|
+
*/
|
|
167
|
+
export declare function useFormControlValue<T extends FormValue>(control: FieldNode<T>): T;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Signal } from '@preact/signals-core';
|
|
2
|
+
/** @internal */
|
|
3
|
+
export type SignalMap = Record<string, Signal<unknown>>;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export type ExtractSignalValues<T extends SignalMap> = {
|
|
6
|
+
[K in keyof T]: T[K] extends Signal<infer V> ? V : never;
|
|
7
|
+
};
|
|
8
|
+
/** @internal */
|
|
9
|
+
export interface SignalConfig<K extends string> {
|
|
10
|
+
key: K;
|
|
11
|
+
useShallowArrayEqual?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Хук для подписки на набор сигналов с кешированием
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function useSignalSubscription<TSignals extends SignalMap, TSnapshot extends ExtractSignalValues<TSignals>>(signals: TSignals, configs: SignalConfig<Extract<keyof TSignals, string>>[], buildSnapshot: (values: ExtractSignalValues<TSignals>) => TSnapshot): TSnapshot;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export { FieldNode } from './core/nodes/field-node';
|
|
|
6
6
|
export { GroupNode } from './core/nodes/group-node';
|
|
7
7
|
export { ArrayNode } from './core/nodes/array-node';
|
|
8
8
|
export type { SetValueOptions } from './core/nodes/form-node';
|
|
9
|
-
export { useFormControl
|
|
9
|
+
export { useFormControl } from './hooks/useFormControl';
|
|
10
|
+
export { useFormControlValue } from './hooks/useFormControlValue';
|
|
11
|
+
export type { FieldControlState, ArrayControlState } from './hooks/types';
|
|
10
12
|
export type { BehaviorSchemaFn } from './core/behavior/types';
|
|
11
13
|
export { validateForm } from './core/validation/validate-form';
|
|
12
14
|
export * as behaviors from './core/behavior';
|