FormWizard
Headless многошаговый визард из @reformer/cdk. Шаги и их правила визард не хранит: это
поток шагов сборки — createForm({ flow: { steps: [{ id, rules }] } }). Визард рисует поток
(текущий шаг, пройденные, доступность) и зовёт его переходы. Кнопка «Далее» — это flow.next():
поток проверяет правила текущего шага и переходит дальше, только если замечаний нет; при
замечаниях визард помечает поля тронутыми, и ошибки шага становятся видны.
Variants — готовые конфигурации компонента: один и тот же компонент с разными преднастроенными пропсами. Витрина состояний (напр. обычный Select и Select с расширенной информацией — два варианта).
Examples — приёмы в коде: как решить задачу. Напр. как разные Data Provider’ы работают с компонентом (рецепты использования).
Поток шагов в сборке
Правила шага привязаны к его идентификатору, а не к позиции: перестановка шагов не рассинхронизирует
правила, а шаг без правил виден в конфиге явно ({ 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-функции визард не трогает.
Откуда визард берёт поток
- Проп
flow— обычноbundle.flow. - Сборка в контексте
FormBundleContext(@reformer/core/react) — её ставитFormRenderer, а в разметке руками можно поставить самому:<FormBundleContext.Provider value={bundle}>. Тогдаformиflowпропами не нужны. - Ни того, ни другого — позиционный поток без правил по числу
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 сборки.