wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

frontendはformikとyupで、backendはozzo-validationを使用してバリデーションを実装する

LPサイトのお問い合わせフォームのバリデーションを作成する時に使ってみた組み合わせを紹介

PR

github.com

frontendのバリデーションを実装

以下を使用してバリデーションを実装

yupでは、バリデーションを以下のように実装できる。

import * as yup from "yup";


const schema = yup.object().shape({
    name: yup.string().required("名前の入力は必須です").max(100, "名前は最大100文字までです"),
    email: yup.string().required("Emailの入力は必須です").email("メールアドレスの形式になっていません"),
});


const request =  async() => {
  const value = {
    name: "",
    email: "test@gmail.com"
  }
  try {
     await schema.validate(value);
  } catch(e) {
     console.log(err.errors); // ["名前の入力は必須です"]
  }
}

ただのバリデーションだったらyupでも簡単に実装可能だが、
フォームに以下のようなエラー表示の実装をしようと思うとyupだけだとerrorをstateで管理しないと実装できない。

f:id:wheatandcat:20220202223904p:plain

そこでフォームの値の管理とバリデーションを同時に行うためにformikを使用した。

formik.org

formikはReact Hooksを使用して以下のように実装できる

import { useFormik } from "formik";
import * as yup from "yup";
import "react";

type State = {
    name: string;
    email: string;
};

const initialState = (): State => {
    return {
        name: "",
        email: "",
    };
};


const schema = yup.object().shape({
    name: yup.string().required("名前の入力は必須です").max(100, "名前は最大100文字までです"),
    email: yup.string().required("Emailの入力は必須です").email("メールアドレスの形式になっていません"),
});


const Form = () => {
    const formik = useFormik<State>({
        initialValues: initialState(),
        validationSchema: schema,
        onSubmit: (values) => {
            console.log(values)
        },
    });

    return (
         <form onSubmit={formik.handleSubmit}>
           <input
             type="text"
             id="name"
             name="name"
             onChange={formik.handleChange}
            />
           {formik.touched.name && formik.errors.name ? (
             <div>{formik.errors.name}</div>
           ) : null}
           <input
             type="text"
             id="email"
             name="email"
             onChange={formik.handleChange}
            />
           {formik.touched.email && formik.errors.email ? (
             <div>{formik.errors.email}</div>
           ) : null}
           <button type="submit">送信</button>
         </form>         
    )
}

上記でフォームの値管理とバリデーションが行えた。あとはcssを整えて以下のように実装

components/organisms/Contact/Form.tsx

import { useFormik } from "formik";
import { useRouter } from "next/router";
import { memo, useState, useCallback } from "react";
import * as yup from "yup";

type State = {
    name: string;
    email: string;
    body: string;
};

const initialState = (): State => {
    return {
        name: "",
        email: "",
        body: "",
    };
};

type Request = {
    body: string;
    name: string;
    email: string;
    env: string;
    userID: string;
    device: string;
    category: string;
};

const schema = yup.object().shape({
    name: yup.string().required("名前の入力は必須です").max(100, "名前は最大100文字までです"),
    email: yup.string().required("Emailの入力は必須です").email("メールアドレスの形式になっていません"),
    body: yup.string().required("本文の入力は必須です").max(2000, "本文は最大2000文字までです"),
});

const url = process.env.NEXT_PUBLIC_INQUIRY_API || "";

