😏

shadcn/uiを使用したフォーム作成(Next.js,zod,react-hook-form)

に公開

はじめに

先日shadcn/uiを初めて触ったので、shadcn/uiの勉強がてらzodとreact-hook-formを使用してフォームを作成したので自分のための忘備録となります。
もしご興味がある方は先日の私の記事をご覧ください。

GitHub

https://github.com/kiyo-8jo/zenn-shadcn-form

環境構築

プロジェクト作成

npx create-next-app@latest

shadcn/uiの初期化

npx shadcn@latest init

formに関するコンポーネントインストール

npx shadcn@latest add form

inputコンポーネントインストール

npx shadcn@latest add input

checkboxコンポーネントインストール

npx shadcn@latest add checkbox

完成時ディレクトリ

├── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components
│   ├── contact
│   │   ├── ContactPage.tsx
│   │   └── schema.ts
│   └── ui
│       ├── button.tsx
│       ├── checkbox.tsx
│       ├── form.tsx
│       ├── input.tsx
│       ├── label.tsx
│       └── textarea.tsx
└── lib
    └── utils.ts

フォーム作成

page.tsx
import ContactPage from "@/components/contact/ContactPage";

export default function Home() {
  return (
    <main>
      <ContactPage />
    </main>
  );
}
schema.ts
import { z } from "zod";

const name: z.ZodString = z
  .string({ required_error: "必須項目です" })
  .min(1, { message: "必須項目です" })
  .max(30, { message: "入力値が長すぎます" });

const email: z.ZodString = z
  .string({ required_error: "必須項目です" })
  .min(1, { message: "必須項目です" })
  .max(50, { message: "入力値が長すぎます" })
  .email({ message: "メールアドレスの形式で入力してください" });

const phone: z.ZodString = z
  .string({ required_error: "必須項目です" })
  .min(10, { message: "電話番号を入力してください" })
  .max(15, { message: "入力値が長すぎます" });

const message: z.ZodString = z
  .string({ required_error: "必須項目です" })
  .min(1, { message: "必須項目です" })
  .max(5000, { message: "入力値が長すぎます" });

const agree: z.ZodLiteral<boolean> = z.literal(true, {
  errorMap: () => ({ message: "同意が必要です" }),
});

export const ContactSchema = z.object({
  name,
  email,
  phone,
  message,
  agree,
});

export type ContactType = z.infer<typeof ContactSchema>;
ContactPage.tsx
"use client";

import { zodResolver } from "@hookform/resolvers/zod";

import { useForm } from "react-hook-form";
import { ContactSchema, ContactType } from "./schema";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "../ui/form";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import { Checkbox } from "../ui/checkbox";
import { Button } from "../ui/button";

const ContactPage = () => {
  const form = useForm<ContactType>({
    mode: "onSubmit",
    resolver: zodResolver(ContactSchema),
    defaultValues: {
      name: "",
      email: "",
      phone: "",
      message: "",
      agree: false,
    },
  });

  const handleOnSubmit = form.handleSubmit((data) => console.log(data));

  return (
    <div className="container h-screen mx-auto pt-10">
      <h1 className="text-center font-bold text-2xl mb-10">お問い合わせ</h1>
      <Form {...form}>
        <form
          method="post"
          onSubmit={handleOnSubmit}
          className="flex flex-col space-y-5"
        >
          {/* name */}
          <FormField
            control={form.control}
            name={"name"}
            render={({ field }) => (
              <FormItem>
                <FormLabel>お名前</FormLabel>
                <FormControl>
                  <Input {...field} placeholder="例) 田中 太郎"></Input>
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          {/* email */}
          <FormField
            control={form.control}
            name={"email"}
            render={({ field }) => (
              <FormItem>
                <FormLabel>メールアドレス</FormLabel>
                <FormControl>
                  <Input {...field} placeholder="例) test@email.com"></Input>
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          {/* phone */}
          <FormField
            control={form.control}
            name={"phone"}
            render={({ field }) => (
              <FormItem>
                <FormLabel>電話番号</FormLabel>
                <FormControl>
                  <Input {...field} placeholder="例) 00011112222"></Input>
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          {/* message */}
          <FormField
            control={form.control}
            name={"message"}
            render={({ field }) => (
              <FormItem>
                <FormLabel>お問い合わせ内容</FormLabel>
                <FormControl>
                  <Textarea
                    {...field}
                    placeholder="例) お問い合わせ内容"
                    className="resize-none h-50"
                  ></Textarea>
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          {/* agree */}
          <div className="self-center">
            <FormField
              control={form.control}
              name={"agree"}
              render={({ field }) => (
                <FormItem>
                  <div className="flex mt-10">
                    <FormControl>
                      <Checkbox
                        checked={field.value}
                        onCheckedChange={field.onChange}
                      />
                    </FormControl>
                    <FormLabel className="ml-2">個人情報取り扱いに同意する</FormLabel>
                  </div>
                  <FormMessage />
                </FormItem>
              )}
            />
          </div>

          <Button type="submit" className="cursor-pointer self-center mt-10">
            送信する
          </Button>
        </form>
      </Form>
    </div>
  );
};

export default ContactPage;

フォーム完成


全ての要素を必須項目にしているのでsubmitできない。


コンソールを見ると要素が取得できていることがわかる。

まとめ

独自のコンポーネント(FormFiled, FormItem, FormControl, FormLabel, FormMessage)とcheckboxのoncheckedChangeに戸惑ったが、理解してしまえばとても簡単にフォームを作成することができた。よりshadcn/uiが好きになった。

Discussion