Open5

shadcn/ui の Formコンポーネント

TohdaTohda

https://ui.shadcn.com/docs/components/form

shadcn/ui の <Form /> コンポーネントはreact-hook-formライブラリのラッパーです。

特徴

  • フォームを構築するための Composable components
  • 制御されたフォームフィールドを構築するための <FormField /> component
  • zod を使用したフォームバリデーション
  • アクセシビリティとエラーメッセージを処理
  • 一意なIDを生成するために React.useId() を使用
  • 状態に基づいてフォームフィールドに正しい aria 属性を適用
  • すべての Radix UIコンポーネント で動作するように構築
  • 自分の好きなスキーマ・ライブラリが使える(shadcn/ui はzod)
  • マークアップとスタイリングを完全にコントロールできる
TohdaTohda

使い方

  • useFormフックを使ってフォームの制御オブジェクトを作成します。
  • Formコンポーネントでフォーム全体をラップし、formプロパティで制御オブジェクトを渡します。
  • FormFieldコンポーネントでControllerをラップし、name属性でフィールド名を指定します。
  • FormItemコンポーネントでフィールド全体をラップし、IDを提供します。
  • FormLabelでフィールドのラベルを定義します。
  • FormControlで実際の入力フィールドをラップし、Inputコンポーネントを使用します。
  • FormDescriptionでフィールドの説明を表示します。
  • FormMessageでエラーメッセージを表示します。
const form = useForm()

<Form form={form}>
  <FormField
    control={form.control}
    name="username"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Username</FormLabel>
        <FormControl>
          <Input placeholder="shadcn" {...field} />
        </FormControl>
        <FormDescription>This is your public display name.</FormDescription>
        <FormMessage />
      </FormItem>
    )}
  />
</Form>
TohdaTohda

1. useFormフックを使ってフォームの制御オブジェクトを作成します。

const form = useForm()

2. Formコンポーネントでフォーム全体をラップし、formプロパティで制御オブジェクトを渡します。

  • Formコンポーネントは、react-hook-formFormProviderをラップしてエクスポートしています。
  • このコンポーネントは、フォーム全体の状態を管理し、子コンポーネントにフォームの状態やメソッドを提供するために使用されます。
const form = useForm()

<Form form={form}>
</Form>

3. FormFieldコンポーネントでControllerをラップし、name属性でフィールド名を指定します。

  • FormFieldコンポーネントは、react-hook-formControllerコンポーネントをラップし、フィールドのコンテキストを提供します。
  • これにより、フォームの各フィールドが一貫して管理され、簡単にエラーメッセージや状態を取得できます。
const form = useForm()

<Form form={form}>
  <FormField
    control={form.control}
    name="username"
  />
</Form>

4. FormItemコンポーネントでフィールド全体をラップし、IDを提供します。

  • FormItemコンポーネントは、各FormFieldをラップするコンテナとして機能し、フィールドごとに固有のIDを提供します。
  • このIDは、フィールドのラベルやエラーメッセージ、説明文と関連付けるために使用されます。
const form = useForm()

<Form form={form}>
  <FormField
    control={form.control}
    name="username"
    render={({ field }) => (
      <FormItem>
      </FormItem>
    )}
  />
</Form>

5. FormLabelでフィールドのラベルを定義します。

フィールドにエラーがある場合、クラス名にtext-destructiveを追加してエラースタイルを適用します。

const form = useForm()

<Form form={form}>
  <FormField
    control={form.control}
    name="username"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Username</FormLabel>
      </FormItem>
    )}
  />
</Form>

6. FormControlで実際の入力フィールドをラップし、Inputコンポーネントを使用します。

FormControlコンポーネントは、実際の入力フィールド(例:<input>, <select>, <textarea>など)をラップし、アクセシビリティ属性やエラーメッセージの表示に必要な属性を適切に設定します。

属性の設定:

  • id: フォームコントロールのIDを設定します。このIDはラベルやエラーメッセージとの関連付けに使用されます。
  • aria-describedby: フォームコントロールの説明文とエラーメッセージを参照するための属性です。エラーがある場合はエラーメッセージのIDも含めます。
  • aria-invalid: フィールドにエラーがあるかどうかを示すための属性です。エラーがある場合はtrueになります。
const form = useForm()

<Form form={form}>
  <FormField
    control={form.control}
    name="username"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Username</FormLabel>
        <FormControl>
          <Input placeholder="shadcn" {...field} />
        </FormControl>
      </FormItem>
    )}
  />
</Form>

7. FormDescriptionでフィールドの説明を表示します。

const form = useForm()

<Form form={form}>
  <FormField
    control={form.control}
    name="username"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Username</FormLabel>
        <FormControl>
          <Input placeholder="shadcn" {...field} />
        </FormControl>
        <FormDescription>This is your public display name.</FormDescription>
      </FormItem>
    )}
  />
</Form>

8. FormMessageでエラーメッセージを表示します。

FormFieldのエラーメッセージを表示するコンポーネントで、エラーがない場合は表示しません。

const form = useForm()

<Form form={form}>
  <FormField
    control={form.control}
    name="username"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Username</FormLabel>
        <FormControl>
          <Input placeholder="shadcn" {...field} />
        </FormControl>
        <FormDescription>This is your public display name.</FormDescription>
        <FormMessage />
      </FormItem>
    )}
  />
</Form>
TohdaTohda

実装サンプル

この状態だと、onSubmitは実行されますが、バリデーションはありません。

"use client";

import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { useForm } from "react-hook-form";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";

export default function BasicForm() {
  const form = useForm();

  const onSubmit = (values) => {
    console.log(values.username);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>名前</FormLabel>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              <FormDescription>アプリの表示名です。</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button className="mt-4">送信</Button>
      </form>
    </Form>
  );
}
TohdaTohda

zodzodResolver をインストールして、スキーマによるバリデーションを実装します。

  • useForm の resolver オプションにzodResolverを利用してスキーマを指定します。
  • onSubmit の引数にスキーマから型を生成して与えます。
const formSchema = z.object({
  username: z.string().min(2, {
    message: "名前は2文字以上で入力してください。",
  }),
});

// ...

const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
});

const onSubmit = (values: z.infer<typeof formSchema>) => {
  console.log(values);
};

"use client";

import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { useForm } from "react-hook-form";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const formSchema = z.object({
  username: z.string().min(2, {
    message: "名前は2文字以上で入力してください。",
  }),
});

export default function BasicForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
  });

  const onSubmit = (values: z.infer<typeof formSchema>) => {
    console.log(values);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>名前</FormLabel>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              <FormDescription>
                この名前はアプリ内で公開されます。
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button className="mt-4">送信</Button>
      </form>
    </Form>
  );
}