Перейти к основному содержимому

FormWizard

Headless многошаговый визард из @reformer/cdk. config — это пара колбэков validateStep / validateAll (не схемы): каждый возвращает boolean | Promise<boolean>. Кнопка «Далее» запускает валидацию текущего шага и блокируется, пока шаг невалиден.

defineSteps — конфиг по селектору шага

Обычный config привязывает правила к шагу по индексу массива: validateStep: (step) => validateModel(model, STEP_SCHEMAS[step - 1]). Индекс хрупкий: добавление или перестановка шага молча рассинхронизируют правила с позицией, а шаг без правил по умолчанию считается «валидным» — тихая дыра.

defineSteps адресует правила по selector шага (тот же id, что у ноды шага в схеме), а не по [step - 1]. Порядок ключей = порядок шагов; шаг без правил объявляется явно через null. На выходе — обычный FormWizardConfig, визард потребляет его как раньше.

import { FormWizard, defineSteps } from '@reformer/cdk/form-wizard';

// step1 / step2 / crossFieldRules — ValidationSchema<Root>
const config = defineSteps<'loan' | 'applicant' | 'confirm', Root>(model, {
steps: {
loan: step1,
applicant: step2,
confirm: null, // без правил — объявлено ЯВНО, а не по умолчанию
},
extras: crossFieldRules, // cross-field/warnings уровня формы — только на submit
});

<FormWizard form={form} config={config}>
{/* …Step / Actions… */}
</FormWizard>;

Результат — { validateStep, validateAll, stepSelectors, createStepController }. validateStep(n) резолвит n → selector → правила внутри (хрупкая индексация инкапсулирована здесь, приложение её больше не пишет); validateAll собирает все непустые шаги плюс extras; stepSelectors — селекторы по порядку (индекс = step - 1). Обе проверки идут с { touch: true } — метятся только провалидированные поля, без сплошного form.markAsTouched(). createStepController относится к живому слою и разобран ниже.

Выигрыш против STEP_SCHEMAS[step - 1]: перестановка шагов остаётся корректной (правила следуют за селектором, а не за индексом); шаг без правил виден в конфиге как null, а не как неявный пробел; вся арифметика смещений живёт в одном месте.

Живая валидация внутри шага — strategy

По умолчанию правила шага проверяются только на «Далее» (validateStep) и на финальном submit (validateAll). defineSteps дополнительно принимает strategy (плюс debounce и liveAfterSubmit) — live-стратегию внутри активного шага: тот же Слой A, что и у createFormValidation/useFormValidation, но привязанный к текущему шагу визарда.

const config = defineSteps<'loan' | 'confirm', Root>(model, {
steps: {
loan: step1,
confirm: null,
},
strategy: 'blur', // подсвечивать ошибки шага по потере фокуса
});

Значения strategy'submit' | 'blur' | 'change' | 'afterFirstSubmit' (та же семантика, что у schema-стратегий Слоя A):

  • submit (default) — прогон только по validateStep/validateAll, живого слоя нет.
  • blur — перепрогон при потере фокуса поля; раскрываются только сблюренные поля (по touched).
  • change — перепрогон на каждый ввод (учитывает debounce); раскрываются только редактированные поля (по dirty).
  • afterFirstSubmit — тихо до первого submit, дальше live по liveAfterSubmit ('change' по умолчанию или 'blur').

defineSteps также возвращает createStepController(step): FormValidationController | null — контроллер live-стратегии для конкретного шага (или null, если у шага нет правил либо strategy не задана). Руками его обычно не дёргают: подписки поднимает хук ниже.

useWizardStepValidation — армит стратегию под текущий шаг

useWizardStepValidation(config) вызывается в теле компонента шага. Он берёт currentStep из FormWizardContext, поднимает контроллер live-стратегии под этот шаг и снимает подписки при смене шага или размонтировании — так живой слой всегда следует за активным шагом.

import { useWizardStepValidation } from '@reformer/cdk/form-wizard';

function LoanStep() {
useWizardStepValidation(config); // config из defineSteps(..., { strategy: 'blur' })
return <>{/* поля шага */}</>;
}

Хук — no-op, если strategy не задана, равна 'submit', или у текущего шага нет правил (объявлен null). Поэтому вызывать его безопасно в любом шаге.

Живой слой поверх, а не вместо

Per-step gate («Далее» → validateStep) и submit (validateAll) не меняютсяstrategy добавляет живой слой подсветки поверх них. Кнопка «Далее» по-прежнему блокируется через validateStep, а финальный прогон идёт через validateAll с { touch: true }.