@letar/forms

Автоматическая генерация форм

Генерация форм из Zod-схемы — от нулевого JSX до тонкой настройки

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

Полный sandbox-пример, прочитанный напрямую из исходников form-develop-app / form-develop-app-shadcn на сборке. Chakra-источник использует Form.AutoFields; shadcn-источник использует FieldAuto — другой уровень иерархии контроля ниже, но та же идея автогенерации полей из Zod-схемы. Аналога для Vue/Angular пока нет — эти вкладки показаны как disabled.

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

import { Badge, Box, Code, Heading, HStack, Separator, Text, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { useState } from 'react'
import { z } from 'zod/v4'
import { DemoPageLayout, SubmittedDataPreview } from '../_components'

/**
 * Демонстрация автоматической генерации форм из Zod схемы
 *
 * Три подхода:
 * 1. Form.FromSchema — полностью автоматическая форма
 * 2. Form.AutoFields — генерация полей внутри Form
 * 3. Смешанный подход — AutoFields + ручные поля
 */

// === Схема для Form.FromSchema ===
const UserSchema = z.object({
  firstName: z
    .string()
    .min(2)
    .meta({ ui: { title: 'Имя', placeholder: 'Введите имя' } }),
  lastName: z
    .string()
    .min(2)
    .meta({ ui: { title: 'Фамилия', placeholder: 'Введите фамилию' } }),
  email: z
    .string()
    .email()
    .meta({ ui: { title: 'Email', placeholder: 'user@example.com' } }),
  age: z
    .number()
    .min(18)
    .max(120)
    .meta({ ui: { title: 'Возраст' } }),
  bio: z
    .string()
    .max(500)
    .meta({ ui: { title: 'О себе', fieldType: 'textarea', placeholder: 'Расскажите о себе...' } }),
  role: z.enum(['admin', 'user', 'guest']).meta({ ui: { title: 'Роль' } }),
  isActive: z.boolean().meta({ ui: { title: 'Активен' } }),
})

type UserFormData = z.infer<typeof UserSchema>

const userInitialValues: UserFormData = {
  firstName: '',
  lastName: '',
  email: '',
  age: 25,
  bio: '',
  role: 'user',
  isActive: true,
}

// === Схема для AutoFields с вложенными объектами ===
const ProfileSchema = z.object({
  name: z.string().meta({ ui: { title: 'Имя профиля' } }),
  settings: z.object({
    theme: z.enum(['light', 'dark', 'auto']).meta({ ui: { title: 'Тема' } }),
    notifications: z.boolean().meta({ ui: { title: 'Уведомления' } }),
    language: z.enum(['ru', 'en', 'de']).meta({ ui: { title: 'Язык' } }),
  }),
})

type ProfileFormData = z.infer<typeof ProfileSchema>

const profileInitialValues: ProfileFormData = {
  name: 'Мой профиль',
  settings: {
    theme: 'auto',
    notifications: true,
    language: 'ru',
  },
}

// === Схема для смешанного подхода ===
const ArticleSchema = z.object({
  title: z
    .string()
    .min(5)
    .max(100)
    .meta({ ui: { title: 'Заголовок' } }),
  slug: z.string().meta({ ui: { title: 'URL slug' } }),
  category: z.enum(['news', 'blog', 'tutorial']).meta({ ui: { title: 'Категория' } }),
  content: z.string().meta({ ui: { title: 'Содержимое', fieldType: 'richText' } }),
  published: z.boolean().meta({ ui: { title: 'Опубликовано', fieldType: 'switch' } }),
  rating: z
    .number()
    .min(1)
    .max(5)
    .meta({ ui: { title: 'Рейтинг', fieldType: 'rating' } }),
})

type ArticleFormData = z.infer<typeof ArticleSchema>

const articleInitialValues: ArticleFormData = {
  title: '',
  slug: '',
  category: 'blog',
  content: '',
  published: false,
  rating: 3,
}

export default function AutoFieldsDemoPage() {
  const [fromSchemaData, setFromSchemaData] = useState<UserFormData | null>(null)
  const [autoFieldsData, setAutoFieldsData] = useState<ProfileFormData | null>(null)
  const [mixedData, setMixedData] = useState<ArticleFormData | null>(null)

  return (
    <DemoPageLayout
      title="Auto Fields Demo"
      description="Демонстрация автоматической генерации форм из Zod схемы с использованием Form.FromSchema, Form.AutoFields и смешанного подхода."
      maxW="1200px"
    >
      <VStack gap={8} align="stretch">
        {/* === СЕКЦИЯ 1: Form.FromSchema === */}
        <Box p={6} borderWidth={1} borderRadius="lg">
          <HStack mb={4}>
            <Heading size="md">1. Form.FromSchema</Heading>
            <Badge colorPalette="green">Полностью автоматически</Badge>
          </HStack>
          <Text color="fg.muted" mb={4}>
            Генерирует всю форму из схемы одной строкой. Поля, кнопки — всё автоматически.
          </Text>

          <Code
            display="block"
            whiteSpace="pre"
            mb={4}
            p={3}
            bg="gray.100"
            borderRadius="md"
            _dark={{ bg: 'gray.800' }}
          >
            {`<Form.FromSchema
  schema={UserSchema}
  initialValue={initialValues}
  onSubmit={handleSubmit}
  submitLabel="Создать пользователя"
/>`}
          </Code>

          <Form.FromSchema
            schema={UserSchema}
            initialValue={userInitialValues}
            onSubmit={setFromSchemaData}
            submitLabel="Создать пользователя"
            showReset
            resetLabel="Очистить"
          />

          {fromSchemaData && <SubmittedDataPreview data={fromSchemaData} title="Отправленные данные:" />}
        </Box>

        <Separator />

        {/* === СЕКЦИЯ 2: Form.AutoFields === */}
        <Box p={6} borderWidth={1} borderRadius="lg">
          <HStack mb={4}>
            <Heading size="md">2. Form.AutoFields</Heading>
            <Badge colorPalette="blue">С кастомным layout</Badge>
          </HStack>
          <Text color="fg.muted" mb={4}>
            Генерирует поля из схемы, но layout контролируется вручную. Поддерживает include/exclude.
          </Text>

          <Code
            display="block"
            whiteSpace="pre"
            mb={4}
            p={3}
            bg="gray.100"
            borderRadius="md"
            _dark={{ bg: 'gray.800' }}
          >
            {`<Form schema={ProfileSchema} ...>
  <HStack>
    <Form.AutoFields include={['name']} />
  </HStack>
  <Form.AutoFields exclude={['name']} />
  <Form.Button.Submit />
</Form>`}
          </Code>

          <Form schema={ProfileSchema} initialValue={profileInitialValues} onSubmit={setAutoFieldsData}>
            <VStack gap={4} align="stretch">
              {/* Имя отдельно сверху */}
              <Form.AutoFields include={['name']} />

              {/* Настройки — автоматически развернётся вложенный объект */}
              <Box p={4} bg="gray.50" borderRadius="md">
                <Text fontWeight="bold" mb={2}>
                  Настройки (автоматическая вложенность)
                </Text>
                <Form.AutoFields include={['settings']} />
              </Box>

              <HStack justify="flex-end">
                <Form.Button.Submit>Сохранить профиль</Form.Button.Submit>
              </HStack>
            </VStack>
          </Form>

          {autoFieldsData && <SubmittedDataPreview data={autoFieldsData} title="Отправленные данные:" />}
        </Box>

        <Separator />

        {/* === СЕКЦИЯ 3: Смешанный подход === */}
        <Box p={6} borderWidth={1} borderRadius="lg">
          <HStack mb={4}>
            <Heading size="md">3. Смешанный подход</Heading>
            <Badge colorPalette="purple">AutoFields + ручные поля</Badge>
          </HStack>
          <Text color="fg.muted" mb={4}>
            Часть полей генерируется автоматически, часть — вручную с кастомной логикой. fieldType в meta определяет
            компонент.
          </Text>

          <Code
            display="block"
            whiteSpace="pre"
            mb={4}
            p={3}
            bg="gray.100"
            borderRadius="md"
            _dark={{ bg: 'gray.800' }}
          >
            {`// В схеме:
content: z.string().meta({ ui: { fieldType: 'richText' } })
rating: z.number().meta({ ui: { fieldType: 'rating' } })
published: z.boolean().meta({ ui: { fieldType: 'switch' } })

// В JSX:
<Form.AutoFields exclude={['content']} />
<Form.Field.RichText name="content" />  // Кастомная обёртка`}
          </Code>

          <Form schema={ArticleSchema} initialValue={articleInitialValues} onSubmit={setMixedData}>
            <VStack gap={4} align="stretch">
              {/* Основные поля автоматически */}
              <HStack gap={4} align="flex-start">
                <Box flex={2}>
                  <Form.Field.Auto name="title" />
                </Box>
                <Box flex={1}>
                  <Form.Field.Auto name="slug" />
                </Box>
              </HStack>

              <HStack gap={4}>
                <Box flex={1}>
                  <Form.Field.Auto name="category" />
                </Box>
                <Box flex={1}>
                  <Form.Field.Auto name="rating" />
                </Box>
                <Box flex={1}>
                  <Form.Field.Auto name="published" />
                </Box>
              </HStack>

              {/* Контент — вручную с кастомной высотой */}
              <Box>
                <Form.Field.RichText name="content" label="Содержимое статьи" />
              </Box>

              <HStack justify="flex-end" gap={2}>
                <Form.Button.Reset variant="outline">Сбросить</Form.Button.Reset>
                <Form.Button.Submit>Опубликовать статью</Form.Button.Submit>
              </HStack>
            </VStack>
          </Form>

          {mixedData && <SubmittedDataPreview data={mixedData} title="Отправленные данные:" />}
        </Box>

        <Separator />

        {/* === Документация fieldType === */}
        <Box p={6} borderWidth={1} borderRadius="lg" bg="gray.50">
          <Heading size="md" mb={4}>
            Поддерживаемые fieldType
          </Heading>
          <Text color="fg.muted" mb={4}>
            В <Code>meta(&#123; ui: &#123; fieldType: &apos;...&apos; &#125; &#125;)</Code>{' '}
            можно указать любой тип поля:
          </Text>

          <HStack gap={8} flexWrap="wrap">
            <VStack align="start" gap={1}>
              <Text fontWeight="bold">Текстовые</Text>
              <Code>string</Code>
              <Code>textarea</Code>
              <Code>password</Code>
              <Code>richText</Code>
              <Code>editable</Code>
              <Code>maskedInput</Code>
            </VStack>

            <VStack align="start" gap={1}>
              <Text fontWeight="bold">Числовые</Text>
              <Code>number</Code>
              <Code>numberInput</Code>
              <Code>slider</Code>
              <Code>rating</Code>
              <Code>currency</Code>
              <Code>percentage</Code>
            </VStack>

            <VStack align="start" gap={1}>
              <Text fontWeight="bold">Дата/время</Text>
              <Code>date</Code>
              <Code>time</Code>
              <Code>dateRange</Code>
              <Code>dateTimePicker</Code>
              <Code>duration</Code>
              <Code>schedule</Code>
            </VStack>

            <VStack align="start" gap={1}>
              <Text fontWeight="bold">Выбор</Text>
              <Code>select</Code>
              <Code>nativeSelect</Code>
              <Code>combobox</Code>
              <Code>radioGroup</Code>
              <Code>checkboxCard</Code>
              <Code>tags</Code>
            </VStack>

            <VStack align="start" gap={1}>
              <Text fontWeight="bold">Булевые</Text>
              <Code>checkbox</Code>
              <Code>switch</Code>
            </VStack>

            <VStack align="start" gap={1}>
              <Text fontWeight="bold">Специализированные</Text>
              <Code>phone</Code>
              <Code>address</Code>
              <Code>pinInput</Code>
              <Code>colorPicker</Code>
              <Code>fileUpload</Code>
            </VStack>
          </HStack>
        </Box>
      </VStack>
    </DemoPageLayout>
  )
}

4 уровня контроля

Библиотека предлагает четыре уровня генерации форм — каждый следующий даёт больше контроля:

УровеньКомпонентJSXКогда использовать
1Form.FromSchemaНетCRUD, прототипы, админки
2Form.AutoFieldsТолько layoutКастомная раскладка с автополями
3Form.Field.*Поля + layoutПолный контроль над каждым полем
4useAppFormВсёИмперативная логика, кастомные хуки

Уровни совместимы — можно свободно комбинировать их в одной форме.

Уровень 1: Form.FromSchema — Ноль JSX

Генерирует полную форму из Zod-схемы без единого поля в JSX:

import { Form } from '@letar/forms'
import { z } from 'zod/v4'

const UserSchema = z.object({
  firstName: z.string().min(2).meta({ ui: { title: 'Имя' } }),
  lastName: z.string().meta({ ui: { title: 'Фамилия' } }),
  email: z.string().email().meta({ ui: { title: 'Email' } }),
  bio: z.string().max(500).meta({ ui: { title: 'О себе', fieldType: 'textarea' } }),
})

<Form.FromSchema
  schema={UserSchema}
  initialValue={{ firstName: '', lastName: '', email: '', bio: '' }}
  onSubmit={save}
  submitLabel="Создать"
/>

Рендерит все четыре поля с лейблами, валидацией и кнопкой отправки.

Пропсы FromSchema

ПропТипПо умолчаниюОписание
schemaZod schemaобязательныйСхема для валидации и генерации полей
initialValueTDataобязательныйНачальные значения формы
onSubmit(data: TData) => voidобязательныйОбработчик отправки
submitLabelReactNode'Save'Текст кнопки отправки
showResetbooleanfalseПоказать кнопку сброса
resetLabelReactNode'Reset'Текст кнопки сброса
excludestring[]—Поля для исключения
validateOnValidateOn—Режим валидации
middlewareFormMiddleware—Middleware для событий формы
disabledboolean—Заблокировать все поля
readOnlyboolean—Режим только чтение
persistenceFormPersistenceConfig—Конфигурация localStorage
offlineFormOfflineConfig—Конфигурация оффлайн-режима
debugboolean | 'force'—JSON-инспектор значений
gapnumber4Отступ между полями
beforeButtonsReactNode—Контент перед кнопками
afterButtonsReactNode—Контент после кнопок

Исключение полей

Скрываем системные поля (id, timestamp-ы):

<Form.FromSchema
  schema={UserSchema}
  initialValue={userData}
  onSubmit={updateUser}
  exclude={['id', 'createdAt', 'updatedAt']}
  showReset
/>

Уровень 2: Form.AutoFields — Автогенерация + кастомный layout

Когда нужен контроль над раскладкой, но не хочется писать каждое поле вручную:

<Form schema={Schema} initialValue={data} onSubmit={save}>
  <HStack gap={4}>
    <Form.AutoFields include={['firstName', 'lastName']} />
  </HStack>
  <Form.AutoFields exclude={['firstName', 'lastName']} />
  <Form.Button.Submit />
</Form>

Пропсы AutoFields

ПропТипПо умолчаниюОписание
includestring[]—Рендерить только эти поля
excludestring[]—Исключить эти поля
recursivebooleantrueРаскрывать вложенные объекты
fieldWrapper(props) => ReactElement—Кастомная обёртка для каждого поля

Кастомная обёртка полей

Обернуть каждое автогенерируемое поле в кастомный JSX:

<Form.AutoFields
  fieldWrapper={({ name, children }) => (
    <Box key={name} p={2} borderWidth={1} borderRadius="md">
      {children}
    </Box>
  )}
/>

Смешанный подход: авто + ручные поля

Комбинируйте автогенерируемые и ручные поля:

<Form schema={ArticleSchema} initialValue={data} onSubmit={save}>
  <HStack gap={4}>
    <Box flex={2}>
      <Form.Field.Auto name="title" />
    </Box>
    <Box flex={1}>
      <Form.Field.Auto name="slug" />
    </Box>
  </HStack>
  <Form.Field.RichText name="content" label="Содержимое статьи" />
  <Form.Button.Submit>Опубликовать</Form.Button.Submit>
</Form>

Form.Field.Auto рендерит подходящий компонент на основе типа поля в схеме.

Маппинг типов

Библиотека автоматически сопоставляет Zod-типы с компонентами полей:

Маппинг по умолчанию (Zod → Компонент)

Zod типКомпонентУсловие
z.string()Form.Field.StringПо умолчанию
z.string()Form.Field.TextareaЕсли maxLength > 200
z.string().email()Form.Field.StringС type="email"
z.number() / z.int() / z.float()Form.Field.Number
z.boolean()Form.Field.Checkbox
z.date()Form.Field.Date
z.enum() / z.literal()Form.Field.NativeSelect
z.array(z.string())Form.Field.TagsМассив строк
z.array(z.object())Form.Group.ListМассив объектов

Переопределение через fieldType

Используйте .meta({ ui: { fieldType } }) для переопределения маппинга:

const Schema = z.object({
  // Textarea вместо строки
  description: z.string().meta({ ui: { title: 'Описание', fieldType: 'textarea' } }),

  // Слайдер вместо числового поля
  rating: z
    .number()
    .min(0)
    .max(10)
    .meta({ ui: { title: 'Рейтинг', fieldType: 'slider' } }),

  // Rich text редактор
  content: z.string().meta({ ui: { title: 'Контент', fieldType: 'richText' } }),

  // Switch вместо чекбокса
  active: z.boolean().meta({ ui: { title: 'Активен', fieldType: 'switch' } }),

  // Radio cards вместо select
  plan: z.enum(['free', 'pro', 'enterprise']).meta({
    ui: {
      title: 'Тариф',
      fieldType: 'radioCard',
      fieldProps: {
        options: [
          { value: 'free', label: 'Бесплатный', description: 'Для личного использования' },
          { value: 'pro', label: 'Профессиональный', description: 'Для команд' },
          { value: 'enterprise', label: 'Корпоративный', description: 'Индивидуально' },
        ],
      },
    },
  }),
})

Все доступные fieldType

Текст: string, textarea, password, passwordStrength, editable, richText, maskedInput

Числа: number, numberInput, slider, rating, currency, percentage

Дата/Время: date, time, dateRange, dateTimePicker, duration, schedule

Логические: checkbox, switch

Выбор: select, nativeSelect, combobox, autocomplete, listbox, radioGroup, radioCard, segmentedGroup, checkboxCard, tags

Специальные: phone, address, pinInput, otpInput, colorPicker, fileUpload

UI-метаданные

Управление внешним видом полей через .meta({ ui: {...} }):

const Schema = z.object({
  email: z
    .string()
    .email()
    .meta({
      ui: {
        title: 'Email-адрес', // Лейбл
        placeholder: 'user@example.com', // Плейсхолдер
        description: 'Мы никому не передаём', // Подсказка под полем
        fieldType: 'string', // Переопределение компонента
        fieldProps: { type: 'email' }, // Пропсы для компонента
      },
    }),
})
Свойство metaТипОписание
titlestringЛейбл поля
placeholderstringТекст-заполнитель
descriptionstringПодсказка под полем
fieldTypeFieldComponentTypeПереопределение компонента
fieldPropsRecord<string, unknown>Дополнительные пропсы компонента
optionsBaseOption[]Опции для select-полей
autocompletestringHTML атрибут autocomplete

Если title не указан, имя поля конвертируется из camelCase в читаемый формат (firstName → First Name).

Вложенные объекты и массивы

Вложенные объекты

Объекты автоматически рендерятся как Form.Group:

const ProfileSchema = z.object({
  name: z.string().meta({ ui: { title: 'Имя' } }),
  settings: z.object({
    theme: z.enum(['light', 'dark']).meta({ ui: { title: 'Тема' } }),
    notifications: z.boolean().meta({ ui: { title: 'Уведомления' } }),
  }),
})

// AutoFields автоматически создаёт группу для settings
<Form.FromSchema schema={ProfileSchema} initialValue={data} onSubmit={save} />

Отключить рекурсивное раскрытие: recursive={false}.

Массивы объектов

Массивы объектов рендерятся как Form.Group.List с кнопками добавления/удаления:

const OrderSchema = z.object({
  customer: z.string(),
  items: z.array(
    z.object({
      product: z.string().meta({ ui: { title: 'Товар' } }),
      qty: z.number().min(1).meta({ ui: { title: 'Количество' } }),
    })
  ),
})

// Каждый элемент массива — карточка с полями + кнопка удаления
<Form.FromSchema schema={OrderSchema} initialValue={data} onSubmit={save} />

Примитивные массивы

Массивы строк рендерятся как Form.Field.Tags:

const Schema = z.object({
  tags: z.array(z.string()).meta({ ui: { title: 'Теги' } }),
})

Автоматические constraints

Ограничения из Zod автоматически передаются как пропсы компонентов:

z.string().min(2).max(100) // → minLength={2} maxLength={100}
z.number().min(1).max(10) // → min={1} max={10}
z.string().email() // → type="email"
z.date().min(new Date()) // → min={now}

Не нужно дублировать ограничения в JSX — схема является единым источником истины.

Когда какой уровень использовать

СценарийРекомендуемый уровень
Админские CRUD-формыForm.FromSchema
Быстрое прототипированиеForm.FromSchema
Простые формы с кастомным layoutForm.AutoFields
Сложные многоколоночные layoutsForm.Field.* (Compound Components)
Мультистеп формыForm.Field.* + Form.Steps
Условный рендерингForm.Field.* + Form.When
Кастомный UX (анимации, inline-editing)useAppForm

Правило: Если форма — линейный вертикальный поток, FromSchema или AutoFields отлично подходят. Для сложных layouts используйте Compound Components.

Живые примеры

Попробуйте интерактивные примеры на forms-example.letar.best:

On this page