今回作るもの


ユーザが入力した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行目にかけて表示を制御しています。