const Form = () => {
    const router = useRouter();
    const [loading, setLoading] = useState(false);

    const formik = useFormik<State>({
        initialValues: initialState(),
        validationSchema: schema,
        onSubmit: (values) => {
            postInquiry(values);
        },
    });

    const postInquiry = useCallback(
        async (values: State) => {
            const userAgent = navigator.userAgent.toLowerCase();

            setLoading(true);

            const req: Request = {
                body: values.body,
                name: values.name,
                email: values.email,
                userID: "",
                env: "LPサイト",
                device: userAgent,
                category: "LPお問い合わせ",
            };

            const response = await fetch(url, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(req),
            });

            setLoading(false);

            if (!response.ok) {
                alert("送信に失敗しました");
                return;
            }

            router.push(`/thanks?name=${values.name}`);
        },
        [router]
    );

    return (
        <form onSubmit={formik.handleSubmit}>
            <div className="flex z-10 flex-col justify-center items-center py-3 md:py-16 my-3">
                <div className="mb-0 md:mb-6 text-3xl leading-snug text-center">お問い合わせ</div>
                <br />
                <div className="mr-4">
                    <div className="flex items-center my-5">
                        <div className="mr-2 md:mr-5 w-10 text-right">名前</div>
                        <div className="w-80 md:w-96">
                            <input
                                className="py-2 px-3 w-full leading-tight rounded border shadow appearance-none"
                                type="text"
                                id="name"
                                name="name"
                                aria-label="name"
                                onChange={formik.handleChange}
                            />
                            {formik.touched.name && formik.errors.name ? (
                                <div className="text-sm text-red-600">{formik.errors.name}</div>
                            ) : null}
                        </div>
                    </div>
                    <div className="flex items-center my-5">
                        <div className="mr-2 md:mr-5 w-10 text-right">Email</div>
                        <div className="w-80 md:w-96">
                            <input
                                className="py-2 px-3 w-full leading-tight rounded border shadow appearance-none "
                                type="text"
                                id="email"
                                name="email"
                                aria-label="email"
                                onChange={formik.handleChange}
                            />
                            {formik.touched.email && formik.errors.email ? (
                                <div className="text-sm text-red-600">{formik.errors.email}</div>
                            ) : null}
                        </div>
                    </div>
                    <div className="flex my-5">
                        <div className="mt-1 mr-2 md:mr-5 w-10 text-right">本文</div>
                        <div className="w-80 md:w-96">
                            <textarea
                                className="py-2 px-3 w-full leading-tight rounded border shadow appearance-none "
                                rows={8}
                                id="body"
                                name="body"
                                aria-label="body"
                                onChange={formik.handleChange}
                            />
                            {formik.touched.body && formik.errors.body ? (
                                <div className="text-sm text-red-600">{formik.errors.body}</div>
                            ) : null}
                        </div>
                    </div>
                </div>
                <div className="flex justify-center items-center mb-10 md:mb-20 ml-8">
                    <button
                        type="submit"
                        disabled={loading}
                        className="py-2 px-4 w-40 font-bold text-white-300 bg-secondary-600 hover:bg-blue-700 rounded-xl disabled:opacity-50 cursor-pointer"
                    >
                        送信
                    </button>
                </div>
            </div>
        </form>
    );
};

export default memo(Form);

これでfrontend側の実装は完了した。

backnedのバリデーションを実装

backnedのバリデーションはozzo-validationを使用した。

github.com

コードの書き方は上記で紹介したyupと近い感じで以下のようにして実装できる

package postInquiry

import (
    "context"
    "net/http"
    "os"
    "time"

    validation "github.com/go-ozzo/ozzo-validation"
    "github.com/go-ozzo/ozzo-validation/v4/is"
)


type Request struct {
    Body     string `json:"body"`
    Name     string `json:"name"`
    Email    string `json:"email"`
}

func (r Request) Validate() error {
    return validation.ValidateStruct(&r,
        validation.Field(
            &r.Name,
            validation.Required.Error("名前の入力は必須です"),
            validation.RuneLength(1, 100).Error("名前は最大100文字までです"),
        ),
        validation.Field(
            &r.Email,
            validation.Required.Error("Emailの入力は必須です"),
            validation.RuneLength(1, 200).Error("ールアドレスは最大200文字までです"),
            is.Email.Error("メールアドレスを入力して下さい"),
        ),
        validation.Field(
            &r.Body,
            validation.Required.Error("本文の入力は必須です"),
            validation.RuneLength(1, 2000).Error("本文は最大2000文字までです"),
        ),
    )
}

func PostInquiry(w http.ResponseWriter, r *http.Request) {
    var req Request

    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    if err := req.Validate(); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

上記を実装してエラーになるRequestパラメータでアクセスすると以下のエラーになる。

f:id:wheatandcat:20220202225812p:plain

これでbackendのバリデーションの実装も完了した。