Open9

お問い合わせフォームの作成記録

ピン留めされたアイテム
masterakmasterak

送信処理や送信ボタン状態管理

SendGrid API を使用しています。
日本の代理店で登録しようとすると、審査が厳しくなって個人利用不可になってしまったので、Twillo SendGridから登録します。
useActionStateへの更新版

  • Next.js - 14.3.0-canary.28
  • react - 18.3.1

送信中と送信成功後に、ボタンを無効化する処理を追加

page.tsx
page.tsx
'use client';

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

export default function App() {
    // フォームエラーの制御
    const methods = useForm<ContactFormType>({
        mode: "onBlur",
        resolver: zodResolver(StringConstaraint)
    })
    // モダールウインドウの制御
    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)}/>
                </FormProvider>
            </div>
        </main>
    </>
    )
}

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

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

export default function Form({ onOk }: Props) {
    const methods = useFormContext<ContactFormType>();
    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
confirm.tsx
'use client';
import { useFormContext } from "react-hook-form";
import { ContactFormType } from './types';
import { SendMessageAction } from "./action";
import { useActionState } from "react";

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

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

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

    const [formState, sendAction, pending] = useActionState(SendMessageAction, {"success": false, "message" : ""});

    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" disabled={pending || formState.success} className={`px-8 py-2 rounded text-white ${formState.success ? "bg-gray-500 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700"} px-8 py-2 mx-auto`}>
                        {pending ? "送信中..." : "送信"}
                    </button>
                    <button type="button" onClick={ onCancel } className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
                        キャンセル
                    </button>
                </div>

                <div className={`flex justify-center mt-4 ${formState.success ? '' : 'text-red-600'}`}>{formState.message}</div>
            </form>

            <div onClick={ onCancel } className="fixed bg-black bg-opacity-50 w-full h-full top-0 left-0 z-10"/>
        </>
    ):(<></>);
};

action.tsx
action.tsx
'use server';

//source sendgrid.env

import { ContactFormType } from './types';
    type State = {
    success?: boolean;
    message?: string;
  };

export async function SendMessageAction(prevState: State,params: ContactFormType) {

  const formData = new FormData()
    Object.entries(params).forEach(([key, value]) => {
    formData.append(key, value)
  })

  const headers = new Headers([
    ['Content-Type', 'application/json'],
    ["Authorization", "Bearer " + process.env.SENDGRID_API_KEY]
  ])

  const requestBody = {
    "personalizations": [
        {
          "to": [
            {
              "email": formData.get("email")
            }
          ]
        }
      ],
    "subject": "お問い合わせを受け付けました。",
    "from": {
        "email": "Your@email.com"
      },
        "content": [
      {
        "type": "text/plain",
        "value": formData.get("name") + "様\r\n\r\n以下の内容でお問い合わせを受け付けました。\r\n------\r\n" + formData.get("message")
      }
    ]
  };

  try {
    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
        method: 'POST',
        headers: headers,
        body: JSON.stringify(requestBody)
      });
    if(response.ok) {
        console.log('Email sent')
        return { 
          success: true,
          message: "お問い合わせを受け付けました。"
      };
    }else {
      console.error(response);
        return { 
          success: false,
          message: "お問い合わせの送信に失敗しました。"
      };
    }
    } catch (error) {
    console.error(error);
      return { 
        success: false,
        message: "サーバーが応答しません。しばらくしてから再度お試しください。"
      };
  }
}
masterakmasterak

お問い合わせフォームの作成!

TODO

  • React Hook Form を使ってお問い合せフォームの雛形を作成
  • 入力にエラーが発生している場合、送信ボタンを押せないようにする モーダルウインドウで発生するみたい...
  • RHFから、データを受取してモーダルウインドウに表示する
  • FIX: モーダルウインドウのCSSがおかしいので要修正
  • 送信処理(旧データのaction.tsを再利用)
  • よく使う型情報は、types.ts を作ってそちらに集約

今後の改修
確認ボタンを押すと送信ボタンと、入力内容の確認をする表示がでるモーダルウインドウを実装する

  1. モーダルウインドウの表示
  2. RHFから、データを受取してモーダルウインドウに表示する
  3. 送信ボタン/閉じるボタン/画面外を押すと閉じる を実装する
  4. 送信処理(旧データのaction.tsを再利用)
  5. action.ts にデータを送信すると送受信結果が返ってくるので、結果をモーダルウインドウに表示する。
  6. 送信用のアクションを呼び出す方法について...
  7. 送信ボタン:送信中 -> 送信結果:送信しました, 送信エラー -> OK:完了 NG:閉じる

