@reformer/core 9.0.0-beta.1 → 9.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.
@@ -35,27 +35,37 @@ import { FormModel } from '../model/types';
35
35
  export type Validator<TForm, TField> = (value: TField, scope: unknown, root: FormModel<TForm>) => ValidationError | null;
36
36
  /**
37
37
  * Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
38
- * {@link Validator}, но возвращает `Promise`. Регистрируется через
39
- * `validateAsync(path, validator, { debounce })`.
38
+ * {@link Validator}, но возвращает `Promise`.
39
+ *
40
+ * @deprecated Осиротевший остаток удалённого оператора `validateAsync` (Ф7). Рантаймом не
41
+ * потребляется — живой async-путь узла поля использует `AsyncValidatorFn` `(value, { signal })`.
42
+ * Экспортируется только ради обратной совместимости.
40
43
  */
41
44
  export type AsyncValidator<TForm, TField> = (value: TField, scope: unknown, root: FormModel<TForm>) => Promise<ValidationError | null>;
42
45
  /**
43
- * Функция условия для applyWhen.
46
+ * Функция-предикат `(value) => boolean`.
47
+ *
48
+ * @deprecated Осиротевший остаток удалённого оператора `applyWhen` (Ф7). Рантаймом не
49
+ * потребляется; экспортируется только ради обратной совместимости.
44
50
  */
45
51
  export type ConditionFn<T> = (value: T) => boolean;
46
52
  /**
47
- * Опции для функции validate.
53
+ * Опции валидатора-фабрики (`required()`/`pattern()`/…). Передаются вторым (или последним)
54
+ * аргументом в фабрику и попадают в возвращаемую {@link ValidationError}.
48
55
  */
