← Назад к вопросам

Как реализовать многошаговую форму?

2.2 Middle🔥 202 комментариев
#JavaScript Core

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация многошаговой формы

Многошаговая форма (multi-step form, wizard) - это форма, разделённая на несколько шагов. Пользователь заполняет часть данных на каждом шаге и переходит к следующему. Это улучшает UX и снижает когнитивную нагрузку.

Базовая архитектура

Для реализации нужны:

  1. State - хранить текущий шаг и данные
  2. Валидация - проверка данных перед переходом
  3. Navigation - кнопки Назад/Далее
  4. 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>
  );
}

Ключевые моменты

  1. State сохраняется между шагами - пользователь может вернуться и отредактировать
  2. Валидация на каждом шаге - проверяем перед переходом
  3. Индикатор прогресса - пользователь видит, где он находится
  4. Кнопка Назад - возможность вернуться и исправить
  5. Финальная отправка - все данные отправляются при завершении

Этот подход значительно улучшает UX и повышает completion rate форм.