Inertia.jsでのフォームとリダイレクト Inertia.js入門 #7

Inertia Form Laravel

この記事では、Inertia.jsでのフォーム送信とリダイレクトについて解説します。

前回の記事ではInertia.visit()について解説しました。今回のInertia FormはInertia.visit()をラップしてフォームに特化したクラスです。ぜひ前回の記事と合わせてお読みください。

Laraveler
Laraveler

Inertiaでフォームを送信するにはどうしたらいいんでしょうか?

シップ
シップ

Inertiaにはフォーム送信に便利なメソッドやプロパティを集めたFormクラスがあります。

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

Inertia Formの使用

データセットとしての使用

まず、formオブジェクトを作成し、Inertia.visit()で送信する際のデータとして使用する場合のサンプルです。

<template>
    <layout>
        <h2 class="text-lg border-b p-1">
            <Link :href="route('bookmark.index')" class="text-blue-700 underline">
            ブックマーク一覧
            </Link>
        </h2>
        <ul class="list-disc list-inside p-2">
            <li v-for="bookmark in bookmarks" class="list-none flex justify-between">
                <a :href="bookmark.url" target=_blank class="text-blue-700" >{{ bookmark.title }}</a>
                <Link as="button" method="delete" :href="route('bookmark.delete',bookmark.id)" preserve-scroll :only="['bookmarks']" class="border border-red-400 m-1 p-1 text-sm text-red-400">削除</Link>
            </li>
        </ul>
        <form @submit.prevent="submit">
            <h3 class="border-l-4 p-1">ブックマーク追加</h3>
            <div class="flex w-full">
                <div class="w-20 text-right bg-gray-200 text-sm p-1 m-1">タイトル</div>
                <div class="w-40">
                    <input type=text name=title v-model="form.title" size=15 class="p-1 m-1 text-sm w-full" />
                </div>
            </div>
            <div class="flex w-full">
                <div class="w-20 text-right bg-gray-200 text-sm p-1 m-1">URL</div>
                <div class="w-40">
                    <input type=text name=url v-model="form.url" size=15 class="p-1 m-1 text-sm w-full"  />
                </div>
            </div>
            <button type="submit" class="border border-gray-400 m-1 p-1 text-sm" :disabled="form.processing">ブックマーク追加</button>
        </form>
    </layout>
</template>

<script>
    import { Link } from '@inertiajs/inertia-vue3'
    import Layout from '@/Pages/Layout'

    export default {
        components: {
            Link,
            Layout,
        },
        data: function() {
            return {
                form: {
                    title: null,
                    url: null,
                },
            }
        },
        props:{
            bookmarks: {
                type: Array,
            },
        },
        computed: {
            bookmarkError() {
                return this.$page.props.errors['addBookmark'] || {}
            },
        },
        methods: {
            submit() {
                this.$inertia.post(route('bookmark.store'), this.form)
            },
        },
    }
</script>

この例ではformをdata()であらかじめ定義しておき、submit()でInertiaリクエストを行うときにformオブジェクトを渡してデータとして送信しています。

フォームヘルパーの利用

では、このサンプルをInertia Formに備わっているメソッドで送信するように修正していきましょう。

        //data()で定義するformをInertia Formで作成したインスタンスに変更
        data: function() {
            return {
                form: this.$inertia.form({
                    title: null,
                    url: null,
                }),
            }
        },


        //送信をInertiaのpostメソッドから、formクラスのpostメソッドを利用するように変更
        methods: {
            submit() {
                this.form.post(route('bookmark.store'))
            },
        },

ここでのポイントは二つです。

//1. Inertia Formのインスタンス作成
this.$inertia.form({フォームのフィールド1: 初期値1, ...})

//2. Inertia Formのpostメソッドでフォームの内容を送信
this.form.post(route('bookmark.store'))

フォーム送信用メソッド

フォーム送信に使えるメソッドは、HTTPメソッドを指定するsubmit()や、各HTTPメソッド用のget(),post(),put(),patch(),delete()が用意されています。

form.submit(method, url, options)
form.get(url, options)
form.post(url, options)
form.put(url, options)
form.patch(url, options)
form.delete(url, options)

使用可能なオプション

使用可能なオプションは、preserveScrollpreserveStateなど、Inertia.visit()で使用できるオプションと一緒です。onSuccess()など各種コールバックも使用できます。

送信前に値を整形

