Формы

Введение

В нашем проекте формы являются ключевым элементом взаимодействия с пользователем. Мы используем компоненты 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>
  );
}

Рекомендации и лучшие практики

  1. Единое состояние формы - Храните все поля формы в одном объекте состояния, это упрощает управление и обновление данных.
  2. Обработка загрузки - Всегда используйте состояние загрузки (isLoading) для блокировки формы во время отправки данных.
  3. Обработка ошибок - Обеспечивайте обратную связь в случае ошибок валидации или проблем при отправке данных.
  4. Сброс формы - После успешной отправки очищайте поля формы, если это необходимо.
  5. Повторное использование логики - Выделяйте повторяющуюся логику форм в отдельные хуки для повторного использования.
  6. Доступность - Всегда связывайте метки (Label) с полями ввода для улучшения доступности.
  7. Валидация - Предпочитайте клиентскую валидацию перед отправкой запроса для экономии ресурсов сервера и улучшения пользовательского опыта.

Дополнительные компоненты форм

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 посетитеофициальную документацию.