🗨️

お問い合わせフォームの確認画面をモーダルウィンドウで表示する。【ReactHookForm】

2024/04/26に公開

React Hook Formを使ったお問い合わせフォームと、入力内容の確認画面をモーダルウィンドウで表示する例を紹介します。
React Hook Formとモーダルウィンドウに関する情報は。インターネットにいくつか転がっていますが、確認画面をモーダルウィンドウにして表示する組み合わせが思ったより少ないので作成しました。

フレームワーク・ライブラリ

  • Next.js 14.2.0
  • React Hook Form 7.51.3
  • Zod 3.22.4
  • @hookform/resolvers 3.3.4

DEMO

form_demo

導入方法

npm install react-hook-form @hookform/resolvers zod

ソースコード

page.tsx

FormProvider:便利機能。

FormProviderを使って<form>タグ とフォーム関連の処理をラップすることで、propsを渡すことなくフォームの値をやり取りできます。

モーダルウインドウの表示制御にはuseStateを使って制御しています。
Zod, useForm の使い方を紹介している記事は、多いので割愛します。

page.tsx
'use client';

import { useForm, FormProvider } from "react-hook-form"
import { Constaraint } from './schema'
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import Form from './form';
import Confirm from './confirm';

type formType = {
    name: string;
    email: string;
    message: string;
}

export default function App() {
    // フォームエラーの制御
    const methods = useForm<formType>({
        mode: "onBlur",
        resolver: zodResolver(Constaraint)
    })
    // モダールウィンドウの制御
    const [isOpen, setIsOpen] = useState(false);

    return (
    <>
        <main className="min-h-screen bg-gray-200 w-full p-8">
            <div className="max-w-xl">
                <FormProvider {...methods}>
                        <Form onOk={() => setIsOpen(true)}/>
                        <Confirm open={isOpen} onCancel={() => setIsOpen(false)} onOk={() => (setIsOpen(false))}/>
                </FormProvider>
            </div>
        </main>
    </>
    )
}
form.tsx

ココでは、useFormContextを使ってフォームの値を取得しています。useFormContextを使うことで、propsを渡すことなくコンポーネント間でフォームの値をやり取りできます。

handleSubmitを使って、フォームの値を引数にしてonOk関数を実行しています。ボタンが押下されると、page.tsxにあるonOk={() => setIsOpen(true)}が実行され、Confirmコンポーネントが表示されます。

form.tsx
'use client';
import { useFormContext } from "react-hook-form";

type formType = {
    name: string;
    email: string;
    message: string;
}

type Props = {
    onOk: () => void;
}

export default function Form({ onOk }: Props) {
    const methods = useFormContext<formType>();
    const { 
        register,
        handleSubmit,
        formState: {errors}
        } = methods;  

    return(
        <>
            <form onSubmit={ handleSubmit(onOk) } className="bg-white shadow-md rounded p-8">
                <h3 className="block text-gray-700 text-lg font-bold mb-2">お問い合わせ</h3>

                <div className="mb-4">
                    <label htmlFor="名前" className="block text-gray-700 text-sm font-bold mb-2">お名前</label>
                    <input {...register("name")} type="text" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
                    <p className="text-red-500 text-xs italic mb-4">{ errors.name?.message }</p>

                    <label htmlFor="メール" className="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
                    <input {...register("email")} type="email" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
                    <p className="text-red-500 text-xs italic mb-4">{ errors.email?.message }</p>

                    <label htmlFor="お問い合わせ" className="block text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
                    <textarea {...register("message")} rows={8} className="resize-none shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
                    <p className="text-red-500 text-xs italic mb-4">{ errors.message?.message }</p>
                </div>

                <div className="flex items-center justify-center">
                    <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-8 rounded focus:outline-none focus:shadow-outline">
                    確認
                    </button>     
                </div>
            </form>
        </>
    )
}
confirm.tsx

モーダルウィンドウによる確認画面を表示しています。
useFormContextを使ってフォームの値を取得して、表示しています。

送信ボタンを押した場合、handleSubmitを使って、フォームの値を引数にしてSendAction関数を実行しています。
SendAction関数内に、送信処理を記述します。

キャンセルボタン、画面外をクリックした場合、onCancelを実行してモーダルウィンドウを閉じます。

confirm.tsx
'use client';
import { useFormContext } from "react-hook-form";
import {SendMessageAction } from "./action";


type formType = {
    name: string;
    email: string;
    message: string;
}

type Props = {
    open: boolean;
    onOk: () => void;
    onCancel: () => void;
  }

export default function Confirm({ open, onOk, onCancel }:Props) {

    const methods = useFormContext<formType>();
    const { 
        getValues, 
        handleSubmit,
    } = methods;

    const SendAction = (data :formType) => {
        //ここから送信処理を実装する。
        otherAction(data);
        onOK();
    };

    return open ?(
        <>
            <form onSubmit={handleSubmit(SendAction)} className="bg-white  top-1/4 left-1/2 transform -translate-x-1/2 w-1/4 p-5 flex flex-col items-start absolute z-20">
                <h2 className="text-xl font-bold mb-5">送信内容の確認</h2>
                <label className="text-gray-700 text-sm font-bold mb-2">お名前</label>
                <p className="text-lg mb-5">{ getValues('name') }</p>
                <label className="text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
                <p className="text-lg mb-5 w-full overflow-hidden">{ getValues('email') }</p>
                <label className="text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
                <p className="text-lg mb-5 break-all max-h-60 overflow-y-auto">{ getValues('message') }</p>

                <div className="flex mt-auto w-full">
                    <button type="submit" className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
                        送信
                    </button>
                    <button type="button" onClick={ onCancel } className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
                        キャンセル
                    </button>
                </div>
            </form>
            {/*モーダルウィンドウ以外をクリックした場合の処理*/}
            <div onClick={ onCancel } className="fixed bg-black bg-opacity-50 w-full h-full top-0 left-0 z-10"/>
        </>
    ):(<></>);
};
schema.tsx

Zodを使って、フォームのバリデーションルールを定義しています。

schema.tsx
import { z } from 'zod';

export const Constaraint = z.object({
  name: z.string()
    .min(1, { message: '入力が必須の項目です' })
    .max(20, { message: '20文字以内で入力してください' })
    .regex(/^[^<>"'\\/]*$/, { message: '特殊文字は使用できません' }),
  email: z.string()
    .min(1, { message: '入力が必須の項目です' })
    .max(255, { message: '255文字以内で入力してください' })
    .email({ message: 'メールアドレスの形式で入力してください' }),
  message: z.string()
    .min(10, { message: '入力が必須の項目です' })
    .max(1000, { message: '1000文字以内で入力してください' })
    .regex(/^[^<>"'\\/]*$/, { message: '特殊文字は使用できません' }),
});

おわりに

React Hook Formを使ったお問い合わせフォームの作成と、確認画面をモーダルウインドウで表示する例を紹介しました。
お問い合わせフォームに、わざわざ確認画面を表示させるなど冗長な感じもしますが、ユーザーの操作ミスを防ぐなどの効果があります。
何かの参考になれば幸いです。

GitHubで編集を提案

Discussion