49
56
  export interface ValidateOptions {
50
- /** Сообщение об ошибке */
57
+ /** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
58
+ * резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */
51
59
  message?: string;
52
- /** Параметры ошибки */
60
+ /** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
53
61
  params?: Record<string, FormValue>;
54
62
  }
55
63
  /**
56
- * Опции для функции validateAsync.
64
+ * @deprecated Осиротевший остаток удалённого оператора `validateAsync` (Ф7): опция `debounce`
65
+ * подключалась тем оператором, которого больше нет. Рантаймом не потребляется; экспортируется
66
+ * только ради обратной совместимости.
57
67
  */
58
68
  export interface ValidateAsyncOptions extends ValidateOptions {
59
- /** Задержка перед выполнением валидации (в мс) */
69
+ /** Задержка перед выполнением валидации (в мс). */
60
70
  debounce?: number;
61
71
  }
@@ -6,6 +6,9 @@ import { Signal } from '@preact/signals-core';
6
6
  * bulk-сеттеры (`model.set`/`model.patch`, `patchValue`/`setValue`) пропускают это поле, чтобы
7
7
  * значение из payload не затирало вычисляемое. Прикладной код напрямую обычно не вызывает.
8
8
  *
9
+ * Идемпотентна на уровне флага: повторные вызовы наращивают счётчик ссылок, чтобы каждый оператор,
10
+ * пишущий в этот сигнал, был сбалансирован своим {@link unmarkDerived} при dispose.
11
+ *
9
12
  * @param signal - Сигнал значения из {@link FormModel}, которым владеет compute
10
13
  *
11
14
  * @example
@@ -17,9 +20,35 @@ import { Signal } from '@preact/signals-core';
17
20
  * ```
18
21
  *
19
22
  * @see {@link isDerived} - проверка пометки
23
+ * @see {@link unmarkDerived} - снять пометку при dispose оператора
20
24
  * @group Utilities
21
25
  */
22
26
  export declare function markDerived(signal: Signal<any>): void;
27
+ /**
28
+ * Снять пометку производного сигнала (обратная операция к {@link markDerived}).
29
+ *
30
+ * Вызывается из очистки (`onDispose`) оператора `compute`/`computeFrom`, когда поведение снимается,
31
+ * но модель/сигнал продолжают жить (динамическая перекоммутация: удалить под-схему → сохранить модель
32
+ * → bulk-set). Без этого сигнал остаётся помеченным навсегда, и bulk-сеттеры навсегда пропускают
33
+ * поле, которое больше ничем не вычисляется.
34
+ *
35
+ * Учитывает счётчик ссылок: пометка снимается только когда снят последний владелец. Вызов на
36
+ * непомеченном сигнале — безопасный no-op.
37
+ *
38
+ * @param signal - Сигнал значения из {@link FormModel}, ранее помеченный {@link markDerived}
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import { markDerived, unmarkDerived } from '@reformer/core';
43
+ *
44
+ * markDerived(model.$.total);
45
+ * onDispose(() => unmarkDerived(model.$.total)); // при снятии compute снова разрешаем bulk-set
46
+ * ```
47
+ *
48
+ * @see {@link markDerived} - пометить сигнал производным
49
+ * @group Utilities
50
+ */
51
+ export declare function unmarkDerived(signal: Signal<any>): void;
23
52
  /**
24
53
  * Производный ли сигнал (помечен через {@link markDerived}).
25
54
  *
@@ -70,7 +70,12 @@ export declare class FormSubmitter<T extends object> {
70
70
  *
71
71
  * @param onSubmit - Callback для отправки данных
72
72
  * @param options - Опции submit
73
- * @returns Результат от onSubmit или null если валидация не пройдена
73
+ * @returns Результат от onSubmit или `null` если валидация не пройдена
74
+ *
75
+ * @remarks
76
+ * Возвращаемый `null` неоднозначен: он сигналит и провал валидации, и легитимный `null`,
77
+ * который вернул `onSubmit` (или void-обработчик). Чтобы различать эти случаи, используйте
78
+ * {@link submitWithResult} — он отдаёт явный флаг `success`.
74
79
  *
75
80
  * @example
76
81
  * ```typescript
@@ -9,7 +9,7 @@ export { FormErrorHandler, ErrorStrategy } from './error-handler';
9
9
  export { createForm, createFormFromModel } from './create-form';
10
10
  export type { CreateFormFromModelArgs } from './create-form';
11
11
  export { registerSignalNode, getNodeForSignal } from './signal-node-registry';
12
- export { markDerived, isDerived } from './derived-registry';
12
+ export { markDerived, isDerived, unmarkDerived } from './derived-registry';
13
13
  export { uniqueId } from './unique-id';
14
14
  export { safeCallback, runOutsideEffect, safeDebouncedCallback } from './safe-effect';
15
15
  export { FormSubmitter } from './form-submitter';
@@ -4,14 +4,28 @@
4
4
  * Эти утилиты решают проблему "Cycle detected" в @preact/signals-core,
5
5
  * которая возникает при модификации сигналов внутри effect.
6
6
  *
7
+ * Отложенная запись ({@link runOutsideEffect}) откладывается на микротаск и при этом:
8
+ * - маршрутизирует синхронные throws и async-rejection через {@link FormErrorHandler} вместо того,
9
+ * чтобы они всплывали неперехваченными в точке микротаска / тонули как unhandledRejection;
10
+ * - возвращает отменитель (canceller) для liveness — вызывающий может отменить ещё не выполненную
11
+ * запись при dispose, чтобы она не сработала против снесённого узла/формы.
12
+ *
13
+ * NB: намеренно НЕ оборачиваем записи в `batch()`. Реципрокные guard'ы слоя данных (например
14
+ * `syncFields` в {@link module:core/model/behaviors}) держат флаг «идёт синхронизация» и сбрасывают его
15
+ * в `finally` ПОСЛЕ записи, полагаясь на то, что встречный эффект флашится СИНХРОННО во время записи
16
+ * (вне batch). `batch()` отложил бы встречный флаш за пределы сброса флага и сломал бы эту защиту
17
+ * (сходимость осталась бы за peek-guard'ом, но появились бы лишние круги). Коалесинг записей в один
18
+ * batch — законная оптимизация, но она требует, чтобы эти guard'ы стали batch-aware, поэтому делается
19
+ * не здесь. См. также `aggregateInto`, где коалесинг реализован на уровне самого оператора.
20
+ *
7
21
  * @group Utils
8
22
  * @module utils/safe-effect
9
23
  */
10
24
  /**
11
25
  * Создает callback, который выполняется вне контекста effect
12
26
  *
13
- * Использует queueMicrotask для отложенного выполнения,
14
- * что позволяет модифицировать сигналы без вызова "Cycle detected"
27
+ * Откладывает вызов через {@link runOutsideEffect} — то есть получает маршрутизацию ошибок и
28
+ * обработку async-rejection «бесплатно».
15
29
  *
16
30
  * @param callback - Функция для выполнения
17
31
  * @returns Обёрнутая функция, безопасная для вызова внутри effect
@@ -33,27 +47,33 @@
33
47
  */
34
48
  export declare function safeCallback<TArgs extends unknown[]>(callback: (...args: TArgs) => void | Promise<void>): (...args: TArgs) => void;
35
49
  /**
36
- * Выполняет функцию вне контекста effect
50
+ * Выполняет функцию вне контекста effect (отложенная запись на микротаск).
37
51
  *
38
- * Более простой вариант для одиночного вызова
52
+ * Сбои fn (синхронный throw и async-rejection) маршрутизируются через {@link FormErrorHandler}, а не
53
+ * всплывают неперехваченными. Возвращает отменитель: вызов до срабатывания микротаска отменяет запись
54
+ * (liveness-охрана — не писать против снесённого узла при dispose в том же тике).
39
55
  *
40
56
  * @param fn - Функция для выполнения
57
+ * @returns Отменитель ещё не выполненной записи (no-op, если запись уже выполнена)
41
58
  *
42
59
  * @example
43
60
  * ```typescript
44
61
  * effect(() => {
45
62
  * const value = signal.value;
46
- * runOutsideEffect(() => {
63
+ * const cancel = runOutsideEffect(() => {
47
64
  * otherSignal.value = transform(value);
48
65
  * });
66
+ * onDispose(cancel); // отменить ожидающую запись, если узел снесут в этом же тике
49
67
  * });
50
68
  * ```
51
69
  */
52
- export declare function runOutsideEffect(fn: () => void | Promise<void>): void;
70
+ export declare function runOutsideEffect(fn: () => void | Promise<void>): () => void;
53
71
  /**
54
72
  * Создает версию callback с поддержкой debounce, безопасную для effect
55
73
  *
56
- * Комбинирует debounce логику с выходом из effect контекста
74
+ * `withDebounce` уже откладывает вызов (таймер) — этого достаточно для выхода из effect-контекста,
75
+ * поэтому дополнительного `queueMicrotask` нет (устранён двойной defer). Сбои callback (в т.ч.
76
+ * async-rejection) маршрутизируются через {@link FormErrorHandler}.
57
77
  *
58
78
  * @param callback - Функция для выполнения
59
79
  * @param withDebounce - Функция debounce обёртки из BehaviorRegistry
@@ -4,7 +4,8 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
4
4
  *
5
5
  * Возвращает чистую функцию-валидатор `(value, control, root)`. Передаётся в `validate()`.
6
6
  *
7
- * Пустыми считаются: `null`, `undefined`, `''` (пустая строка).
7
+ * Пустыми считаются: `null`, `undefined`, `''` (пустая строка), `[]` (пустой массив —
8
+ * обязательный multi-select / FormArray без выбранных элементов).
8
9
  * Для boolean полей требуется значение `true`.
9
10
  *
10
11
  * @param options - Опции валидатора ({@link ValidateOptions}): `message`, `params`
@@ -191,6 +191,20 @@ export interface FieldControlState<T> {
191
191
  * ```
192
192
  */
193
193
  componentProps: Record<string, unknown>;
194
+ /**
195
+ * Флаг изменения поля.
196
+ * `true` когда значение поля отличается от начального.
197
+ *
198
+ * Реактивный аналог `control.dirty` — паритет с {@link ArrayControlState.dirty}.
199
+ *
200
+ * @example
201
+ * ```tsx
202
+ * const { dirty } = useFormControl(field);
203
+ *
204
+ * return dirty ? <span className="badge">Изменено</span> : null;
205
+ * ```
206
+ */
207
+ dirty: boolean;
194
208
  }