transform()メソッドを使用して、送信前にフォームの値を整形してから送信することができます。

//titleが入力されていない場合は「無題」という名前で送信する
this.form.transform(function(data){
    return {
        title: data.title ? data.title : '無題',
        url: data.url,
    }
}).post(route('bookmark.store'))

processing属性

フォームの送信中はprocessing属性がtrueになります。これを利用すると二重送信を防ぐために送信中は送信ボタンを無効化したりできます。

以下は、送信中は送信ボタンを無効化し、カーソルを禁止マークに変える例です。(クラスはtailwindcssでの例)

<button type="submit" class="border border-gray-400 m-1 p-1 text-sm" :disabled="form.processing" v-bind:class="{'cursor-not-allowed': form.processing}">ブックマーク追加</button>

progress属性

ファイルをアップロード中はprogress属性で進行状況を取得できます。

<progress v-if="form.progress" :value="form.progress.percentage" max="100">
  {{ form.progress.percentage }}%
</progress>

hasErrors属性, errors属性

フォーム送信の結果、バリデーションエラーが発生した場合はhasErrors属性がtrueになり、各種エラーになったフィールドとエラー内容の配列がerrors属性にセットされます。

//urlフィールドにエラーがあった場合に表示する
<div v-show="form.errors.url" class="p-1 m-1 text-sm text-red-400">{{ form.errors.url }}</div>

v-ifで判定するとエラーが出る・・・理由分かる方いたら教えてください。

<div v-if="form.errors.url" class="p-1 m-1 text-sm text-red-400">{{ form.errors.url }}</div>
[Vue warn]: Unhandled error during execution of scheduler flush. This is likely a Vue internals bug. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next
Uncaught (in promise) TypeError: parent is null

(2021/12/28 バージョンアップでエラーが出なくなっていました。おそらくバグだったようです)

フォームのエラー状態を解除するにはclearErrors()メソッドを使います。引数で指定した特定のフィールドのエラーだけを解除することもできます。

// すべてのエラーを解除
this.form.clearErrors()

// 特定のフィールでのエラーを解除
this.form.clearErrors('field', 'anotherfield')

wasSuccessful属性, recentlySuccessful属性

フォームの送信がエラーなく完了するとwasSuccessful属性がtrueにセットされます。recentlySuccessful属性も2秒間だけtrueにセットされます。
これを使ったフォーム送信後のフラッシュメッセージ表示例です。

//フォーム送信完了時に2秒間だけ表示されるメッセージ
<div v-if="form.recentlySuccessful" class="p-1 m-1 bg-green-200">送信が完了しました。</div>

フォームのリセット、変更検知

reset()メソッドでフォームの内容を初期値にリセットできます。引数で指定した特定のフィールドだけを初期値に戻すこともできます。
isDirty属性はフォームの内容が変更された場合にtrueになります。これを利用して変更された場合にのみ有効なリセットボタンを作成するとこのようになります。

//isDirty属性がfalseならdisabled属性が有効(つまりボタンとしては無効)
<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>

送信キャンセル

cancel()メソッドで送信中のフォームの送信を中止することができます。

<button @click="form.cancel()" type="button" class="border bg-red-200 m-1 p-1 text-sm">送信中止</button>

//processing属性を利用して送信中のみ中止ボタンを表示させる
<button v-if="form.processing" @click="form.cancel()" type="button" class="border bg-red-200 m-1 p-1 text-sm">送信中止</button>

リダイレクト

フォーム送信後はリダイレクトさせることが多いですが、普通にLaravelのredirect()ヘルパーを使用してリダイレクトすることでInertiaがうまくレスポンスをInertiaレスポンスに変換してくれます。

以下はブックマーク追加後、ブックマーク一覧ページへリダイレクトする場合のサンプルソースです。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\Bookmark;
use Illuminate\Support\Facades\Validator;

class BookmarkController extends Controller
{
    //一覧表示
    public function index () 
    {
        return Inertia::render('Bookmark/Index',['bookmarks' => fn() => Bookmark::all()]);
    }
    
    //検索メソッド
    public function search ($queryWord) 
    {
        return Inertia::render('Bookmark/Index',[
            'bookmarks' => Bookmark::Where(
                'title', 'like', '%'.$queryWord.'%'
            )->orWhere(
                'url', 'like', '%'.$queryWord.'%'
            )->get()
        ]);
    }

