Inertiaでのバリデーション Inertia.js入門 #8

Inertia入門#8 バリデーション Laravel

この記事では、Inertia.jsを使った際のバリデーションエラーの表示について解説します。

前回の記事ではInertia.jsでのフォームとリダイレクトについて解説しました。今回はその続きですのでぜひ前回の記事と合わせてお読みください。

Laraveler
Laraveler

Inertiaでフォームの入力エラーを表示したい場合、どうしたらいいんでしょう?

シップ
シップ

Inertiaにはバリデーションエラーをキャッチして表示する機能がしっかりついていますので解説しますね!

この記事で使用しているバージョン
  • Laravel 8
  • Vue 3
  • Inertia.js 0.8.4

バリデーションとは?

バリデーションとは、入力された値が正しいか、基準に合っているかを確認することです。さまざ生ルールで値をチェックして、そのチェックに合格しなかった場合は再度入力を促すなどして、必要な値がしっかりと登録され、逆に不正な値は登録されないようにします。

バリデーションの方法

サーバーサイドバリデーション

送られてきた値をサーバー側でチェックすることです。LaravelやRuby on Rails, Djangoなどフレームワークに備わっているバリデーション機能を利用する場合が多いです。

クライアントサイドバリデーション

ブラウザで入力値をチェックすることです。input要素にrequired属性をつけて入力必須であることを知らせたり、email属性を指定してメールアドレスとして妥当かどうかブラウザで送信前にチェックが行えます。

JavaScriptを使ってもっと細かいバリデーションをクライアントサイドで行うこともありますが、バリデーションを通らず直接データが送信される可能性も考慮して、サーバーサイドでのバリデーションと併用して行います。

バリデーションはクライアントサイドで行うだけでは不十分です。サーバーサイドで必ず行いましょう。

サンプル

お問い合わせフォームでメールアドレスとお問い合わせ内容を入力するフォームがあり、送信ボタンをクリックすると、エラーがなければ確認ページへ移動し、エラーがある場合は移動せずエラーを表示するというサンプルを作成してみます。

完全なソースコードはGitHubをご覧ください。

web.phpのルーティング

/Contactはお問い合わせフォームの表示、/Contact/confirmは確認画面のルート、/Contact/sendは送信完了画面のルートとなります。

use App\Http\Controllers\ContactController;
//略
Route::get('/Contact',[App\Http\Controllers\ContactController::class, 'index'])->name('contact.index');
Route::post('/Contact/confirm',[App\Http\Controllers\ContactController::class, 'confirm'])->name('contact.confirm');
Route::post('/Contact/send',[App\Http\Controllers\ContactController::class, 'send'])->name('contact.send');

Laravelのコントローラー

ContactControllerクラスを作成します。

php artisan make:controller ContactController

コントローラーファイルのひな型として「app/Http/Controllers/ContactController.php」が作成されるのでこのファイルを編集していきます。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;

class ContactController extends Controller
{
    //フォーム表示
    public function index () 
    {
        return Inertia::render('Contact/Index');
    }

    //確認表示
    public function confirm (Request $request) 
    {
        $request->validate([
            'mail' => ['required', 'email'],
            'message' => ['required', 'max:5000'],
        ]);
        return Inertia::render('Contact/Confirm',['mail'  => $request->mail, 'message'  => $request->message]);
    }

    //送信
    public function send (Request $request) 
    {
        $request->validate([
            'mail' => ['required', 'email'],
            'message' => ['required', 'max:5000'],
        ]);

        //問い合わせ内容を保存したりメールで送信する処理をここに記述

        return Inertia::render('Contact/Complete');
    }
}

Laravelでのバリデーション

        $request->validate([
            'mail' => ['required', 'email'],
            'message' => ['required', 'max:5000'],
        ]);

Laravel側のバリデーションコードは通常と全く同じです。$requestのvalidate()メソッドでリクエストに含まれる値が渡したルールに則っているかをチェックし、エラーがあると元の同じページを表示するレスポンスが返ります。

そのレスポンスにはエラー内容がInertiaによって自動的に含まれます。Vue側でpropsでそれを用いて画面にエラーを表示することができます。

エラー時のJSONレスポンス

{
    "component": "Contact/Index",
    "props": {
        "errorBags": {
            "default": {
                "mail": [
                    "The mail field is required."
                ],
                "message": [
                    "The message field is required."
                ]
            }
        },
        "errors": {
            "mail": "The mail field is required.",
            "message": "The message field is required."
        }
    },
    "url": "/Contact",
    "version": "207fd484b7c2ceeff7800b8c8a11b3b6"
}

Vueコンポーネント

