@letar/forms

Мультистеп формы

Пошаговые мастера с валидацией

Живой пример

Steps

API: Form.Steps

Разбейте длинную форму на управляемые шаги:

<Form schema={Schema} initialValue={data} onSubmit={save}>
  <Form.Steps animated validateOnNext>
    <Form.Steps.Step title="Личные данные">
      <Form.Field.String name="firstName" />
      <Form.Field.String name="lastName" />
      <Form.Field.String name="email" />
    </Form.Steps.Step>

    <Form.Steps.Step title="Адрес">
      <Form.Field.String name="street" />
      <Form.Field.String name="city" />
      <Form.Field.String name="zip" />
    </Form.Steps.Step>

    <Form.Steps.Step title="Настройки">
      <Form.Field.Checkbox name="newsletter" />
      <Form.Field.Select name="language" />
    </Form.Steps.Step>

    <Form.Steps.Navigation />
  </Form.Steps>
</Form>

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

Полный пример из песочницы form-develop-app / form-develop-app-shadcn, прочитан напрямую из исходников на сборке. У shadcn тот же compound-компонент (FormSteps, beta, уровня формы, не Field) с более компактной двухшаговой схемой — механика та же, масштаб меньше.

apps/form-develop-app/src/app/steps-demo/page.tsx
'use client'

