Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализация многошаговой формы
Многошаговая форма (multi-step form, wizard) - это форма, разделённая на несколько шагов. Пользователь заполняет часть данных на каждом шаге и переходит к следующему. Это улучшает UX и снижает когнитивную нагрузку.
Базовая архитектура
Для реализации нужны:
- State - хранить текущий шаг и данные
- Валидация - проверка данных перед переходом
- Navigation - кнопки Назад/Далее
- Progress indicator - визуализация прогресса
Реализация на React + TypeScript
import { useState } from 'react';
interface FormData {
firstName: string;
lastName: string;
email: string;
phone: string;
address: string;
city: string;
country: string;
cardNumber: string;
}
const initialFormData: FormData = {
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
city: '',
country: '',
cardNumber: ''
};
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>(initialFormData);
const [errors, setErrors] = useState<Partial<FormData>>({});
const totalSteps = 3;
// Обновление данных формы
const handleChange = (field: keyof FormData, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}));
// Очищаем ошибку для этого поля
setErrors(prev => ({
...prev,
[field]: ''
}));
};
// Валидация текущего шага
const validateStep = (step: number): boolean => {
const newErrors: Partial<FormData> = {};
switch(step) {
case 1:
if (!formData.firstName) newErrors.firstName = 'Введите имя';
if (!formData.lastName) newErrors.lastName = 'Введите фамилию';
if (!formData.email) newErrors.email = 'Введите email';
else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Некорректный email';
}
break;
case 2:
if (!formData.address) newErrors.address = 'Введите адрес';
if (!formData.city) newErrors.city = 'Введите город';
if (!formData.country) newErrors.country = 'Выберите страну';
break;
case 3:
if (!formData.cardNumber) newErrors.cardNumber = 'Введите номер карты';
else if (!/^\d{16}$/.test(formData.cardNumber.replace(/\s/g, ''))) {
newErrors.cardNumber = 'Номер карты должен содержать 16 цифр';
}
break;
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Переход на следующий шаг
const handleNext = () => {
if (validateStep(currentStep)) {
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
} else {
handleSubmit();
}
}
};
// Переход на предыдущий шаг
const handlePrevious = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
// Отправка формы
const handleSubmit = async () => {
console.log('Отправка данных:', formData);
try {
const response = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
alert('Форма успешно отправлена!');
setFormData(initialFormData);
setCurrentStep(1);
}
} catch (error) {
alert('Ошибка при отправке формы');
}
};
return (
<div className="form-container">
{/* Индикатор прогресса */}
<ProgressIndicator currentStep={currentStep} totalSteps={totalSteps} />
{/* Шаги формы */}
<div className="form-content">
{currentStep === 1 && (
<Step1 formData={formData} errors={errors} handleChange={handleChange} />
)}
{currentStep === 2 && (
<Step2 formData={formData} errors={errors} handleChange={handleChange} />
)}
{currentStep === 3 && (
<Step3 formData={formData} errors={errors} handleChange={handleChange} />
)}
</div>
{/* Навигация */}
<div className="form-navigation">
<button
onClick={handlePrevious}
disabled={currentStep === 1}
className="btn btn-secondary"
>
Назад
</button>
<button
onClick={handleNext}
className="btn btn-primary"
>
{currentStep === totalSteps ? 'Отправить' : 'Далее'}
</button>
</div>
</div>
);
}
// Компонент для индикатора прогресса
function ProgressIndicator({ currentStep, totalSteps }: { currentStep: number; totalSteps: number }) {
const steps = ['Личные данные', 'Адрес', 'Платёж'];
return (
<div className="progress-indicator">
{steps.map((step, index) => (
<div key={index} className="progress-step">
<div className={`step-number ${currentStep > index + 1 ? 'completed' : ''} ${currentStep === index + 1 ? 'active' : ''}`}>
{currentStep > index + 1 ? '✓' : index + 1}
</div>
<div className="step-label">{step}</div>
{index < steps.length - 1 && (
<div className={`step-connector ${currentStep > index + 1 ? 'completed' : ''}`} />
)}
</div>
))}
</div>
);
}
// Шаг 1: Личные данные
function Step1({ formData, errors, handleChange }: any) {
return (
<div className="form-step">
<h2>Личные данные</h2>
<div className="form-group">
<label>Имя</label>
<input
type="text"
value={formData.firstName}
onChange={(e) => handleChange('firstName', e.target.value)}
className={errors.firstName ? 'input-error' : ''}
placeholder="Иван"
/>
{errors.firstName && <span className="error">{errors.firstName}</span>}
</div>
<div className="form-group">
<label>Фамилия</label>
<input
type="text"
value={formData.lastName}
onChange={(e) => handleChange('lastName', e.target.value)}
className={errors.lastName ? 'input-error' : ''}
placeholder="Иванов"
/>
{errors.lastName && <span className="error">{errors.lastName}</span>}
</div>
<div className="form-group">
<label>Email</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
className={errors.email ? 'input-error' : ''}
placeholder="ivan@example.com"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div className="form-group">
<label>Телефон</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
placeholder="+7 (999) 999-99-99"
/>
</div>
</div>
);
}
// Шаг 2: Адрес
function Step2({ formData, errors, handleChange }: any) {
return (
<div className="form-step">
<h2>Адрес доставки</h2>
<div className="form-group">
<label>Адрес</label>
<input
type="text"
value={formData.address}
onChange={(e) => handleChange('address', e.target.value)}
className={errors.address ? 'input-error' : ''}
placeholder="ул. Арбат, д. 10"
/>
{errors.address && <span className="error">{errors.address}</span>}
</div>
<div className="form-group">
<label>Город</label>
<input
type="text"
value={formData.city}
onChange={(e) => handleChange('city', e.target.value)}
className={errors.city ? 'input-error' : ''}
placeholder="Москва"
/>
{errors.city && <span className="error">{errors.city}</span>}
</div>
<div className="form-group">
<label>Страна</label>
<select
value={formData.country}
onChange={(e) => handleChange('country', e.target.value)}
className={errors.country ? 'input-error' : ''}
>
<option value="">Выберите страну</option>
<option value="russia">Россия</option>
<option value="belarus">Беларусь</option>
<option value="ukraine">Украина</option>
</select>
{errors.country && <span className="error">{errors.country}</span>}
</div>
</div>
);
}
// Шаг 3: Платёж
function Step3({ formData, errors, handleChange }: any) {
return (
<div className="form-step">
<h2>Данные платёжной карты</h2>
<div className="form-group">
<label>Номер карты</label>
<input
type="text"
value={formData.cardNumber}
onChange={(e) => handleChange('cardNumber', e.target.value.replace(/\s/g, '').replace(/(\d{4})/g, '$1 ').trim())}
className={errors.cardNumber ? 'input-error' : ''}
placeholder="1234 5678 9012 3456"
maxLength="19"
/>
{errors.cardNumber && <span className="error">{errors.cardNumber}</span>}
</div>
</div>
);
}
export default MultiStepForm;
CSS стили
.form-container {
max-width: 500px;
margin: 40px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
}
.progress-indicator {
display: flex;
align-items: center;
margin-bottom: 40px;
justify-content: space-between;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #ddd;
font-weight: bold;
margin-bottom: 8px;
background: white;
transition: all 0.3s;
}
.step-number.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.step-number.completed {
background: #28a745;
color: white;
border-color: #28a745;
}
.step-connector {
position: absolute;
width: 100%;
height: 2px;
background: #ddd;
top: 20px;
left: 50%;
z-index: -1;
}
.step-connector.completed {
background: #28a745;
}
.form-step {
margin: 20px 0;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group input.input-error,
.form-group select.input-error {
border-color: #dc3545;
}
.error {
color: #dc3545;
font-size: 12px;
margin-top: 5px;
display: block;
}
.form-navigation {
display: flex;
gap: 10px;
margin-top: 30px;
justify-content: space-between;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
Альтернатива: React Hook Form + многошаговая форма
import { useForm } from 'react-hook-form';
function MultiStepFormHookForm() {
const [step, setStep] = useState(1);
const { register, handleSubmit, formState: { errors }, watch } = useForm({
mode: 'onBlur'
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{step === 1 && (
<>
<input {...register('name', { required: 'Имя обязательно' })} />
{errors.name && <span>{errors.name.message}</span>}
</>
)}
{step === 2 && (
<>
<input {...register('email', { required: 'Email обязателен' })} />
{errors.email && <span>{errors.email.message}</span>}
</>
)}
<button onClick={() => setStep(step - 1)}>Назад</button>
<button onClick={() => setStep(step + 1)}>Далее</button>
</form>
);
}
Ключевые моменты
- State сохраняется между шагами - пользователь может вернуться и отредактировать
- Валидация на каждом шаге - проверяем перед переходом
- Индикатор прогресса - пользователь видит, где он находится
- Кнопка Назад - возможность вернуться и исправить
- Финальная отправка - все данные отправляются при завершении
Этот подход значительно улучшает UX и повышает completion rate форм.