この記事では、Inertia.jsでのフォーム送信とリダイレクトについて解説します。
前回の記事ではrouter.visit()について解説しました。今回のInertia Formはrouter.visit()をラップしてフォームに特化したクラスです。ぜひ前回の記事と合わせてお読みください。
Inertiaでフォームを送信するにはどうしたらいいんでしょうか?
Inertiaにはフォーム送信に便利なメソッドやプロパティを集めたFormクラスがあります。
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)
使用可能なオプション
使用可能なオプションは、preserveScrollやpreserveStateなど、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でファイルアップロード用のコントローラーを作成していきます。
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を空にするか、preserveStateをfalseにすることでクリアできます。
//テンプレートで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に掲載しています。
Comments