    //追加メソッド
    public function store (Request $request) 
    {
        $validator = Validator::make($request->all(), [
            'title' => ['required', 'string', 'max:255'],
            'url' => ['required', 'url', 'max:255', 'unique:App\Models\Bookmark,url'],
        ])->validate();

        $bookmark = new Bookmark;
        $bookmark->title = $request->title;
        $bookmark->url = $request->url;
        $bookmark->save();
        return redirect()->route('bookmark.index'); //追加後ブックマーク一覧ページへリダイレクト
    }

    //削除メソッド
    public function destroy ($id) 
    {
        Bookmark::destroy($id);
        return redirect()->route('bookmark.index');//削除後ブックマーク一覧ページへリダイレクト
    }
}

外部ページへのリダイレクト

外部のサイトや、Inertiaではないページへリダイレクトさせたいとき、サーバーサイドでInertia::location()メソッドを利用します。

//Laravelで外部ページへリダイレクトするレスポンスを返す
return Inertia::location($url);

これにより409 Conflictレスポンスが作成され、このX-Inertia-Locationヘッダーにリダイレクト先のURLが含まれます。Inertiaのクライアントサイドでそれを検知し、windows.location = urlを実行してページを移動させます。

サンプル

今回使用したInertiaフォームを使ったサンプルです。

<template>
    <layout title="ブックマーク一覧">
        <h2 class="text-lg border-b p-1">
            <Link :href="route('bookmark.index')" class="text-blue-700 underline">
            ブックマーク一覧
            </Link>
        </h2>
        <ul class="list-disc list-inside p-2">
            <li v-for="bookmark in bookmarks" :key="bookmark.id" class="list-none flex justify-between">
                <a :href="bookmark.url" target=_blank class="text-blue-700" >{{ bookmark.title }}</a>
                <Link as="button" method="delete" :href="route('bookmark.delete',bookmark.id)" preserve-scroll :only="['bookmarks']" class="border border-red-400 m-1 p-1 text-sm text-red-400">削除</Link>
            </li>
        </ul>
        <form @submit.prevent="submit">
            <h3 class="border-l-4 p-1">ブックマーク追加</h3>
            <div class="flex w-full">
                <div class="w-20 text-right bg-gray-200 text-sm p-1 m-1">タイトル</div>
                <div class="w-40">
                    <input type=text name=title v-model="form.title" size=15 class="p-1 m-1 text-sm w-full" v-bind:class="{'border-red-300': form.errors.title}"/>
                </div>
                <div v-if="form.errors.title" class="p-1 m-1 text-sm text-red-400">{{ form.errors.title }}</div>
            </div>
            <div class="flex w-full">
                <div class="w-20 text-right bg-gray-200 text-sm p-1 m-1">URL</div>
                <div class="w-40">
                    <input type=text name=url v-model="form.url" size=15 class="p-1 m-1 text-sm w-full" v-bind:class="{'border-red-300': form.errors.url}" />
                </div>
                <div v-if="form.errors.url" class="p-1 m-1 text-sm text-red-400">{{ form.errors.url }}</div>
            </div>
            <button type="submit" class="border border-gray-400 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>
            <div v-if="form.recentlySuccessful" class="p-1 m-1 bg-green-200">送信が完了しました。</div>
        </form>
    </layout>
</template>

<script>
    import { Link } from '@inertiajs/inertia-vue3'
    import Layout from '@/Pages/Layout'

    export default {
        components: {
            Link,
            Layout,
        },
        data: function() {
            return {
                form: this.$inertia.form({
                    title: null,
                    url: null,
                }),
            }
        },
        props:{
            bookmarks: {
                type: Array,
            },
        },
        methods: {
            submit() {
                this.form.transform(function(data){
                    return {
                        title: data.title ? data.title : '無題',
                        url: data.url,
                    }
                }).post(route('bookmark.store'))
            },
        },
    }
</script>

ファイルアップロード

FormDataオブジェクトへの変換

Inertiaでファイルを送信する際にリクエストデータはFormDataオブジェクトに変換されて送信されます。ファイルを送信する際はエンコード形式を「multipart/form-data」にする必要があるためです。

forceFormData属性をtrueにセットすることで明示的にFormDataオブジェクトとして扱うようにすることもできます。

LaravelとInertiaでのファイルアップロード

Laravelのweb.phpにファイルアップロード用のルートを追加してみます。

