Skip to main content

FormNavigation

Headless compound component для multi-step form wizard.

Базовое использование

import { FormNavigation } from '@reformer/ui/form-navigation';

const config = {
stepValidations: {
1: step1Schema,
2: step2Schema,
},
fullValidation: fullFormSchema,
};

<FormNavigation form={form} config={config}>
<FormNavigation.Step component={Step1Form} control={form} />
<FormNavigation.Step component={Step2Form} control={form} />

<FormNavigation.Actions onSubmit={handleSubmit}>
<FormNavigation.Prev>Назад</FormNavigation.Prev>
<FormNavigation.Next>Далее</FormNavigation.Next>
<FormNavigation.Submit>Отправить</FormNavigation.Submit>
</FormNavigation.Actions>
</FormNavigation>

Sub-компоненты

КомпонентНазначение
FormNavigationRoot provider
FormNavigation.StepРендерит компонент когда шаг активен
FormNavigation.IndicatorHeadless индикатор шагов (render props)
FormNavigation.ActionsКонтейнер для кнопок навигации (compound или render props)
FormNavigation.PrevКнопка "Назад"
FormNavigation.NextКнопка "Далее"
FormNavigation.SubmitКнопка отправки формы
FormNavigation.ProgressHeadless отображение прогресса (render props)

FormNavigation.Indicator

<FormNavigation.Indicator steps={STEPS}>
{({ steps, goToStep, currentStep }) => (
<nav>
{steps.map((step) => (
<button
key={step.number}
onClick={() => goToStep(step.number)}
disabled={!step.canNavigate}
aria-current={step.isCurrent ? 'step' : undefined}
>
{step.isCompleted ? '✓' : step.number} {step.title}
</button>
))}
</nav>
)}
</FormNavigation.Indicator>

Определение шагов

interface FormNavigationIndicatorStep {
number: number; // Номер шага (1-based)
title: string;
icon?: string;
}

Render Props

interface FormNavigationIndicatorRenderProps {
steps: FormNavigationIndicatorStepWithState[];
goToStep: (step: number) => boolean;
currentStep: number;
totalSteps: number;
completedSteps: number[];
}

interface FormNavigationIndicatorStepWithState {
number: number;
title: string;
icon?: string;
isCurrent: boolean;
isCompleted: boolean;
canNavigate: boolean;
}

FormNavigation.Actions

Поддерживает два режима: Compound Components (рекомендуется) и Render Props (для сложных случаев).

Compound Components (рекомендуется)

<FormNavigation.Actions onSubmit={handleSubmit} className="flex gap-4">
<FormNavigation.Prev>← Назад</FormNavigation.Prev>
<FormNavigation.Next>Далее →</FormNavigation.Next>
<FormNavigation.Submit loadingText="Отправка...">
Отправить
</FormNavigation.Submit>
</FormNavigation.Actions>

Кнопки автоматически становятся disabled когда недоступны:

  • Prev — на первом шаге
  • Next — на последнем шаге
  • Submit — не на последнем шаге

С кастомными кнопками (asChild)

<FormNavigation.Actions onSubmit={handleSubmit} className="flex gap-4">
<FormNavigation.Prev asChild>
<Button variant="ghost">← Назад</Button>
</FormNavigation.Prev>
<FormNavigation.Next asChild>
<Button variant="primary">Далее →</Button>
</FormNavigation.Next>
<FormNavigation.Submit asChild loadingText="Отправка...">
<Button variant="success">Отправить</Button>
</FormNavigation.Submit>
</FormNavigation.Actions>

Props кнопок

interface FormNavigationPrevProps {
children: ReactNode;
asChild?: boolean; // Использовать child как элемент
disabled?: boolean; // Дополнительное disabled (OR с автоматическим)
// + все остальные button props
}

interface FormNavigationNextProps {
children: ReactNode;
asChild?: boolean;
disabled?: boolean;
}

interface FormNavigationSubmitProps {
children: ReactNode;
asChild?: boolean;
disabled?: boolean;
loadingText?: ReactNode; // Контент во время отправки
}

Render Props (для сложных layout)

