@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.
@@ -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, ValidationError, FormFields } from '../core/types';
3
+ import type { FormValue, FormFields } from '../core/types';
4
+ import type { FieldControlState, ArrayControlState } from './types';
4
5
  /**
5
- * @internal
6
- */
7
- interface FieldControlState<T> {
8
- value: T;
9
- pending: boolean;
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
- * Хук для работы с FieldNode - возвращает состояние поля с подписками на сигналы
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, useFormControlValue } from './hooks/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';