Формы
Введение
В нашем проекте формы являются ключевым элементом взаимодействия с пользователем. Мы используем компоненты Shadcn UI и нативные React подходы для построения форм. В этом разделе документации описаны основные принципы работы с формами, доступные компоненты и рекомендуемые практики.
Базовые компоненты форм
В нашем проекте используются следующие базовые компоненты для создания форм:
Input
Компонент для текстовых полей ввода. Поддерживает все стандартные HTML атрибуты для элемента input.
import { Input } from "@/components/ui/input"; <Input type="email" placeholder="example@mail.com" required value={email} onChange={(e) => setEmail(e.target.value)} />
Label
Компонент для создания подписей к полям формы.
import { Label } from "@/components/ui/label"; <Label htmlFor="email">Email</Label> <Input id="email" ... />
Button
Компонент для кнопок формы с различными вариантами оформления.
import { Button } from "@/components/ui/button"; <Button type="submit" disabled={isLoading}> {isLoading ? "Отправка..." : "Отправить"} </Button>
Switch
Компонент переключателя для булевых значений.
import { Switch } from "@/components/ui/switch"; <div className="flex items-center space-x-2"> <Switch id="dark-mode" checked={isDarkMode} onCheckedChange={setIsDarkMode} /> <Label htmlFor="dark-mode">Темная тема</Label> </div>
Создание простой формы
Пример создания простой формы с обработкой состояния и отправкой данных:
"use client"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; export default function ContactForm() { const [isLoading, setIsLoading] = useState(false); const [formData, setFormData] = useState({ name: "", email: "", message: "" }); const [error, setError] = useState(""); const handleChange = (e) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(""); try { const response = await fetch("/api/contact", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData) }); if (!response.ok) { const data = await response.json(); throw new Error(data.message || "Произошла ошибка"); } // Сброс формы после успешной отправки setFormData({ name: "", email: "", message: "" }); alert("Сообщение отправлено!"); } catch (err) { setError(err.message || "Не удалось отправить сообщение"); } finally { setIsLoading(false); } }; return ( <form onSubmit={handleSubmit} className="space-y-4"> <div className="space-y-2"> <Label htmlFor="name">Имя</Label> <Input id="name" name="name" value={formData.name} onChange={handleChange} required /> </div> <div className="space-y-2"> <Label htmlFor="email">Email</Label> <Input id="email" name="email" type="email" value={formData.email} onChange={handleChange} required /> </div> <div className="space-y-2"> <Label htmlFor="message">Сообщение</Label> <textarea id="message" name="message" value={formData.message} onChange={handleChange} required className="w-full min-h-[120px] rounded-md border border-input bg-transparent px-3 py-2" /> </div> {error && <div className="text-red-500 text-sm">{error}</div>} <Button type="submit" disabled={isLoading}> {isLoading ? "Отправка..." : "Отправить"} </Button> </form> ); }
Валидация форм
Существует несколько подходов к валидации форм в нашем проекте:
1. Нативная валидация HTML
Простейший способ валидации с использованием встроенных HTML атрибутов, таких как required, minlength, maxlength, pattern и т.д.
<Input required minLength={8} maxLength={50} pattern="[a-zA-Z0-9]+" title="Используйте только буквы и цифры" />
2. Ручная валидация в React
Подход, используемый в примерах нашего кода, где мы проверяем входные данные перед отправкой формы:
const handleSubmit = async (e) => { e.preventDefault(); setError(""); // Валидация if (formData.password.length < 8) { setError("Пароль должен содержать минимум 8 символов"); return; } if (formData.password !== formData.confirmPassword) { setError("Пароли не совпадают"); return; } // Продолжаем обработку формы если валидация прошла setIsLoading(true); // ... }
3. Расширенная валидация с библиотеками
Для более сложных форм рекомендуется использовать библиотеки как React Hook Form и Zod:
Для добавления React Hook Form и Zod к проекту, выполните:
npm install react-hook-form zod @hookform/resolvers
Пример формы с валидацией через React Hook Form и Zod:
"use client"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; // Определение схемы валидации const formSchema = z.object({ name: z.string().min(2, "Имя должно содержать минимум 2 символа"), email: z.string().email("Введите корректный email адрес"), password: z .string() .min(8, "Пароль должен содержать минимум 8 символов") .regex( /[A-Z]/, "Пароль должен содержать хотя бы одну заглавную букву" ) .regex( /[0-9]/, "Пароль должен содержать хотя бы одну цифру" ), confirmPassword: z.string() }).refine((data) => data.password === data.confirmPassword, { message: "Пароли не совпадают", path: ["confirmPassword"] }); export default function RegisterForm() { const [isLoading, setIsLoading] = useState(false); const { register, handleSubmit, formState: { errors }, reset } = useForm({ resolver: zodResolver(formSchema), defaultValues: { name: "", email: "", password: "", confirmPassword: "" } }); const onSubmit = async (data) => { setIsLoading(true); try { // Отправка данных на сервер const response = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error("Не удалось зарегистрироваться"); } // Сброс формы reset(); } catch (error) { console.error(error); } finally { setIsLoading(false); } }; return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <div className="space-y-2"> <Label htmlFor="name">Имя</Label> <Input id="name" {...register("name")} /> {errors.name && ( <p className="text-red-500 text-sm">{errors.name.message}</p> )} </div> <div className="space-y-2"> <Label htmlFor="email">Email</Label> <Input id="email" type="email" {...register("email")} /> {errors.email && ( <p className="text-red-500 text-sm">{errors.email.message}</p> )} </div> <div className="space-y-2"> <Label htmlFor="password">Пароль</Label> <Input id="password" type="password" {...register("password")} /> {errors.password && ( <p className="text-red-500 text-sm">{errors.password.message}</p> )} </div> <div className="space-y-2"> <Label htmlFor="confirmPassword">Подтверждение пароля</Label> <Input id="confirmPassword" type="password" {...register("confirmPassword")} /> {errors.confirmPassword && ( <p className="text-red-500 text-sm">{errors.confirmPassword.message}</p> )} </div> <Button type="submit" disabled={isLoading}> {isLoading ? "Регистрация..." : "Зарегистрироваться"} </Button> </form> ); }
Примеры из проекта
1. Форма входа (LoginForm)
Используется в модальном окне авторизации. Обрабатывает вход пользователя и отправляет данные на сервер.
// components/auth/LoginForm.tsx "use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; export default function LoginForm({ onToggleMode }) { const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [formData, setFormData] = useState({ email: "", password: "", }); const handleChange = (e) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null); try { const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData), }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || "Не удалось войти"); } router.push("/dashboard"); } catch (err) { setError( err instanceof Error ? err.message : "Произошла ошибка при входе" ); } finally { setIsLoading(false); } }; return ( <form onSubmit={handleSubmit} className="space-y-4"> <div className="space-y-2"> <Label htmlFor="email">Email</Label> <Input id="email" name="email" type="email" required value={formData.email} onChange={handleChange} /> </div> <div className="space-y-2"> <Label htmlFor="password">Пароль</Label> <Input id="password" name="password" type="password" required value={formData.password} onChange={handleChange} /> </div> {error && <div className="text-red-500 text-sm">{error}</div>} <Button type="submit" className="w-full" disabled={isLoading}> {isLoading ? "Вход..." : "Войти"} </Button> </form> ); }
2. Форма изменения настроек профиля
Позволяет пользователю обновить свои настройки в личном кабинете.
// app/dashboard/_components/settings/ProfileSettings.tsx "use client"; import { useState } from "react"; import { useAuth } from "@/contexts/AuthContext"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { toast } from "sonner"; export default function ProfileSettings() { const { user, refreshUser } = useAuth(); const [isLoading, setIsLoading] = useState(false); const [formData, setFormData] = useState({ firstName: user?.first_name || "", lastName: user?.last_name || "", }); const handleChange = (e) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); try { const response = await fetch("/api/user/profile", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ first_name: formData.firstName, last_name: formData.lastName, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || "Не удалось обновить профиль"); } await refreshUser(); toast.success("Профиль успешно обновлен"); } catch (error) { toast.error( error instanceof Error ? error.message : "Не удалось обновить профиль" ); } finally { setIsLoading(false); } }; return ( <Card> <CardHeader> <CardTitle>Профиль</CardTitle> </CardHeader> <CardContent> <form onSubmit={handleSubmit} className="space-y-4"> <div className="grid grid-cols-2 gap-4"> <div className="space-y-2"> <Label htmlFor="firstName">Имя</Label> <Input id="firstName" name="firstName" value={formData.firstName} onChange={handleChange} /> </div> <div className="space-y-2"> <Label htmlFor="lastName">Фамилия</Label> <Input id="lastName" name="lastName" value={formData.lastName} onChange={handleChange} /> </div> </div> <Button type="submit" disabled={isLoading}> {isLoading ? "Сохранение..." : "Сохранить изменения"} </Button> </form> </CardContent> </Card> ); }
Рекомендации и лучшие практики
- Единое состояние формы - Храните все поля формы в одном объекте состояния, это упрощает управление и обновление данных.
- Обработка загрузки - Всегда используйте состояние загрузки (isLoading) для блокировки формы во время отправки данных.
- Обработка ошибок - Обеспечивайте обратную связь в случае ошибок валидации или проблем при отправке данных.
- Сброс формы - После успешной отправки очищайте поля формы, если это необходимо.
- Повторное использование логики - Выделяйте повторяющуюся логику форм в отдельные хуки для повторного использования.
- Доступность - Всегда связывайте метки (Label) с полями ввода для улучшения доступности.
- Валидация - Предпочитайте клиентскую валидацию перед отправкой запроса для экономии ресурсов сервера и улучшения пользовательского опыта.
Дополнительные компоненты форм
Shadcn UI предоставляет множество дополнительных компонентов для создания более сложных форм. При необходимости вы можете установить их с помощью следующих команд:
# Установка компонента Select для выпадающих списков npx shadcn@latest add select # Установка Checkbox для чекбоксов npx shadcn@latest add checkbox # Установка RadioGroup для радио кнопок npx shadcn@latest add radio-group # Установка Textarea для многострочного ввода npx shadcn@latest add textarea # Установка календаря для выбора даты npx shadcn@latest add calendar
Для более подробной информации о доступных компонентах Shadcn UI посетитеофициальную документацию.