参考
SendGridのAPI KEY発行手続きと、クイックスタートガイド

masterakmasterak

お問い合わせフォームの雛形

Next.js + Tailwind で作成
フォームの作成には、React Hook Form

Form.tsx
'use client';

import { StringConstaraint } from './schema'
import { useForm } from "react-hook-form"
import { zodResolver } from '@hookform/resolvers/zod';

interface ContactFormType {
  name: string;
  email: string;
  text: string;
}

export default function Form() {

  const { 
    register,
    handleSubmit,
    formState: {errors}
    } = useForm<ContactFormType>({
      mode: "onBlur",
      resolver: zodResolver(StringConstaraint)
    });  

  const onSubmit = (data :ContactFormType) => {
    console.log(data);
  };

  return(
    <div className="max-w-xl">
      <form onSubmit={handleSubmit(onSubmit)} 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("text")} 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.text?.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>
        <div className="flex items-center justify-center text-red-500 text-xs italic mt-2"></div>
      </form>
    </div>
  );
}
masterakmasterak

スキーマの作成

スキーマ宣言ライブラリには、Zod を使用

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

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

button type ="sbummit"を押下した際に、モーダルウインドウを表示するように修正

Form.tsx
'use client';

import { StringConstaraint } from './schema'
import { useForm } from "react-hook-form"
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';

type ContactFormType = {
  name: string;
  email: string;
  text: string;
}

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

const Modal = (props: ModalProps) => {
  return props.open ? (
    <>
      <div className="bg-white  top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-48 p-5 flex flex-col items-start absolute z-20">
        <h1 className="text-xl font-bold mb-5">Title</h1>
        <p className="text-lg mb-5">Dialog Message.</p>
        <div className="flex mt-auto w-full">
          
          <button onClick={() => props.onOk()} className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
            送信
          </button>

          <button onClick={() => props.onCancel()} className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
            キャンセル
          </button>
        </div>
      </div>

      <div
        className="fixed bg-black bg-opacity-50 w-full h-full top-o left-0 z-10"
        onClick={() => props.onCancel()}
      />
    </>
  ) : (<></>);
};

export default function Form() {
  
  const [isOpen, setIsOpen] = useState(false);
  
  const { 
    register,
    handleSubmit,
    formState: {errors}
    } = useForm<ContactFormType>({
      mode: "onBlur",
      resolver: zodResolver(StringConstaraint)
    });  

  const onSubmit = (data :ContactFormType) => {
    console.log(data);
    setIsOpen(true);
  };

  return(
    <>
      <Modal
        open={isOpen}
        onCancel={() => setIsOpen(false)}
        onOk={() => setIsOpen(false)}
      />

      <div className="max-w-xl">
        <form onSubmit={handleSubmit(onSubmit)} 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("text")} 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.text?.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>
          <div className="flex items-center justify-center text-red-500 text-xs italic mt-2"></div>
        </form>
      </div>  
    </>
  );
}
masterakmasterak

form のデータをuseStateに載せて、モーダルウインドウへの表示。

form.tsx
'use client';

import { StringConstaraint } from './schema'
import { useForm } from "react-hook-form"
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';

type ContactFormType = {
  name: string;
  email: string;
  text: string;
}

type ModalProps = {
  open: boolean;
+ propForm: ContactFormType;
  onCancel: () => void;
  onOk: () => void;
};

const Modal = (props: ModalProps) => {
  return props.open ? (
    <>
      <div 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">{props.propForm?.name}</p>
+      <label className="text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
+      <p className="text-lg mb-5 w-full overflow-hidden">{props.propForm?.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">{props.propForm?.text}</p>

        <div className="flex mt-auto w-full">
          
          <button onClick={() => props.onOk()} className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
            送信
          </button>

          <button onClick={() => props.onCancel()} className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
            キャンセル
          </button>
        </div>
      </div>

      {/*モーダルウインドウ以外をクリックした場合の処理*/}
      <div onClick={() => props.onCancel()} className="fixed bg-black bg-opacity-50 w-full h-full top-0 left-0 z-10"/>
      
    </>
  ) : (<></>);
};

export default function Form() {

+ const [formProps, setFormProps] = useState<ContactFormType>({name: '', email: '', text: ''});
  const [isOpen, setIsOpen] = useState(false);
  
  const { 
    register,
    handleSubmit,
    formState: {errors}
    } = useForm<ContactFormType>({
      mode: "onBlur",
      resolver: zodResolver(StringConstaraint)
    });  

  const onSubmit = (formProps :ContactFormType) => {
    console.log(formProps);
+   setFormProps(formProps);
    setIsOpen(true);
  };

  return(
    <>
      <Modal
        open={isOpen}
+       propForm={formProps}
        onCancel={() => setIsOpen(false)}
        onOk={() => setIsOpen(false)}
      />

      <div className="max-w-xl">
        <form onSubmit={handleSubmit(onSubmit)} 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("text")} 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.text?.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>
          <div className="flex items-center justify-center text-red-500 text-xs italic mt-2"></div>
        </form>
      </div>  
    </>
  );
}
masterakmasterak