お問い合わせフォームを表示するIndex.vueを作成します。Inertia.jsのフォームヘルパーを使用した例となります。

<template>
    <layout>
        <h2 class="text-lg border-b p-1">
            <inertia-link :href="route('contact.index')" class="text-blue-700 underline">
            お問い合わせ
            </inertia-link>
        </h2>

        <form @submit.prevent="submit">
            <h3 class="border-l-4 p-1">メールアドレスとお問い合わせ内容をご入力ください</h3>
            
            <div v-if="form.hasErrors" class="border border-red-100 p-1 m-1 text-sm text-red-600">
                入力された値をもう一度確認してください。
                <ul class="list-disc list-inside">
                    <li v-for="error in form.errors" >{{error}}</li>
                </ul>
            </div>

            <div class="w-40 border-l-4 text-sm p-1 m-1">メールアドレス</div>
            <div class="w-80">
                <input type=text name=mail v-model="form.mail" class="p-1 m-1 text-sm w-full" v-bind:class="{'border-red-300': form.errors.mail}"/>
            </div>
            <div v-show="form.errors.mail" class="p-1 m-1 text-sm text-red-400">{{ form.errors.mail }}</div>
        
            <div class="w-40 border-l-4 text-sm p-1 m-1">お問い合わせ内容</div>
            <div class="w-80">
                <textarea name=message v-model="form.message" class="p-1 m-1 text-sm w-full h-32" v-bind:class="{'border-red-300': form.errors.message}" />
            </div>
            <div v-show="form.errors.message" class="p-1 m-1 text-sm text-red-400">{{ form.errors.message }}</div>
            
            <button type="submit" class="border bg-green-200 m-1 p-1 text-sm" :disabled="form.processing" v-bind:class="{'cursor-not-allowed': form.processing}">確認画面へ進む</button>
            <button @click="form.reset()" type="reset" class="border bg-gray-200 m-1 p-1 text-sm" :disabled="!form.isDirty" v-bind:class="{'cursor-not-allowed': !form.isDirty}">リセット</button>
            <button v-if="form.processing" @click="form.cancel()" type="button" class="border bg-red-200 m-1 p-1 text-sm">送信中止</button>
        </form>
    </layout>
</template>

<script>
    import Layout from '@/Pages/Layout'

    export default {
        components: {
            Layout,
        },
        data: function() {
            return {
                form: this.$inertia.form("Contact",{
                    mail: null,
                    message: null,
                }),
            }
        },
        mounted: function(){
            document.title = "お問い合わせ";
        },
        methods: {
            submit() {
                this.form.post(route('contact.confirm'),{
                    preserveState: true,
                })
            },
        },
    }
</script>

フォームヘルパーの作成

form: this.$inertia.form("Contact",{
                    mail: null,
                    message: null,
                }),

formをInertia Formとして定義しています。第一引数でユニークキーを渡すとフォームに入力した値などがブラウザの履歴に保存されて、戻るボタンなどで戻った時に入力してあったフォームデータが再現されます。

エラーをまとめて表示

    <div v-if="form.hasErrors" class="border border-red-100 p-1 m-1 text-sm text-red-600">
        入力された値をもう一度確認してください。
        <ul class="list-disc list-inside">
            <li v-for="error in form.errors" >{{error}}</li>
        </ul>
    </div>
  1. div要素をv-if="form.hasErrors"でフォームにエラーがある場合にブロックを表示しています。
  2. li要素をv-for="error in form.errors"でエラーの数だけループして各エラーを表示します。

エラーを個別に表示

    <div class="w-80">
        <input type=text name=mail v-model="form.mail" class="p-1 m-1 text-sm w-full" v-bind:class="{'border-red-300': form.errors.mail}"/>
    </div>
    <div v-show="form.errors.mail" class="p-1 m-1 text-sm text-red-400">{{ form.errors.mail }}</div>
  1. input要素にv-bind:class="{'border-red-300': form.errors.mail}"
    各フィールドのエラーがある場合は赤色の枠をつけるクラスをバインドしています。
  2. div要素をv-show="form.errors.mail"で、フィールド別にエラーがあったか判定し、エラーがあった場合は{{ form.errors.mail }}でそのフィールドのエラーを表示しています。

エラーバッグ

Inertia.visit()でデータ送信した際に、複数のフォームが同一ページにある場合、どのフォームのエラーなのかを区別するためにオプションのerrorBag属性にエラーバッグ名を入れておくことができます。

エラーバッグの使い方は以前の記事を参考にしてください。

Inertiaフォームヘルパーを使って送信する場合は、どのフォームから送信されたかをInertiaで識別しているためエラーバッグ名をつける必要はありません。

表示例
Inertia Validation

サンプルソース

