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

Laravel

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

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

Laraveler

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

シップ

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

この記事で使用しているバージョン
  • Laravel 10
  • Vue 3 (Composition API)
  • Inertia 1.0.10

Inertia Formの使用

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

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

<script setup>
import { reactive, computed } from "vue";
import { Link, router } from '@inertiajs/vue3'
import Layout from '@/Pages/Layout.vue'

const props = defineProps({ 
    bookmarks: {
        type: Array
    }, 
    errors: {
        type: Object
    }
});
const form = reactive({
    title: null,
    url: null,
});

const submit = () => {
    router.post(route('bookmark.store'), form);
}
</script>

<template>
    <Layout title="ブックマーク一覧">
        <h2 class="text-lg border-b p-2">
            <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 v-model="form.title" size=15 class="p-1 m-1 text-sm w-full border" />
                </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 v-model="form.url" size=15 class="p-1 m-1 text-sm w-full border" />
                </div>
            </div>
            <button type="submit" class="border border-gray-400 m-1 p-1 text-sm">ブックマーク追加</button>
        </form>
    </Layout>
</template>

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

フォームヘルパーの利用

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

//useFormをインポート
import { Link, router, useForm } from '@inertiajs/vue3'  
      
//formオブジェクトをuseFormインスタンスに変更
const form = useForm({
    title: null,
    url: null,
});


//送信をInertiaのpostメソッドから、useFormのpostメソッドを利用するように変更
const submit = () => {
    form.post(route('bookmark.store'));
}

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

//1. フォームヘルパーの作成
useForm({フォームのフィールド1: 初期値1, ...})

//2. フォームヘルパーのpostメソッドでフォームの内容を送信
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など、router.visit()で使用できるオプションと一緒です。onSuccess()など各種コールバックも使用できます。

送信前に値を整形

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

//titleが入力されていない場合は「無題」という名前で送信する
    form
    .transform((data) => ({
        ...data,
        title: data.title ? data.title : '無題',
    }))
    .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-if="form.errors.url" class="p-1 m-1 text-sm text-red-400">{{ form.errors.url }}</div>

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

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

// 特定のフィールでのエラーを解除
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>

デフォルト値のセット

defaults()メソッドで、現在のフォームの値を、初期値として設定することが出来ます。reset()メソッドを実行した際にこの値に戻されます。引数で指定した特定のフィールドだけ初期値を設定することもできます。

<button @click="form.defaults()" type="button" 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レスポンスに変換してくれます。

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

return to_route('bookmark.index'); //ブックマーク一覧ページへリダイレクト

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

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

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

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

サンプル

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

<script setup>
import { reactive, computed } from "vue";
import { Link, router, useForm } from '@inertiajs/vue3'
import Layout from '@/Pages/Layout.vue'

const props = defineProps({ 
    bookmarks: {
        type: Array
    }, 
    errors: {
        type: Object
    }
});
const form = useForm({
    title: null,
    url: null,
});

const submit = () => {
    form
    .transform((data) => ({
        ...data,
        title: data.title ? data.title : '無題',
    }))
    .post(route('bookmark.store'));
    console.log("form posted");
}
</script>

<template>
    <Layout title="ブックマーク一覧">
        <h2 class="text-lg border-b p-2">
            <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 v-model="form.title" size=15 class="p-1 m-1 text-sm w-full border" />
                </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 v-model="form.url" size=15 class="p-1 m-1 text-sm w-full border" />
                </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 v-if="form.processing" @click="form.cancel()" type="button" class="border bg-red-200 m-1 p-1 text-sm">送信中止</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 @click="form.defaults()" type="button" class="border bg-gray-200 m-1 p-1 text-sm" :disabled="!form.isDirty" v-bind:class="{'cursor-not-allowed': !form.isDirty}">初期値として設定</button>
            <div v-if="form.recentlySuccessful" class="p-1 m-1 bg-green-200">送信が完了しました。</div>
        </form>
    </Layout>
</template>

ファイルアップロード

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でファイルアップロード用のコントローラーを作成していきます。

Dockerの場合は「make app」でAppコンテナに入ってからこのコマンドを入力してください
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ファイルの作成

<script setup>
import { ref } from "vue";
import { Link, useForm } from '@inertiajs/vue3'

const props = defineProps({ 
    imageSrc: {
        type: String
    }, 
});
const form = useForm({
    file: null,
});
const file = ref(null); //File inputを参照するためのref

const submit = () => {
    form.post('/Upload',{
        onSuccess: (page) => {
            file.value = '';
        }
    })
}
</script>

<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>

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で知ることができます。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=fileとした要素のrefを同名の変数名fileで作成しておくと、fileで要素を参照できます。
const file = ref(null);

//成功時に、refで直接inputのfileのvalueを空にする
onSuccess: (page) => {
    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 10+Inertia.js+Tailwindインストール 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.jsのLinkコンポーネントについて解説します。
Inertia.jsでXHRリクエスト Inertia.js入門 #6
この記事では、Inertia.jsで非同期通信を行うrouter.visit()について解説します。
Inertia.jsでのフォームとリダイレクト Inertia.js入門 #7
この記事では、Inertia.jsでのフォーム送信とリダイレクトについて解説します。
Inertiaでのバリデーション Inertia.js入門 #8
この記事では、Inertia.jsを使った際のバリデーションエラーの表示について解説します。
Inertiaで共通データとフラッシュメッセージ Inertia.js入門 #9
この記事では、Inertiaのレスポンスに毎回同じデータを含める方法やフラッシュメッセージの使い方について解説します。
この記事を書いた人

PHPが好物な個人開発プログラマ。フリーランスエンジニアとしてWebサービス作ったりしてます。15年の経験を生かしてMENTAでメンターもやってます。WordPressやPHPでお困りのことがあればご相談に乗りますのでDMください。

Follow on SNS
Laravel
SOHO MIND

Comments