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

FormWizard

Headless многошаговый визард из @reformer/cdk. Шаги и их правила визард не хранит: это поток шагов сборки — createForm({ flow: { steps: [{ id, rules }] } }). Визард рисует поток (текущий шаг, пройденные, доступность) и зовёт его переходы. Кнопка «Далее» — это flow.next(): поток проверяет правила текущего шага и переходит дальше, только если замечаний нет; при замечаниях визард помечает поля тронутыми, и ошибки шага становятся видны.

Поток шагов в сборке​

Правила шага привязаны к его идентификатору, а не к позиции: перестановка шагов не рассинхронизирует правила, а шаг без правил виден в конфиге явно ({ id: 'confirm' }). Если визард рисует рендерер по схеме, id шага — это selector узла шага.

import { createForm } from '@reformer/core';
import { useFormBundle } from '@reformer/core/react';
import { FormWizard } from '@reformer/cdk/form-wizard';

// loanRules / contactsRules / crossStepRules — ValidationDefinition<CreditForm> (defineValidation)
function CreditWizard() {
const bundle = useFormBundle(() =>
createForm<CreditForm>({
model: createCreditModel(),
validation: { extras: crossStepRules }, // правила вне шагов — только при отправке
flow: {
steps: [
{ id: 'loan', rules: loanRules },
{ id: 'contacts', rules: contactsRules },
{ id: 'confirm' }, // шаг без правил
],
},
})
);

return (
<FormWizard form={bundle.form} flow={bundle.flow}>
<FormWizard.Step component={LoanStep} control={bundle.form} />
<FormWizard.Step component={ContactsStep} control={bundle.form} />
<FormWizard.Step component={ConfirmStep} control={bundle.form} />

<FormWizard.Actions onSubmit={() => void bundle.submission.submit(sendApplication)}>
<FormWizard.Prev>Назад</FormWizard.Prev>
<FormWizard.Next>Далее</FormWizard.Next>
<FormWizard.Submit loadingText="Отправка…">Отправить</FormWizard.Submit>
</FormWizard.Actions>
</FormWizard>
);
}

Полный набор правил сборка собирает сама: правила всех шагов плюс extras. Его проверяет bundle.validate() и отправка bundle.submission.submit(handler); один шаг — bundle.validateStep('loan') (неизвестный шаг — исключение). Правила — стабильные ссылки уровня модуля (defineValidation).

Порядок FormWizard.Step совпадает с порядком шагов потока. Шаги считаются и в обёртках (<div> вокруг шагов), render-props-функции визард не трогает.

Откуда визард берёт поток​

  1. Проп flow — обычно bundle.flow.
  2. Сборка в контексте FormBundleContext (@reformer/core/react) — её ставит FormRenderer, а в разметке руками можно поставить самому: <FormBundleContext.Provider value={bundle}>. Тогда form и flow пропами не нужны.
  3. Ни того, ни другого — позиционный поток без правил по числу FormWizard.Step: переходы работают, но шаги не проверяются.

Отправка​

Отправка формы — отдельный объект сборки bundle.submission. bundle.submission.submit(handler) — один прогон всех правил, при успехе — обработчик со значениями модели; сигнал bundle.submission.submitting — идёт ли обработчик. Результат — SubmitResult:

const outcome = await bundle.submission.submit(sendApplication);
// { status: 'submitted', result } — обработчик отработал
// { status: 'invalid' | 'error' | 'cancelled', validation } — проверка не пропустила
// { status: 'failed', error } — обработчик бросил исключение

Как до неё доходит кнопка визарда:

  • сборка в контексте (рендерер или FormBundleContext.Provider) — FormWizard.Submit и кнопка submit из render-props зовут onSubmit через bundle.submission: только после успешной проверки всей формы, а при отказе (invalid, error) вся форма помечается тронутой;
  • без контекста (поток пропом flow) — onSubmit зовётся по нажатию, проверку делает приложение: onSubmit={() => void bundle.submission.submit(sendApplication)}, как в примере выше.

