Компоненты шагов
Создание отдельных компонентов шагов для многошаговой формы.
Обзор
Каждый компонент шага:
- Получает экземпляр формы через проп
control - Отображает свои поля с помощью
FormField - Может показывать/скрывать поля на основе значений формы
- Использует
useFormControlдля подписки на изменения значений
Структура компонента шага
Все компоненты шагов следуют одному паттерну:
import type { GroupNodeWithControls } from '@reformer/core';
import { FormField } from '@/components/ui/form-field';
import type { CreditApplicationForm } from '../types';
interface StepProps {
control: GroupNodeWithControls<CreditApplicationForm>;
}
export function StepName({ control }: StepProps) {
return (
<div className="space-y-6">
<h2 className="text-xl font-bold">Заголовок шага</h2>
<FormField control={control.fieldName} />
{/* Другие поля... */}
</div>
);
}
Шаг 1: Основная информация о кредите
Первый шаг собирает данные о кредите с условными полями:
reformer-tutorial/src/forms/credit-application/steps/BasicInfoForm.tsx
import type { GroupNodeWithControls } from '@reformer/core';
import { useFormControl } from '@reformer/core';
import type { CreditApplicationForm } from '../types/credit-application.types';
import { FormField } from '@/components/ui/FormField';
interface BasicInfoFormProps {
control: GroupNodeWithControls<CreditApplicationForm>;
}
export function BasicInfoForm({ control }: BasicInfoFormProps) {
// Подписка на изменения loanType
const { value: loanType } = useFormControl<CreditApplicationForm['loanType']>(control.loanType);
return (
<div className="space-y-6">
<h2 className="text-xl font-bold">Основная информация о кредите</h2>
{/* Общие поля */}
<FormField control={control.loanType} />
<FormField control={control.loanAmount} />
<FormField control={control.loanTerm} />
<FormField control={control.loanPurpose} />
{/* Условные поля: Ипотека */}
{loanType === 'mortgage' && (
<>
<h3 className="text-lg font-semibold mt-4">Информация о недвижимости</h3>
<FormField control={control.propertyValue} />
<FormField control={control.initialPayment} />
</>
)}
{/* Условные поля: Автокредит */}
{loanType === 'car' && (
<>
<h3 className="text-lg font-semibold mt-4">Информация об автомобиле</h3>
<FormField control={control.carBrand} />
<FormField control={control.carModel} />
<div className="grid grid-cols-2 gap-4">
<FormField control={control.carYear} />
<FormField control={control.carPrice} />
</div>
</>
)}
</div>
);
}
Ключевые моменты
useFormControl— подписывается на изменения значения поля и вызывает ре-рендер- Условный рендеринг — показ/скрытие полей на основе
loanType - Grid-раскладка — использование CSS grid для полей рядом
Шаг 2: Персональные данные
Этот шаг демонстрирует использование вложенных форм:
reformer-tutorial/src/forms/credit-application/steps/PersonalInfoForm.tsx
import type { GroupNodeWithControls } from '@reformer/core';
import type { CreditApplicationForm } from '../types/credit-application.types';
import { FormField } from '@/components/ui/FormField';
// TODO: Реализуем на следующем этапе документации
import { PersonalDataForm } from '../nested-forms/PersonalDataForm';
import { PassportDataForm } from '../nested-forms/PassportDataForm';
interface PersonalInfoFormProps {
control: GroupNodeWithControls<CreditApplicationForm>;
}
export function PersonalInfoForm({ control }: PersonalInfoFormProps) {
return (
<div className="space-y-6">
<h2 className="text-xl font-bold">Персональные данные</h2>
{/* Вложенная форма: Личные данные */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Личные данные</h3>
<PersonalDataForm control={control.personalData} />
</div>
{/* Вложенная форма: Паспортные данные */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Паспортные данные</h3>
<PassportDataForm control={control.passportData} />
</div>
{/* Дополнительные документы */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Дополнительные документы</h3>
<div className="grid grid-cols-2 gap-4">
<FormField control={control.inn} />
<FormField control={control.snils} />
</div>
</div>
</div>
);
}
Шаг 3: Контактная информация
Демонстрирует переиспользование вложенных форм и операции с группами:
reformer-tutorial/src/forms/credit-application/steps/ContactInfoForm.tsx
import type { GroupNodeWithControls } from '@reformer/core';
import { useFormControl } from '@reformer/core';
import type { CreditApplicationForm } from '../types/credit-application.types';
import { FormField } from '@/components/ui/FormField';
// TODO: Реализуем на следующем этапе документации
import { AddressForm } from '../nested-forms/AddressForm';
interface ContactInfoFormProps {
control: GroupNodeWithControls<CreditApplicationForm>;
}
export function ContactInfoForm({ control }: ContactInfoFormProps) {
const { value: sameAsRegistration } = useFormControl(control.sameAsRegistration);
// Копировать адрес регистрации в адрес проживания
const copyAddress = () => {
const regAddress = control.registrationAddress.getValue();
control.residenceAddress.setValue(regAddress);
};
// Очистить адрес проживания
const clearAddress = () => {
control.residenceAddress.reset();
};
return (
<div className="space-y-6">
<h2 className="text-xl font-bold">Контактная информация</h2>
{/* Телефоны */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Телефоны</h3>
<div className="grid grid-cols-2 gap-4">
<FormField control={control.phoneMain} />
<FormField control={control.phoneAdditional} />
</div>
</div>
{/* Email */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Email</h3>
<div className="grid grid-cols-2 gap-4">
<FormField control={control.email} />
<FormField control={control.emailAdditional} />
</div>
</div>
{/* Адрес регистрации */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Адрес регистрации</h3>
<AddressForm control={control.registrationAddress} />
</div>
{/* Чекбокс "совпадает" */}
<FormField control={control.sameAsRegistration} />
{/* Адрес проживания (условный) */}
{!sameAsRegistration && (
<div className="space-y-4 p-4 bg-gray-50 rounded-lg">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Адрес проживания</h3>
<button
type="button"
onClick={copyAddress}
className="text-sm text-blue-600 hover:underline"
>
Скопировать из регистрации
</button>
</div>
<AddressForm control={control.residenceAddress} />
<button
type="button"
onClick={clearAddress}
className="text-sm text-gray-600 hover:underline"
>
Очистить
</button>
</div>
)}
</div>
);
}
Операции с группами
getValue()— получить все значения из вложенной группыsetValue()— установить все значения во вложенной группеreset()— сбросить группу к начальным значениям
Шаг 4: Информация о занятости
Показывает условные секции на основе статуса занятости:
reformer-tutorial/src/forms/credit-application/steps/EmploymentForm.tsx
import type { GroupNodeWithControls } from '@reformer/core';
import { useFormControl } from '@reformer/core';
import type { CreditApplicationForm } from '../types/credit-application.types';
import { FormField } from '@/components/ui/FormField';
interface EmploymentFormProps {
control: GroupNodeWithControls<CreditApplicationForm>;
}
export function EmploymentForm({ control }: EmploymentFormProps) {
const { value: employmentStatus } = useFormControl<CreditApplicationForm['employmentStatus']>(
control.employmentStatus
);
const isEmployed = employmentStatus === 'employed';
const isSelfEmployed = employmentStatus === 'selfEmployed';
return (
<div className="space-y-6">
<h2 className="text-xl font-bold">Информация о занятости</h2>
<FormField control={control.employmentStatus} />
{/* Секция для работающих */}
{isEmployed && (
<div className="space-y-4 p-4 bg-blue-50 rounded-lg">
<h3 className="text-lg font-semibold">Информация о компании</h3>
<FormField control={control.companyName} />
<div className="grid grid-cols-2 gap-4">
<FormField control={control.companyInn} />
<FormField control={control.companyPhone} />
</div>
<FormField control={control.companyAddress} />
<FormField control={control.position} />
<div className="grid grid-cols-2 gap-4">
<FormField control={control.workExperienceTotal} />
<FormField control={control.workExperienceCurrent} />
</div>
</div>
)}
{/* Секция для самозанятых */}
{isSelfEmployed && (
<div className="space-y-4 p-4 bg-green-50 rounded-lg">
<h3 className="text-lg font-semibold">Информация о бизнесе</h3>
<FormField control={control.businessType} />
<FormField control={control.businessInn} />
<FormField control={control.businessActivity} />
</div>
)}
{/* Секция дохода (для работающих и самозанятых) */}
{(isEmployed || isSelfEmployed) && (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Доход</h3>
<div className="grid grid-cols-2 gap-4">
<FormField control={control.monthlyIncome} />
<FormField control={control.additionalIncome} />
</div>
<FormField control={control.additionalIncomeSource} />
</div>
)}
</div>
);
}
Шаг 5: Дополнительная информация
Демонстрирует работу с массивами (рассматривается в следующем разделе):
import type { GroupNodeWithControls } from '@reformer/core';
import { useFormControl } from '@reformer/core';
import type { CreditApplicationForm } from '../types/credit-application.types';
import { FormField } from '@/components/ui/FormField';
// TODO: Реализуем на следующем этапе документации
import { FormArraySection } from '../components/FormArraySection';
import { PropertyForm } from '../nested-forms/PropertyForm';
import { ExistingLoanForm } from '../nested-forms/ExistingLoanForm';
import { CoBorrowerForm } from '../nested-forms/CoBorrowerForm';
interface AdditionalInfoFormProps {
control: GroupNodeWithControls<CreditApplicationForm>;
}
export function AdditionalInfoForm({ control }: AdditionalInfoFormProps) {
const { value: hasProperty } = useFormControl(control.hasProperty);
const { value: hasExistingLoans } = useFormControl(control.hasExistingLoans);
const { value: hasCoBorrower } = useFormControl(control.hasCoBorrower);
return (
<div className="space-y-6">
<h2 className="text-xl font-bold">Дополнительная информация</h2>
{/* Общая информация */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Общие сведения</h3>
<FormField control={control.maritalStatus} />
<div className="grid grid-cols-2 gap-4">
<FormField control={control.dependents} />
<FormField control={control.education} />
</div>
</div>
{/* Массив имущества */}
<div className="space-y-4">
<FormField control={control.hasProperty} />
{hasProperty && (
<FormArraySection
title="Имущество"
control={control.properties}
itemComponent={PropertyForm}
addButtonLabel="+ Добавить имущество"
/>
)}
</div>
{/* Массив существующих кредитов */}
<div className="space-y-4">
<FormField control={control.hasExistingLoans} />
{hasExistingLoans && (
<FormArraySection
title="Существующие кредиты"
control={control.existingLoans}
itemComponent={ExistingLoanForm}
addButtonLabel="+ Добавить кредит"
/>
)}
</div>
{/* Массив созаёмщиков */}
<div className="space-y-4">
<FormField control={control.hasCoBorrower} />
{hasCoBorrower && (
<FormArraySection
title="Созаёмщики"
control={control.coBorrowers}
itemComponent={CoBorrowerForm}
addButtonLabel="+ Добавить созаёмщика"
/>
)}
</div>
</div>
);
}
Шаг 6: Подтверждение
Финальный шаг со всеми согласиями:
reformer-tutorial/src/forms/credit-application/steps/ConfirmationForm.tsx
import type { GroupNodeWithControls } from '@reformer/core';
import type { CreditApplicationForm } from '../types/credit-application.types';
import { FormField } from '@/components/ui/FormField';
interface ConfirmationFormProps {
control: GroupNodeWithControls<CreditApplicationForm>;
}
export function ConfirmationForm({ control }: ConfirmationFormProps) {
return (
<div className="space-y-6">
<h2 className="text-xl font-bold">Подтверждение</h2>
<div className="space-y-4">
<FormField control={control.agreePersonalData} />
<FormField control={control.agreeCreditHistory} />
<FormField control={control.agreeMarketing} />
<FormField control={control.agreeTerms} />
<FormField control={control.confirmAccuracy} />
</div>
<div className="mt-6">
<FormField control={control.electronicSignature} />
</div>
</div>
);
}
Лучшие практики
1. Используйте семантические секции
Группируйте связанные поля с заголовками:
<div className="space-y-4">
<h3 className="text-lg font-semibold">Заголовок секции</h3>
<FormField control={control.field1} />
<FormField control={control.field2} />
</div>
2. Используйте Grid для раскладки
Используйте CSS grid для полей рядом:
<div className="grid grid-cols-2 gap-4">
<FormField control={control.firstName} />
<FormField control={control.lastName} />
</div>
3. Условный рендеринг с useFormControl
Подписывайтесь только на нужные поля:
const { value: status } = useFormControl(control.status);
// Ре-рендер только при изменении status
{
status === 'active' && <ActiveSection />;
}
4. Выделяйте переиспользуемые паттерны
Если используете один и тот же layout несколько раз, выделите его:
function TwoColumnFields({ left, right }) {
return (
<div className="grid grid-cols-2 gap-4">
<FormField control={left} />
<FormField control={right} />
</div>
);
}
Следующий шаг
Теперь давайте узнаем, как создавать переиспользуемые вложенные компоненты форм и работать с массивами.