入力内容の確認Vueコンポーネント

<template>
    <layout>
        <h2 class="text-lg border-b p-1">
            <inertia-link :href="route('contact.index')" class="text-blue-700 underline">
            お問い合わせ
            </inertia-link>
        </h2>

        <form @submit.prevent="submit">
            <h3 class="border-l-4 p-1">ご入力いただいた内容をご確認ください</h3>
            <div class="w-40 border-l-4 text-sm p-1 m-1">メールアドレス</div>
            <div class="w-40">
                <div class="p-1 m-1 text-sm w-full">
                    {{ form.mail }}
                </div>
                <input type=hidden name=mail v-model="form.mail" size=25 />
            </div>
            <div class="w-40 border-l-4 text-sm p-1 m-1">お問い合わせ内容</div>
            <div class="w-40">
                <pre class="p-1 m-1 text-sm w-full">{{ form.message }}</pre>
                <input type=hidden name=message v-model="form.message" />
            </div>
            <button type="submit" class="border bg-green-200 m-1 p-1 text-sm" :disabled="form.processing" v-bind:class="{'cursor-not-allowed': form.processing}">お問い合わせを送信</button>
            <button @click="back" type="button" class="border bg-gray-200 m-1 p-1 text-sm">入力画面へ戻る</button>
            <button v-if="form.processing" @click="form.cancel()" type="button" class="border bg-red-200 m-1 p-1 text-sm">送信中止</button>
        </form>
    </layout>
</template>

<script>
    import Layout from '@/Pages/Layout'

    export default {
        components: {
            Layout,
        },
        data: function() {
            return {
                form: this.$inertia.form({
                    mail: this.mail,
                    message: this.message,
                }),
            }
        },
        props:{
            mail: {
                type: String,
            },
            message: {
                type: String,
            }
        },
        mounted: function(){
            document.title = "お問い合わせ内容の確認";
        },
        methods: {
            submit() {
                this.form.post(route('contact.send'))
            },
            back(){
                window.history.back()
            }
        },
    }
</script>

完了画面 Vueコンポーネント

<template>
    <layout>
        <h2 class="text-lg border-b p-1">
            <inertia-link :href="route('contact.index')" class="text-blue-700 underline">
            お問い合わせ
            </inertia-link>
        </h2>

        <h3 class="border-l-4 p-1">お問い合わせが送信されました</h3>
    </layout>
</template>

<script>
    import Layout from '@/Pages/Layout'

    export default {
        components: {
            Layout,
        },
        mounted: function(){
            document.title = "お問い合わせ完了";
        }
    }
</script>
今回のまとめ
  • バリデーションにはクライアントサイドとサーバーサイドで行う方法がある
  • Laravelでのバリデーションをそのまま使用できる
  • フォームヘルパーを使用する場合はerrorsオブジェクトでエラーを取得できる

次回は特定のデータを毎回のInertiaレスポンスに含める方法について取り上げます。

Inertia.js入門記事一覧
Inertia.jsでシンプルにSPAを構築する Inertia入門#1
Inertia.jsを使うとLaravelやRubyonRailsなどのフレームワーク上でAjax用のAPIやコントローラーを作成しなくても、通常のビューを使う要領でSPA(シングルページアプリケーション)が構築できます。
Laravel+Inertia.jsのインストールとHello World Inertia入門#2
この記事ではLaravelにInertia.jsをインストールしてHelloWorldを出力する方法までを解説します。
Inertia.jsのルーティングとレスポンス作成 Inertia入門#3
Laravel8でjetstreamを入れるとLimewireかInertiaかを選べます。この記事ではInertiaを使ってルーティングやInertiaレスポンスの返し方を解説します。
Inertia.jsでページレイアウト Inertia入門#4
この記事では、ページレイアウトについて解説します。親レイアウトに子ページを組み込むことで、ヘッダーやフッターを共通化できます。
Inertia linkの使い方 Inertia.js入門 #5
この記事では、Inertia linkについて解説します。
InertiaでXHRリクエスト Inertia.js入門 #6
この記事では、Inertia.jsで非同期通信を行うInertia.visit()について解説します。
Inertia.jsでのフォームとリダイレクト Inertia.js入門 #7
この記事では、Inertia.jsでのフォーム送信とリダイレクトについて解説します。
Inertiaでのバリデーション Inertia.js入門 #8
この記事では、Inertia.jsを使った際のバリデーションエラーの表示について解説します。
Inertiaで共通データとフラッシュメッセージを毎回のレスポンスに含める Inertia.js入門 #9
この記事では、Inertiaのレスポンスに毎回同じデータを含める方法やフラッシュメッセージの使い方について解説します。

Comments

タイトルとURLをコピーしました