Из любого потомка визарда отправку можно вызвать и вручную — submit из useFormWizard() ведёт себя так же и возвращает SubmitResult.

Управление снаружи визарда​

Ref визарда не нужен: поток — обычный объект сборки, им управляют напрямую. Переходы возвращают FlowTransition — { status: 'moved', from, to } либо { status: 'stayed', step, reason }, где reason — 'invalid' | 'error' | 'cancelled' | 'edge' | 'locked'. cancelled у next() — проверка шага устарела: её сменила следующая, поток перевели либо модель изменилась, пока она шла (поле правили во время async-правила) — шаг пройденным не считается, переход повторяют.

import type { FormBundle } from '@reformer/core';
import { useFormFlow } from '@reformer/core/react';

function WizardToolbar({ bundle }: { bundle: FormBundle<CreditForm> }) {
const flow = bundle.flow!;
const { current, index, isLast, validating } = useFormFlow(flow); // перерисовка при смене шага

return (
<div>
<span>
Шаг {index + 1} из {flow.steps.length}: {current}
</span>
<button onClick={() => flow.goTo('loan')}>К условиям кредита</button>
<button disabled={isLast || validating} onClick={() => void flow.next()}>
Далее
</button>
</div>
);
}
Член потокаНазначение
stepsидентификаторы шагов по порядку
current / indexсигналы текущего шага и его номера с нуля
completedсигнал пройденных шагов (ReadonlySet<string>)
isFirst / isLastсигналы крайних шагов
validatingидёт ли проверка шага в next()
next()проверить текущий шаг и перейти дальше — Promise<FlowTransition>
previous()шаг назад; пройденными остаются только шаги до него
goTo(id)перейти на шаг; на непройденный — stayed с reason: 'locked', неизвестный — исключение
canGoTo(id)доступен ли шаг: первый — всегда, остальные — после прохождения предыдущего
reset()вернуться на первый шаг и забыть пройденные

В поведении формы поток приходит в область: defineFormBehavior(({ model, flow }) => …) — например, вернуть пользователя на шаг при смене условия:

import { defineFormBehavior, onChange } from '@reformer/core/behaviors';

export const creditBehavior = defineFormBehavior<CreditForm>(({ model, flow }) => {
onChange(model.$.loanType, () => {
flow?.goTo('loan');
});
});

Состояние внутри визарда — useFormWizard​

Потомки визарда читают контекст хуком useFormWizard(): form, flow, currentStep (с единицы), totalSteps, completedSteps (номера с единицы), isFirstStep, isLastStep, isValidating, isSubmitting, переходы goToNextStep(), goToPreviousStep(), goToStep(n) и submit(handler). goToStep(n) принимает номер шага с единицы и возвращает false, если шаг ещё недоступен.

Колбэк onStepChange(step) визарда получает номер нового шага с единицы; scrollToTop (по умолчанию true) прокручивает страницу к началу при смене шага.

Живая проверка​

По умолчанию правила шага проверяются только на «Далее», а вся форма — при отправке. Живую проверку включает стратегия сборки — validation.strategy:

createForm<CreditForm>({
model: createCreditModel(),
validation: { extras: crossStepRules, strategy: 'blur' },
flow: { steps: [{ id: 'loan', rules: loanRules }, { id: 'confirm' }] },
});

Значения — 'submit' (по умолчанию, живой проверки нет), 'blur', 'change' (с debounce) и 'afterFirstSubmit' (тихо до первой попытки отправки, затем по liveAfterSubmit; «Далее», bundle.validate() и bundle.validateStep() живую фазу не включают). Стратегию армирует useFormBundle. Ошибки видны только у полей, которые трогали или меняли, поэтому поля следующих шагов не подсвечиваются раньше времени. Переход «Далее» и отправка от стратегии не зависят.

Прежний контракт

config={…} с validateStep / validateAll, defineSteps, useWizardStepValidation, FormWizardConfig, FormWizardHandle и ref визарда удалены. Шаги и правила — createForm({ flow }), отправка — bundle.submission.submit(handler), управление снаружи — bundle.flow, живая проверка — validation.strategy сборки.