<FormNavigation.Actions onSubmit={handleSubmit}>
{({ prev, next, submit, isFirstStep, isLastStep, isValidating }) => (
<div>
{!isFirstStep && (
<button onClick={prev.onClick} disabled={prev.disabled}>
Назад
</button>
)}
{!isLastStep ? (
<button onClick={next.onClick} disabled={next.disabled}>
{isValidating ? 'Проверка...' : 'Далее'}
</button>
) : (
<button onClick={submit.onClick} disabled={submit.disabled}>
{submit.isSubmitting ? 'Отправка...' : 'Отправить'}
</button>
)}
</div>
)}
</FormNavigation.Actions>

Render Props типы

interface FormNavigationActionsRenderProps {
prev: { onClick: () => void; disabled: boolean };
next: { onClick: () => void; disabled: boolean };
submit: { onClick: () => void; disabled: boolean; isSubmitting: boolean };
isFirstStep: boolean;
isLastStep: boolean;
isValidating: boolean;
isSubmitting: boolean;
}

FormNavigation.Progress

<FormNavigation.Progress>
{({ current, total, percent }) => (
<div>
Шаг {current} из {total} ({percent}%)
<div style={{ width: `${percent}%` }} />
</div>
)}
</FormNavigation.Progress>

Render Props

interface FormNavigationProgressRenderProps {
current: number;
total: number;
percent: number;
completedCount: number;
isFirstStep: boolean;
isLastStep: boolean;
}

Внешнее управление через Ref

const navRef = useRef<FormNavigationHandle<FormType>>(null);

// Программная навигация
navRef.current?.goToStep(2);
navRef.current?.goToNextStep();
navRef.current?.goToPreviousStep();

// Submit с валидацией
const result = await navRef.current?.submit(async (values) => {
return api.submit(values);
});

<FormNavigation ref={navRef} form={form} config={config}>
...
</FormNavigation>

Конфигурация

interface FormNavigationConfig<T> {
stepValidations: Record<number, ValidationSchemaFn<T>>;
fullValidation: ValidationSchemaFn<T>;
}

Валидация происходит автоматически:

  • При next.onClick: валидируется текущий шаг
  • При submit.onClick: валидируется вся форма

Полный пример

const STEPS = [
{ number: 1, title: 'Основное', icon: '📋' },
{ number: 2, title: 'Контакты', icon: '📞' },
{ number: 3, title: 'Подтверждение', icon: '✅' },
];

function MultiStepForm() {
const navRef = useRef<FormNavigationHandle<MyForm>>(null);
const form = useMemo(() => createForm(), []);

const config = useMemo(() => ({
stepValidations: {
1: basicInfoSchema,
2: contactSchema,
3: confirmationSchema,
},
fullValidation: fullSchema,
}), []);

const handleSubmit = async () => {
const result = await navRef.current?.submit(async (values) => {
return api.createApplication(values);
});
if (result) {
alert('Успешно!');
}
};

return (
<FormNavigation ref={navRef} form={form} config={config}>
{/* Stepper */}
<FormNavigation.Indicator steps={STEPS}>
{({ steps, goToStep }) => (
<div className="flex gap-4">
{steps.map((step) => (
<button
key={step.number}
onClick={() => goToStep(step.number)}
disabled={!step.canNavigate}
className={step.isCurrent ? 'active' : ''}
>
{step.icon} {step.title}
</button>
))}
</div>
)}
</FormNavigation.Indicator>

{/* Steps */}
<FormNavigation.Step component={BasicInfoForm} control={form} />
<FormNavigation.Step component={ContactForm} control={form} />
<FormNavigation.Step component={ConfirmationForm} control={form} />

{/* Navigation (compound components) */}
<FormNavigation.Actions onSubmit={handleSubmit} className="flex justify-between mt-8">
<FormNavigation.Prev asChild>
<Button>← Назад</Button>
</FormNavigation.Prev>
<div className="flex-1" />
<FormNavigation.Next asChild>
<Button>Далее →</Button>
</FormNavigation.Next>
<FormNavigation.Submit asChild loadingText="Отправка...">
<Button>Отправить</Button>
</FormNavigation.Submit>
</FormNavigation.Actions>

{/* Progress */}
<FormNavigation.Progress>
{({ current, total, percent }) => (
<div className="text-center mt-4 text-gray-600">
Шаг {current} из {total}{percent}% завершено
</div>
)}
</FormNavigation.Progress>
</FormNavigation>
);
}