この記事では、Inertia.jsで非同期通信を行うrouter.visit()について解説します。
前回の記事ではInertia LinkでXHRリクエストを行う方法について解説しました。前回の記事はこちらからご覧ください。
Inertia.jsで非同期通信はどうやったらいいんでしょうか?
Inertia.jsで非同期通信を行うためのメソッドがrouter.visit()です。フォームの送信にも使えますよ!
XHRリクエストとは
前回Inertia LinkでリンクやボタンからXHRリクエストを作成する方法を学びました。そもそもXHRリクエストとは、XMLHttpRequestの略で、JavascriptからサーバーへHTTPリクエストを行うことです。
SPA(シングルページアプリケーション)では、初回のアクセス時にすべてのHTML構造を読み込み、その後は必要なデータだけを必要に応じて読み込んで画面を更新していきます。その必要なデータを読み込む際に必要なのが、XHRリクエストです。
Inertia.jsでは、初回以降サーバーへのリクエストをInertiaリクエストによって行います。そのInertiaリクエストを作成するのが、前回説明したInertia Linkや、今回説明するrouter.visit()です。
router.visit()を使うことで、フォームの送信やファイルアップロードが行えます。
router.visit()の使い方
メソッドの呼び出し方と引数
router.visit()は以下の引数を取ります。
import { router } from '@inertiajs/vue3'
router.visit(url, {
method: 'get',
data: {},
replace: false,
preserveState: false,
preserveScroll: false,
only: [],
headers: {},
errorBag: null,
forceFormData: false,
onCancelToken: cancelToken => {},
onCancel: () => {},
onBefore: visit => {},
onStart: visit => {},
onProgress: progress => {},
onSuccess: page => {},
onError: errors => {},
onFinish: visit => {},
})
各HTTPメソッド専用の関数
router.visit()では引数のmethodでどのHTTPメソッドを使用するか指定できますが、GET用、POST用など専用の関数も用意されています。第一引数でURL、第二引数にデータ配列、そしてvisit()と同じオプションを第三引数で指定できます。
import { router } from '@inertiajs/vue3'
router.get(url, data, options)
router.post(url, data, options)
router.put(url, data, options)
router.patch(url, data, options)
router.delete(url, options)
router.reload(options) // 現在のURLを再読み込みする。preserveState と preserveScroll はtrueにセットされる。
router.visit()や各種専用メソッドのオプション
HTTPメソッド
POSTリクエストを作成するには、2つの方法があります。専用のメソッドを使うか、visitメソッドのmehodオプションにpostを指定するかです。
import { router } from '@inertiajs/vue3'
//Inertia.postメソッドを利用する方法
router.post(url, data, options)
//Inertia.visitのmethodオプションに'post'を指定する方法
router.visit(url, { method: 'post' })
送信データ
送信するデータはget(),put(),post(),patch()メソッドを使用する場合は第二引数で,visit()メソッドを使用する場合はdataオプションで指定します。
import { router } from '@inertiajs/vue3'
//専用メソッドを使用する場合
router.post('/Bookmark/store',{
title: 'Yahoo!',
url: 'https://www.yahoo.co.jp/',
});
//visitメソッドを使用する場合
router.visit('/Bookmark/store',{
method: 'post',
data:{
title: 'Yahoo!',
url: 'https://www.yahoo.co.jp/',
}
});
履歴管理
Inertia.jsは、異なるURLへのリクエストが行われたとき、自動的にそのURLをブラウザ履歴に追加しますが、replaceオプションをtrueにセットすると、ページ移動の際に履歴を追加するのではなく、現在の履歴を書き換えるようにすることができます。
import { router } from '@inertiajs/vue3'
router.visit('/Bookmark/search/google',{ replace: true });
状態の保持
デフォルトではInertiaリクエストによって新たなページが読み込まれると、フォームに入力された値やスクロール位置などはリセットされます。preserveStateオプションをtrueにセットすると、それらの状態を維持してリクエストを行うことができます。
例えばフォーム送信する際に、バリデーションの結果エラーがあった場合などフォームの内容をクリアせずにおきたいときなどに使用します。
import { router } from '@inertiajs/vue3'
router.visit('/Bookmark/search/google',{ preserveState: true });
レスポンスに応じてpreserveStateを後からセットすることもできます。エラーがある場合はフォームの内容を残しておき、エラーがなければクリアするなどの用途に使えます。
//検索結果が見つからない場合はフォーム入力値をそのまま残す
import { router } from '@inertiajs/vue3'
router.visit(route('bookmark.search', this.searchWord),{
preserveState: function(page){
return Object.keys(page.props.bookmarks).length == 0
},
});
スクロールポジションの保持
ページ間の移動をする場合、デフォルトの動作はスクロールポジションがリセットされ、ページ最上部へと移動します。これをpreserveScrollオプションをtrueにセットすることで防げます。
import { router } from '@inertiajs/vue3'
router.visit(route('bookmark.search', 'google'),{ preserveScroll: true });
部分的なデータ読み込み
そのページに含まれるデータのうち、ある特定のデータだけを読み込めばいい場合、onlyオプションでその読み込むデータを指定できます。
import { router } from '@inertiajs/vue3'
//bookmarksだけを読み込みたい場合
router.visit(route('bookmark.search', 'google'),{ only: ['bookmarks'] });
エラーバッグ名
同じページに複数のフォームがあり、さらに同じ名前の項目がある場合、どのフォームでバリデーションエラーが発生したのかを識別するための名前をerrorBagオプションで指定します。
(関係する部分のみ抽出しています。フルのコードはソースコードからご覧ください)
//Vue側のエラー表示
<div v-if="bookmarkError.url" class="p-1 m-1 text-sm text-red-400">{{ bookmarkError.url }}</div>
//Script setup
//computedをインポート
import { computed } from "vue";
//propsを定義
const props = defineProps({
errors: {
type: Object
}
});
//post
const addBookmark = () => {
router.visit(route('bookmark.store'),{
method: 'post', //POSTメソッドで送信
data:{
title: newTitle.value, //送信データを指定
url: newUrl.value,
},
errorBag: 'addBookmark', //エラーバッグ名を指定
});
};
//computedでエラーバッグ内のエラーを計算
const bookmarkError = computed(() => {
return props.errors.addBookmark || {}
});
//コントローラー側でのバリデーション
$validator = Validator::make($request->all(), [
'title' => ['required', 'string', 'max:255'],
'url' => ['required', 'string', 'max:255'],
])->validate();
//エラーがあった場合のpropsのJSONデータ(一部)
"errors":{
"addBookmark":{
"title":"The title field is required.",
"url":"The url field is required."}
}
ファイルアップロード
Inertiaリクエストにファイルが含まれる場合、自動的にFormDataオブジェクトに変換されて送信されます。forceFormDataオプションをtrueにセットすると、強制的にFormDataオブジェクトに変換して送信が行われます。
this.$inertia.post('/companies', data, {
forceFormData: true,
})
カスタムヘッダー
HTTPヘッダーに独自のヘッダーを追加してリクエストしたい場合はheadersオプションで指定します。
import { router } from '@inertiajs/vue3'
router.post('/users', data, {
headers: {
'Custom-Header': 'value',
},
})
リクエストのキャンセル
Inertiaリクエストを行った後で、中止したい場合は発行されたキャンセルトークンのcancel()メソッドを呼び出すことでリクエストをキャンセルできます。キャンセルされた場合onCancel()とonFinish()イベントコールバックが呼び出されます。
キャンセルトークンは、onCancelToken()コールバックで取得できます。
//キャンセルボタンを表示
<button v-if="cancelToken.cancel" v-on:click="cancelToken.cancel()" class="border border-red-400 m-1 p-1 text-sm">キャンセル</button>
<script setup>
import { reactive} from "vue";
//VueのDataとしてcancelTokenを定義
const cancelToken = reactive({cancel: false});
//router.visitを行うメソッド
router.visit(route('bookmark.store'),{
method: 'post', //POSTメソッドで送信
data:{
title: newTitle.value, //送信データを指定
url: newUrl.value,
},
onCancelToken: (_cancelToken) => (Object.assign(cancelToken, _cancelToken)), //キャンセルトークンが発行されたらキャンセルトークンを保存
onCancel: () => {console.log( 'onCancel' ) }, //リクエストキャンセル時
onFinish: visit => {Object.assign(cancelToken, {cancel: false}); console.log( 'onFinish' )}, //リクエスト完了時。成功、エラー、キャンセルそれぞれのコールバック実行後に呼び出される
});
イベントコールバック
Inertiaリクエストの進行状況に合わせてイベントコールバックが呼び出されます。
onBefore : リクエスト直前
onBefore()コールバックはリクエストを行う直前に呼び出されます。falseを返すとリクエストがキャンセルされます。例えば、confirmダイアログを表示して確認を行えます。
import { router } from '@inertiajs/vue3'
router.delete(`/users/${user.id}`, {
onBefore: () => confirm('本当に削除しますか?'),
})
onStart : リクエスト開始時
Inertiaリクエストが開始されたときにonStart()が呼び出されます。コールバックの引数でrouter.visitに渡したURLやデータ、オプションがオブジェクトで取得できます。
onProgress : リクエスト進行中
Inertiaリクエストの進行中にonProgress()が呼び出されます。ファイルアップロードの状況確認などに使うことができます。引数で進行具合のオブジェクトを取得できます。
onSuccess : リクエスト成功時
Inertiaリクエストが成功し、結果が返ってきたときにonSuccess()が呼び出されます。引数では結果として返ってきた新しいページデータが取得できます。
onError : エラー時
Inertiaリクエストが失敗したり、エラーが返ってきた場合にonError()が呼び出されます。引数ではエラーオブジェクトが渡されます。
//渡されるエラーオブジェクトの例。
//errorBagでエラーバッグ名を渡していてもエラーオブジェクトには含まれないことが分かります。
{ title: "The title field is required.", url: "The url field is required." }
onCancel : キャンセル時
前項で説明したキャンセルトークンを利用してリクエストのキャンセルが行われたときにonCancel()が呼び出されます。
onFinish : リクエスト完了時
リクエストが成功しようが失敗しようが、キャンセルされようがonFinish()は最後に呼び出されます。onSuccess()やonError()コールバックの中でPromiseでメソッドを返すと、それらが処理された後でonFinish()は呼び出されます。
import { router } from '@inertiajs/vue3'
router.post(url, {
onSuccess: () => {
return Promise.all([
this.doThing(),
this.doAnotherThing()
])
}
onFinish: visit => {
// ここは doThing() と doAnotherThing() の処理が終わってから呼び出されます。
},
})
実例
//各イベントコールバックの状態をlogに出力します。
import { router } from '@inertiajs/vue3'
router.visit(route('bookmark.store'),{
method: 'post',
data:{
title: this.newTitle,
url: this.newUrl,
},
onBefore: (visit) => confirm('追加しますか?'),
onStart: (visit) => { console.log( visit ) },
onProgress: (progress) => { console.log( progress ) },
onSuccess: (page) => { console.log( page ) },
onError: (errors) => {console.log( errors ) },
onCancel: () => {console.log( 'onCancel' ) },
onFinish: visit => {console.log( 'onFinish' )},
});
サンプル
今回使用したrouter.visit()を使ったサンプルです。すべてのソースコードはGitHubにも掲載しています。
<script setup>
import { ref, 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 newTitle = ref('');
const newUrl = ref('');
const cancelToken = reactive({cancel: false});
const bookmarkError = computed(() => {
return props.errors.addBookmark || {}
});
const addBookmark = () => {
router.visit(route('bookmark.store'),{
method: 'post', //POSTメソッドで送信
data:{
title: newTitle.value, //送信データを指定
url: newUrl.value,
},
errorBag: 'addBookmark', //エラーバッグ名を指定
preserveState: function(page){ //フォームの内容を保持するかどうか
return Object.keys(page.props.errors).length != 0 //エラーがあればフォームの内容を保持する
},
preserveScroll: true, //スクロールポジションを維持する
onCancelToken: (_cancelToken) => (Object.assign(cancelToken, _cancelToken)), //キャンセルトークンが発行されたらキャンセルトークンを保存
onBefore: (visit) => confirm('追加しますか?'), //リクエスト送信前に確認ダイアログを表示し、その結果に応じて続行・キャンセル
onStart: (visit) => { console.log( visit ) }, //リクエスト開始時に
onProgress: (progress) => { console.log( progress ) }, //リクエスト進行中
onSuccess: (page) => { console.log( page ) }, //リクエスト成功時
onError: (errors) => {console.log( errors ) }, //エラー発生時
onCancel: () => {console.log( 'onCancel' ) }, //リクエストキャンセル時
onFinish: visit => {Object.assign(cancelToken, {cancel: false}); console.log( 'onFinish' )}, //リクエスト完了時。成功、エラー、キャンセルそれぞれのコールバック実行後に呼び出される
});
};
</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>
<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=newTitle v-model="newTitle" size=15 class="p-1 m-1 text-sm w-full border" />
</div>
<div v-if="bookmarkError.title" class="p-1 m-1 text-sm text-red-400">{{ bookmarkError.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=newUrl v-model="newUrl" size=15 class="p-1 m-1 text-sm w-full border" />
</div>
<div v-if="bookmarkError.url" class="p-1 m-1 text-sm text-red-400">{{ bookmarkError.url }}</div>
</div>
<button v-on:click="addBookmark" class="border border-gray-400 m-1 p-1 text-sm">ブックマーク追加</button>
<button v-if="cancelToken.cancel" v-on:click="cancelToken.cancel()" class="border border-red-400 m-1 p-1 text-sm">キャンセル</button>
</Layout>
</template>
<?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' => 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', $parameters = [], $status = 303, $headers = []);
}
//削除メソッド
public function destroy ($id)
{
Bookmark::destroy($id);
return redirect()->route('bookmark.index', $parameters = [], $status = 303, $headers = []);
}
}
Comments