コンポーネント毎に分割する
FormProviderを使って、複数コンポーネントでformデータの共有

Schemaは変更なし

page.tsx
'use client';

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

type ContactFormType = {
    name: string;
    email: string;
    text: string;
}

export default function App() {
    // フォームエラーの制御
    const methods = useForm<ContactFormType>({
        mode: "onBlur",
        resolver: zodResolver(StringConstaraint)
    })
    // モダールウインドウの制御
    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 className="bg-white shadow-md rounded p-8">
                        <Form onOk={() => setIsOpen(true)}/>
                        <Confirm open={isOpen} onCancel={() => setIsOpen(false)} onOk={() => (setIsOpen(false))}/>
                    </form>
                </FormProvider>
            </div>
        </main>
    </>
    )
}
form.tsx
'use client';
import { useFormContext } from "react-hook-form";

type ContactFormType = {
    name: string;
    email: string;
    text: string;
}

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

export default function Form({ onOk }: Props) {
    const methods = useFormContext<ContactFormType>();

    const { 
        register,
        formState: {errors}
        } = methods;  

    return(
        <>
        <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("text")} 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.text?.message}</p>
        </div>

        <div className="flex items-center justify-center">
        <button type="button" onClick={onOk} 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>
        <div className="flex items-center justify-center text-red-500 text-xs italic mt-2"></div>
        </>
    )
}
confirm.tsx
'use client';
import { useFormContext } from "react-hook-form";
import {SendMessageAction } from "./action";


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

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

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

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

    const SendAction = (data :ContactFormType) => {
        SendMessageAction(data)
    };

    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"/>
        </>
    ):(<></>);
};
masterakmasterak

モーダル表示を作り変えた...

FIX
・ReactHookFormをのFormProviderを使ったFormをAction属性で飛ばすと、中身が空っぽのデータが送られるんだけど、これどうやって解決すればいいのかわからない。
-> <Form> のActionが使えないと、useFormStatus Pendingが使えない...
-> 現状は、button subimit を押下すると。const action が実行されて、送信中は追加送信できないように変更。
-> 【TODO:送信中の実装待ち】
・ちょうど、useFormState から useActionStateなるものに変更されたみたい
-> 色んな人の記事を漁るに、useFormStateにpennding機能を追加したものみたい。
-> https://react.dev/reference/react/useActionState リファレンスはまだ、書き換え途中?
-> Next.jsで試してみたけど、なにやらエラーが発生する...
-> TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_2__.useActionState) is not a function or its return value is not iterable
-> Next.js canary版にしたら出来る模様
-> useActionStateを使うと、onSubmit属性でも、pendingのやり取りができる。便利

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

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

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

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

    const [formState, sendAction, pending] = useActionState(SendMessageAction, {"success": false, "message" : ""});

    const action = (params :ContactFormType) => {
        formState.success = true;
        const formData = new FormData()
        Object.entries(params).forEach(([key, value]) => {
            formData.append(key, value)
        })
        sendAction(formData);
    };

    return open ?(
        <>
            <form onSubmit={handleSubmit(action)} 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" disabled={pending || formState.success} className={`px-8 py-2 rounded text-white ${formState.success ? "bg-gray-500 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700"} px-8 py-2 mx-auto`}>
                    {pending ? "送信中..." : "送信"}
                    </button>
                    <button type="button" onClick={ onCancel } className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
                        キャンセル
                    </button>
                </div>

                <div className={`flex justify-center mt-4 ${formState.success ? '' : 'text-red-600'}`}>{formState.message}</div>
            </form>
            {/*モーダルウインドウ以外をクリックした場合の処理*/}
            <div onClick={ onCancel } className="fixed bg-black bg-opacity-50 w-full h-full top-0 left-0 z-10"/>
        </>
    ):(<></>);
};

action.ts はSendGrid Next.js App RouterとServer Actionsを使ってメールを送るフォームを作成するのuse server を使用。

masterakmasterak

$ app/form/ 下に新規作成

types.ts
export type ContactFormType = {
    name: string;
    email: string;
    message: string;
  }