import { Box, Card, Container, Heading, Stack, Text, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { z } from 'zod/v4'

// Schema for multi-step form
const RegistrationSchema = z.object({
  // Step 1: Personal info
  firstName: z.string().min(2, 'Minimum 2 characters'),
  lastName: z.string().min(2, 'Minimum 2 characters'),
  birthDate: z.string().optional(),

  // Step 2: Contact info
  email: z.string().email('Invalid email'),
  phone: z.string().optional(),

  // Step 3: Account settings
  username: z.string().min(3, 'Minimum 3 characters'),
  notifications: z.boolean().default(true),
  theme: z.enum(['light', 'dark', 'system']).default('system'),
})

type RegistrationData = z.infer<typeof RegistrationSchema>

const initialValues: RegistrationData = {
  firstName: '',
  lastName: '',
  birthDate: '',
  email: '',
  phone: '',
  username: '',
  notifications: true,
  theme: 'system',
}

export default function StepsDemoPage() {
  const handleSubmit = (data: RegistrationData) => {
    // eslint-disable-next-line no-console -- демо-приложение
    console.log('Form submitted:', data)
    alert('Registration complete!\n\n' + JSON.stringify(data, null, 2))
  }

  return (
    <Container maxW="container.md" py={8}>
      <VStack gap={8} align="stretch">
        <Heading size="xl">Form.Steps Demo</Heading>
        <Text color="fg.muted">Multi-step form with validation on each step</Text>

        <Card.Root>
          <Card.Body>
            <Form initialValue={initialValues} schema={RegistrationSchema} onSubmit={handleSubmit}>
              <Form.Steps validateOnNext linear colorPalette="brand" animated>
                <Form.Steps.Indicator showDescriptions />

                <Box my={6}>
                  <Form.Steps.Step title="Personal" description="Your details">
                    <Stack gap={4}>
                      <Heading size="md">Personal Information</Heading>
                      <Form.Field.String name="firstName" label="First Name" required />
                      <Form.Field.String name="lastName" label="Last Name" required />
                      <Form.Field.Date name="birthDate" label="Birth Date" />
                    </Stack>
                  </Form.Steps.Step>

                  <Form.Steps.Step title="Contact" description="How to reach you">
                    <Stack gap={4}>
                      <Heading size="md">Contact Information</Heading>
                      <Form.Field.String name="email" label="Email" required />
                      <Form.Field.String name="phone" label="Phone" />
                    </Stack>
                  </Form.Steps.Step>

                  <Form.Steps.Step title="Account" description="Settings">
                    <Stack gap={4}>
                      <Heading size="md">Account Settings</Heading>
                      <Form.Field.String name="username" label="Username" required />
                      <Form.Field.Switch name="notifications" label="Enable notifications" />
                      <Form.Field.RadioGroup
                        name="theme"
                        label="Theme"
                        options={[
                          { value: 'light', label: 'Light' },
                          { value: 'dark', label: 'Dark' },
                          { value: 'system', label: 'System' },
                        ]}
                      />
                    </Stack>
                  </Form.Steps.Step>

                  <Form.Steps.CompletedContent>
                    <Stack gap={4} py={4}>
                      <Heading size="md" color="green.500">
                        All steps complete!
                      </Heading>
                      <Text>Review your information and click Submit to finish registration.</Text>
                    </Stack>
                  </Form.Steps.CompletedContent>
                </Box>

                {/* data-* на кнопках навигации: якоря для подсказок наставника и тестов (v2.14.22+) */}
                <Form.Steps.Navigation
                  prevLabel="Back"
                  nextLabel="Continue"
                  submitLabel="Create Account"
                  prevProps={{ 'data-assist-id': 'steps-demo.prev' }}
                  nextProps={{ 'data-assist-id': 'steps-demo.next' }}
                  submitProps={{ 'data-assist-id': 'steps-demo.submit' }}
                />
              </Form.Steps>
            </Form>
          </Card.Body>
        </Card.Root>

        {/* Non-linear example with animation */}
        <Heading size="lg" mt={8}>
          Non-linear Steps (with animation)
        </Heading>
        <Text color="fg.muted">Click on any step to navigate directly. Animation duration: 0.5s</Text>

        <Card.Root>
          <Card.Body>
            <Form
              initialValue={{ step1Field: '', step2Field: '', step3Field: '' }}
              // eslint-disable-next-line no-console -- демо-приложение
              onSubmit={(data) => console.log('Non-linear form:', data)}
            >
              <Form.Steps colorPalette="teal" animated animationDuration={0.5}>
                <Form.Steps.Indicator />

                <Box my={6}>
                  <Form.Steps.Step title="Step 1">
                    <Form.Field.String name="step1Field" label="Step 1 Field" />
                  </Form.Steps.Step>

                  <Form.Steps.Step title="Step 2">
                    <Form.Field.String name="step2Field" label="Step 2 Field" />
                  </Form.Steps.Step>

                  <Form.Steps.Step title="Step 3">
                    <Form.Field.String name="step3Field" label="Step 3 Field" />
                  </Form.Steps.Step>
                </Box>

                <Form.Steps.Navigation />
              </Form.Steps>
            </Form>
          </Card.Body>
        </Card.Root>
      </VStack>
    </Container>
  )
}

Возможности

  • animated — плавные переходы между шагами
  • validateOnNext — валидация текущего шага перед переходом
  • Form.Steps.Navigation — кнопки Назад / Далее / Отправить
  • Индикатор — отображение прогресса по шагам
  • Навигация клавиатурой — поддержка стрелок

Пропсы кнопок навигации

prevProps, nextProps, submitProps и skipProps передают дополнительные атрибуты каждой кнопке — data-* (якоря подсказок наставника, data-testid, аналитика), aria-* и другие атрибуты нативной кнопки. Литералы data-* пишутся без as:

<Form.Steps.Navigation
  prevProps={{ 'data-assist-id': 'wizard.prev' }}
  nextProps={{ 'data-assist-id': 'wizard.next' }}
  submitProps={{ 'data-assist-id': 'wizard.submit' }}
/>
  • «Далее» и «Отправить» — две разные кнопки. На последнем шаге вместо nextProps действует submitProps; атрибуты с одной кнопки на другую не переезжают.
  • onClick, disabled, loading и type задаёт сам компонент: в тип пропсов они не входят, а в рантайме перекрываются, поэтому навигацию по шагам через эти пропсы не сломать.
  • Доступно с @letar/forms 2.14.22; те же пропсы есть в @letar/forms-shadcn, @letar/forms-vue и @letar/forms-vue-shadcn.

Экран после завершения

Form.Steps.CompletedContent рендерит экран, показываемый после подтверждения последнего шага — сводку или панель «проверьте перед отправкой»:

<Form.Steps validateOnNext>
  <Form.Steps.Step title="Личные данные">...</Form.Steps.Step>
  <Form.Steps.Step title="Адрес">...</Form.Steps.Step>

  <Form.Steps.CompletedContent>
    <p>Всё готово! Проверьте данные и нажмите «Отправить».</p>
  </Form.Steps.CompletedContent>

  <Form.Steps.Navigation />
</Form.Steps>

Его присутствие в дереве меняет поведение кнопки на последнем шаге (@letar/forms 2.16.0+): Continue сначала переводит в состояние «завершено» (там рендерится CompletedContent), и только оттуда Form.Steps.Navigation переключается на Submit. Без CompletedContent последний шаг по-прежнему отправляет форму сразу — экран завершения подключается по желанию, существующие формы не затрагиваются.

Сохранение шагов

Шаги можно сохранять в localStorage для восстановления при перезагрузке страницы:

<Form.Steps animated validateOnNext persistKey="checkout-wizard">
  ...
</Form.Steps>

On this page