195
209
  /**
196
210
  * Состояние массива формы, возвращаемое хуком {@link useFormControl} для {@link ArrayNode}.
@@ -325,4 +339,20 @@ export interface ArrayControlState<T> {
325
339
  * ```
326
340
  */
327
341
  dirty: boolean;
342
+ /**
343
+ * Флаг отключения массива.
344
+ * `true` когда массив отключён (`ArrayNode.disable()`), в том числе через
345
+ * распространение disable от родительской группы.
346
+ *
347
+ * Используйте для отключения UI-действий добавления/удаления, когда массив
348
+ * структурно неизменяем.
349
+ *
350
+ * @example
351
+ * ```tsx
352
+ * const { disabled } = useFormControl(itemsArray);
353
+ *
354
+ * return <button disabled={disabled} onClick={() => control.push()}>Add</button>;
355
+ * ```
356
+ */
357
+ disabled: boolean;
328
358
  }
@@ -14,4 +14,11 @@ export interface SignalConfig<K extends string> {
14
14
  * Хук для подписки на набор сигналов с кешированием
15
15
  * @internal
16
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;
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,
18
+ /**
19
+ * Стабильная идентичность подписки (обычно сам узел-контрол). subscribe/getSnapshot
20
+ * пересоздаются ТОЛЬКО при её смене, а не на каждом рендере — как у useFormControlValue,
21
+ * который ключует по `[control]`. Это убирает per-render churn (dispose+recreate preact-effect),
22
+ * сохраняя реактивность при подмене контрола на другой узел.
23
+ */
24
+ subscriptionKey?: unknown): TSnapshot;