Validation
Schema-driven validation with Zod and custom error handling
Full example
The complete sandbox example, read directly from the form-develop-app source at build time —
automatic constraint extraction from the Zod schema (min/max, length, email/url, patterns, dates,
array bounds) across every field type.
'use client'
import { Box, Code, Heading, HStack, Separator, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { useState } from 'react'
import { z } from 'zod/v4'
import { DemoPageLayout, SubmittedDataPreview } from '../_components'
/**
* Демонстрация автоматического извлечения constraints из Zod схемы
*
* Поля автоматически получают:
* - min/max/step для чисел
* - minLength/maxLength для строк
* - type="email"/"url" для email/url
* - pattern для regex
* - min/max для дат
* - minItems/maxItems для массивов (отключение Add/Remove кнопок)
*/
const ConstraintsSchema = z.object({
// === СТРОКИ ===
// minLength + maxLength → автоматический helperText "От 2 до 50 символов"
title: z
.string()
.min(2)
.max(50)
.meta({
ui: { title: 'Название (2-50 символов)', placeholder: 'Введите название' },
}),
// maxLength → автоматический helperText "Максимум 200 символов"
description: z
.string()
.max(200)
.meta({
ui: { title: 'Описание (макс. 200)', placeholder: 'Краткое описание' },
}),
// email() → type="email"
email: z
.string()
.email()
.meta({
ui: { title: 'Email', placeholder: 'user@example.com' },
}),
// url() → type="url"
website: z
.string()
.url()
.optional()
.meta({
ui: { title: 'Веб-сайт', placeholder: 'https://example.com' },
}),
// length() → minLength=maxLength=6
code: z
.string()
.length(6)
.meta({
ui: { title: 'Код (ровно 6 символов)', placeholder: 'ABC123' },
}),
// regex() → pattern
phone: z
.string()
.regex(/^\+7\d{10}$/)
.optional()
.meta({
ui: { title: 'Телефон (regex)', placeholder: '+79991234567' },
}),
// === ЧИСЛА ===
// min + max → автоматический helperText "От 1 до 10"
rating: z
.number()
.min(1)
.max(10)
.meta({
ui: { title: 'Рейтинг (1-10)' },
}),
// int() → step=1
quantity: z
.number()
.int()
.min(1)
.max(100)
.meta({
ui: { title: 'Количество (целое)' },
}),
// multipleOf() → step
price: z
.number()
.min(0)
.max(10000)
.multipleOf(0.01)
.meta({
ui: { title: 'Цена (шаг 0.01)' },
}),
// Слайдер с автоматическими min/max
progress: z
.number()
.min(0)
.max(100)
.meta({
ui: { title: 'Прогресс (слайдер)' },
}),
// === ДАТЫ ===
// min + max для дат
startDate: z.coerce
.date()
.min(new Date('2024-01-01'))
.meta({
ui: { title: 'Дата начала (мин: 2024-01-01)' },
}),
endDate: z.coerce
.date()
.max(new Date('2025-12-31'))
.meta({
ui: { title: 'Дата окончания (макс: 2025-12-31)' },
}),
// === МАССИВЫ ===
// min + max для массивов → автоматическое отключение Add/Remove
tags: z
.array(z.string().min(1))
.min(1)
.max(5)
.meta({
ui: { title: 'Теги (1-5 элементов)' },
}),
})
type ConstraintsFormData = z.infer<typeof ConstraintsSchema>
const initialValues: ConstraintsFormData = {
title: '',
description: '',
email: '',
website: '',
code: '',
phone: '',
rating: 5,
quantity: 1,
price: 0,
progress: 50,
startDate: new Date('2024-06-01'),
endDate: new Date('2025-06-01'),
tags: ['example'],
}
export default function ConstraintsDemoPage() {
const [submittedData, setSubmittedData] = useState<ConstraintsFormData | null>(null)
const handleSubmit = (data: ConstraintsFormData) => {
setSubmittedData(data)
}
return (
<DemoPageLayout
title="Constraints Demo"
description="Автоматическое извлечение constraints из Zod схемы. Поля получают min/max/step/type автоматически из схемы."
maxW="800px"
>
<Form initialValue={initialValues} schema={ConstraintsSchema} onSubmit={handleSubmit}>
<VStack gap={6} align="stretch">
{/* Секция: Строки */}
<Box>
<Heading size="md" mb={4}>
Строки
</Heading>
<VStack gap={4} align="stretch">
<Box>
<Form.Field.String name="title" />
<Code fontSize="xs" mt={1}>
z.string().min(2).max(50) → minLength=2 maxLength=50
</Code>
</Box>
<Box>
<Form.Field.Textarea name="description" />
<Code fontSize="xs" mt={1}>
z.string().max(200) → maxLength=200
</Code>
</Box>
<HStack gap={4} align="flex-start">
<Box flex={1}>
<Form.Field.String name="email" />
<Code fontSize="xs" mt={1}>
z.string().email() → type="email"
</Code>
</Box>
<Box flex={1}>
<Form.Field.String name="website" />
<Code fontSize="xs" mt={1}>
z.string().url() → type="url"
</Code>
</Box>
</HStack>
<HStack gap={4} align="flex-start">
<Box flex={1}>
<Form.Field.String name="code" />
<Code fontSize="xs" mt={1}>
z.string().length(6) → min=max=6
</Code>
</Box>
<Box flex={1}>
<Form.Field.String name="phone" />
<Code fontSize="xs" mt={1}>
z.string().regex() → pattern
</Code>
</Box>
</HStack>
</VStack>
</Box>
<Separator />
{/* Секция: Числа */}
<Box>
<Heading size="md" mb={4}>
Числа
</Heading>
<VStack gap={4} align="stretch">
<HStack gap={4} align="flex-start">
<Box flex={1}>
<Form.Field.Number name="rating" />
<Code fontSize="xs" mt={1}>
z.number().min(1).max(10) → min=1 max=10
</Code>
</Box>
<Box flex={1}>
<Form.Field.Number name="quantity" />
<Code fontSize="xs" mt={1}>
z.number().int() → step=1
</Code>
</Box>
</HStack>
<Box>
<Form.Field.Number name="price" />
<Code fontSize="xs" mt={1}>
z.number().multipleOf(0.01) → step=0.01
</Code>
</Box>
<Box>
<Form.Field.Slider name="progress" showValue colorPalette="blue" />
<Code fontSize="xs" mt={1}>
z.number().min(0).max(100) → Slider min=0 max=100
</Code>
</Box>
</VStack>
</Box>
<Separator />
{/* Секция: Даты */}
<Box>
<Heading size="md" mb={4}>
Даты
</Heading>
<HStack gap={4} align="flex-start">
<Box flex={1}>
<Form.Field.Date name="startDate" />
<Code fontSize="xs" mt={1}>
z.date().min(new Date) → min="2024-01-01"
</Code>
</Box>
<Box flex={1}>
<Form.Field.Date name="endDate" />
<Code fontSize="xs" mt={1}>
z.date().max(new Date) → max="2025-12-31"
</Code>
</Box>
</HStack>
</Box>
<Separator />
{/* Секция: Массивы */}
<Box>
<Heading size="md" mb={4}>
Массивы
</Heading>
<Form.Group.List
name="tags"
wrapper={({ children }) => (
<VStack align="stretch" gap={2}>
{children}
<Form.Group.List.Button.Add defaultValue="">Добавить тег</Form.Group.List.Button.Add>
<Code fontSize="xs">z.array().min(1).max(5) → Add отключается при 5, Remove при 1</Code>
</VStack>
)}
>
<HStack gap={2}>
<Form.Field.String placeholder="Введите тег" />
<Form.Group.List.Button.Remove />
</HStack>
</Form.Group.List>
</Box>
<Separator />
<Form.Button.Submit>Отправить</Form.Button.Submit>
</VStack>
</Form>
{submittedData && (
<SubmittedDataPreview
data={Object.fromEntries(
Object.entries(submittedData).map(([key, value]) => [
key,
value instanceof Date ? value.toISOString().split('T')[0] : value,
]),
)}
title="Отправленные данные:"
/>
)}
</DemoPageLayout>
)
}shadcn-вариант этого примера ещё не готов.
Vue-пример для этого поля появится позже.
Angular-пример для этого поля появится позже.
Schema-Based Validation
All validation is defined in the Zod schema. The form automatically validates fields and displays errors.
const Schema = z
.object({
email: z.string().email('Invalid email address'),
password: z
.string()
.min(8, 'Must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain an uppercase letter')
.regex(/[0-9]/, 'Must contain a number'),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
})Error Summary
Display all form errors in one place:
<Form schema={Schema} initialValue={data} onSubmit={save}>
<Form.Field.String name="email" />
<Form.Field.Password name="password" />
<Form.Field.Password name="confirmPassword" />
<Form.Errors title="Please fix the following:" />
<Form.Button.Submit />
</Form>Auto Constraint Hints
The library automatically generates helper text from Zod constraints:
z.string().min(2).max(100)
// Helper text: "2–100 characters"
z.number().min(1).max(10)
// Helper text: "1–10"
z.string().email()
// Helper text: "Must be a valid email"Strip Unknown Fields
Always use .strip() in production to remove extra fields:
const Schema = z
.object({
name: z.string(),
email: z.string().email(),
})
.strip()