PR

Next.jsを使ったインタラクティブなバリデーションの実装方法

バリデーション成功 Web開発
記事内に広告が含まれています。
スポンサーリンク
スポンサーリンク
スポンサーリンク

今回作るもの

 ユーザが入力したLogin IDを登録できるか否かを判定するインタラクティブなフォームバリデーションです。

 もちろん、フォームが送信された後にバリデーションするという方法もあるのですが、ユーザビリティを考慮すると、入力段階で随時バリデーションしてくれるフォームの方が使いやすそうです。
散々入力させられた挙句、いざフォームを送信したらエラーが出て一から入力をやり直す羽目になったら最悪ですよね…

 以前、どこかのサイトでこのように随時判定してくれるフォームを見かけたのをきっかけに、「カッコイイ! 自分でも作ってみたい!」という安直な動機で自作してみることにしました。

 今回は、バックエンド連携(実際にデータベース照会を行う部分)は省略し、フロントエンドのみで完結させようと思います。

スポンサーリンク

実践編

下準備

Next.js プロジェクトの新規作成

npx create-next-app
create-next-app@14.2.6
Ok to proceed? (y) y
√ What is your project named? ... form
√ Would you like to use TypeScript?  Yes
√ Would you like to use ESLint?  Yes
√ Would you like to use Tailwind CSS?  Yes
√ Would you like to use `src/` directory? Yes
√ Would you like to use App Router? (recommended)  Yes
√ Would you like to customize the default import alias (@/*)?  Yes        
√ What import alias would you like configured? ... @/*

 上記の要領でNext.jsのプロジェクトを新しく作ります。

 一通りインストールが終了したら、プロジェクトディレクトリに移動し、npm run devで開発モードを立ち上げ、ページが正しく表示されることを確認してください。デフォルトの場合、URLは「http://localhost:3000/」になると思います。

パッケージの追加

 今回は、shadcn/uiを使ってフォームを装飾していきたいので、別途shadcn/uiを追加インストールしたいと思います。

PS C:\Users\XXX\Desktop\WebApp\form> npx shadcn-ui@latest init
√ Which style would you like to use? » Default
√ Which color would you like to use as base color? » Slate
√ Would you like to use CSS variables for colors? ... no / yes

 また、shadcn/uiから提供されているコンポーネントのうち、今回はFormとInputを使うのでこちらも併せて追加しておきます。

PS C:\Users\XXX\Desktop\WebApp\form> npx shadcn-ui@latest add form
PS C:\Users\XXX\Desktop\WebApp\form> npx shadcn-ui@latest add input
PS C:\Users\XXX\Desktop\WebApp\form> npm install react-icons --save

 「npx shadcn-ui@latest add form」を実行すると、Formコンポーネントに加え、Buttonコンポーネント及びLabelコンポーネントは自動で追加してくれたのですが、Inputコンポーネントだけはなぜか自動で追加してくれなかったので、追加でnpxしました。

 また、バリデーション結果をアイコンでも表示したいので、react-iconsも追加します。

フォームの作成

 ひとまず公式ドキュメントからソースコードをそのまま引用させていただきます。

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
})

export function ProfileForm() {
  // ...

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <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>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

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

このままVS Codeに貼り付けたところエラーが出てしまったので、エラーが出ない程度に改変します。

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
})

export function ProfileForm() {
  
    const form = useForm({
        resolver: zodResolver(formSchema),
    })

  return (
    <Form {...form}>
      <form className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Login ID</FormLabel>
              <FormControl>
                <Input placeholder="user01" {...field} />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

このファイルを「form.tsx」として、src > app > components ディレクトリに保存します。

page.tsxの差し替え

 デフォルトでは、トップページがNext.jsのテンプレートになっているので、src > app > page.tsxを編集して内容を差し替えます。

 私は以下のようにしました。

import Image from "next/image";
import { ProfileForm } from "@/app/components/form";

export default function Home() {
  return (
    <>
      <div className="flex flex-col items-center justify-center min-h-screen py-2">
        <ProfileForm />
      </div>
    </>
  );
}

 再び http://localhost:3000/ にアクセスすると、以下の画面に切り替わっていると思います。

バリデーションの実装

 いよいよインタラクティブなバリデーションの実装をしてみたいと思います。

再び src > app > components の form.tsx を編集します。最終的に完成したのが以下のコードです。

"use client"

import { useState, useEffect } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import {
    Form,
    FormControl,
    FormDescription,
    FormField,
    FormItem,
    FormLabel,
    FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { IconContext } from 'react-icons'
import { RxCrossCircled, RxCheckCircled } from "react-icons/rx"


export function ProfileForm() {

    const form = useForm({});

    const [NGWords] = useState(["admin", "root", "superuser"]);
    const [isNGWord, setIsNGWord] = useState(false);

    const [LoginID, setLoginID] = useState("");

    useEffect(() => {
        if (NGWords.includes(LoginID) || !LoginID) {
            setIsNGWord(true);
        } else {
            setIsNGWord(false);
        }
    }, [LoginID, NGWords]);

    return (
        <Form {...form}>
            <form className="space-y-8">
                <FormField
                    control={form.control}
                    name="username"
                    render={({ field }) => (
                        <FormItem>
                            <FormLabel>Login ID</FormLabel>
                            <FormControl>
                                <Input placeholder="user01" {...field}
                                    value={LoginID}
                                    onChange={(e) => setLoginID(e.target.value)} />
                            </FormControl>
                            <FormDescription>
                                {isNGWord ? (
                                    <IconContext.Provider value={{ color: 'rgb(220, 38, 38)', size: '20px' }}>
                                        <div style={{ display: 'flex', alignItems: 'center' }}>
                                            <RxCrossCircled style={{ marginRight: '8px' }} />
                                            <span><b className="text-red-600">{LoginID} is not available.</b></span>
                                        </div>
                                    </IconContext.Provider>
                                ) : (
                                    <IconContext.Provider value={{ color: 'rgb(22, 163, 74)', size: '20px' }}>
                                        <div style={{ display: 'flex', alignItems: 'center' }}>
                                            <RxCheckCircled style={{ marginRight: '8px' }} />
                                            <span><b className="text-green-600">{LoginID} is available.</b></span>
                                        </div>
                                    </IconContext.Provider>
                                )}
                            </FormDescription>
                            <FormMessage />
                        </FormItem>
                    )}
                />
                <Button type="submit">Submit</Button>
            </form>
        </Form>
    )
}

ソースコードの解説

 27行目:”admin”, “root”, “superuser”の3つを登録できないLogin IDとして、定義しました。
 32行目からのuseEffect文で、Inputフォームの値入力・変更をトリガーとして、随時 OK・NGの判定
 を行っています。
 判定結果は、JSXの条件付きレンダーで55行目から69行目にかけて表示を制御しています。

スポンサーリンク
スポンサーリンク
Web開発
シェアする
しばをフォローする
タイトルとURLをコピーしました