use App\Http\Controllers\UploadController;
//略
Route::resource('/Upload',UploadController::class);

Laravelでファイルアップロード用のコントローラーを作成していきます。

php artisan make:controller UploadController

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

<?php

namespace App\Http\Controllers;

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

class UploadController extends Controller
{
    public function index () 
    {
        return Inertia::render('Upload');
    }
    
    public function store(Request $request){
        $request->validate([
            'file' => ['required', 'file', 'max:1024'],
        ]);//ファイルがアップロードされたかのバリデーション

        $file_name = $request->file('file')->getClientOriginalName();//元のファイル名を取得
        $request->file('file')->storeAs('public',$file_name);//アップロードされたファイルを保存
        return Inertia::render('Upload',['imageSrc' => '/storage/'.$file_name]);//ファイルパスを返す
    }
}

Vueファイルの作成

<template>
    <div>
        <h2 class="text-lg border-b p-1">
            <Link href="/Upload" class="text-blue-700 underline">
            アップロード
            </Link>
        </h2>
        <img v-if="imageSrc" :src="imageSrc" />
        <form @submit.prevent="submit">
            <h3 class="border-l-4 p-1">ファイルアップロード</h3>
            <div class="flex w-full">
                <div class="w-20 text-right bg-gray-200 text-sm p-1 m-1">ファイル</div>
                <div class="w-80">
                    <input type=file name=file ref=file @input="form.file = $event.target.files[0]"  class="p-1 m-1 text-sm w-full" />
                </div>
                <div v-show="form.errors.file" class="p-1 m-1 text-sm text-red-400">{{ form.errors.file }}</div>
            </div>
            <button type="submit" class="border border-gray-400 m-1 p-1 text-sm" :disabled="form.processing" v-bind:class="{'cursor-not-allowed': form.processing}">アップロード</button>
            <button v-if="form.processing" @click="form.cancel()" type="button" class="border bg-red-200 m-1 p-1 text-sm">送信中止</button>
            <progress v-if="form.progress" :value="form.progress.percentage" max="100">{{ form.progress.percentage }}%</progress>
            <div v-if="form.recentlySuccessful" class="p-1 m-1 bg-green-200">アップロードが完了しました。</div>
        </form>
    </div>
</template>

<script>
    import { Link } from '@inertiajs/inertia-vue3'

    export default {
        components: {
            Link,
        },
        data: function() {
            return {
                form: this.$inertia.form({
                    file: null,
                }),
            }
        },
        props:{
            imageSrc:{
                type: String,
            }
        },
        methods: {
            submit() {
                this.form.post('/Upload',{
                    onSuccess: (page) => {
                        this.$refs.file.value = '';
                    }
                })
            },
        },
    }
</script>

input type=file

ファイル選択ボタンは次のようになります。ファイル選択inputにv-modelは使えないため、ファイル選択時にform.fileに選択されたファイルを代入してInertiaフォームから扱えるようにしています。

<input type=file name=file ref=file @input="form.file = $event.target.files[0]"  class="p-1 m-1 text-sm w-full" />

progress属性

ファイルアップロード中に進行状況をprogress属性のpercentageで知ることができます。Inertia progressライブラリを使用して、バーを進行状況に応じて伸ばしていくサンプルはこちらです。

<progress v-if="form.progress" :value="form.progress.percentage" max="100">{{ form.progress.percentage }}%</progress>

アップロード成功時にファイル選択をクリアする

ファイル選択inputにv-modelでバインドしていないため、form.reset()メソッドではファイルの選択がクリアされません。refで直接fileのvalueを空にするか、preserveStatefalseにすることでクリアできます。

//成功時に、refで直接inputのfileのvalueを空にする
onSuccess: (page) => {
    this.$refs.file.value = '';
}

//preserveStateをFalseにすると、フォームの状態など含めて元に戻る
preserveState: (page) => Object.keys(page.props.errors).length,

localhost/Uploadへアクセスして確認してみます。

表示例

今回のソースコード全体はGitHubに掲載しています。

今回のまとめ
  • Inertia フォームヘルパーを使用するとフォーム送信に便利な機能を利用できる
  • リダイレクトはInertiaが良しなに処理してくれる
  • Inertiaリクエストでファイルアップロードも可能

次回はInertiaとLaravelによるバリデーションを取り上げます。

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をコピーしました