InertiaでXHRリクエスト Inertia.js入門 #6

Inertia Visit Laravel

この記事では、Inertia.jsで非同期通信を行うInertia.visit()について解説します。

前回の記事ではInertia linkでXHRリクエストを行う方法について解説しました。前回の記事はこちらからご覧ください。

Laraveler
Laraveler

Inertiaでフォームデータの送信はどうしたらいいの?

シップ
シップ

Inertiaで非同期通信を行うためのメソッドがInertia.visit()です。フォームの送信にも使えますよ!

XHRリクエストとは

前回Inertia LinkでリンクやボタンからXHRリクエストを作成する方法を学びました。そもそもXHRリクエストとは、XMLHttpRequestの略で、JavascriptからサーバーへHTTPリクエストを行うことです。

SPA(シングルページアプリケーション)では、初回のアクセス時にすべてのHTML構造を読み込み、その後は必要なデータだけを必要に応じて読み込んで画面を更新していきます。その必要なデータを読み込む際に必要なのが、XHRリクエストです。

Inertia.jsでは、初回以降サーバーへのリクエストをInertiaリクエストによって行います。そのInertiaリクエストを作成するのが、前回説明したInertia Linkだったり、今回説明するInertia.visit()です。

Inertia.visit()を使うことで、フォームの送信やファイルアップロードが行えます。

Inertia.visit()の使い方

メソッドの呼び出し方と引数

Inertia.visit()は以下の引数を取ります。

this.$inertia.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メソッド専用の関数

Inertia.visitでは引数のmethodでどのHTTPメソッドを使用するか指定できますが、GET用、POST用など専用の関数も用意されています。第一引数でURL、第二引数にデータ配列、そしてVisit()と同じオプションを第三引数で指定できます。

this.$inertia.get(url, data, options)
this.$inertia.post(url, data, options)
this.$inertia.put(url, data, options)
this.$inertia.patch(url, data, options)
this.$inertia.delete(url, options)
this.$inertia.replace(url, options)
this.$inertia.reload(options) // 現在のURLを再読み込みする。preserveState と preserveScroll はtrueにセットされる。

Inertia.visit()や各種専用メソッドのオプション

HTTPメソッド

POSTリクエストを作成するには、2つの方法があります。専用のメソッドを使うか、visitメソッドのmehodオプションにpostを指定するかです。

//Inertia.postメソッドを利用する方法
this.$inertia.post(url, data, options)

//Inertia.visitのmethodオプションに'post'を指定する方法
this.$inertia.visit(url, { method: 'post' })

putまたはpatchメソッドでのファイルアップロードはLaravelではサポートされていないため実際にはpostメソッドを使用してリクエストが行われます。

送信データ

送信するデータはget(),put(),post(),patch()メソッドを使用する場合は第二引数で,visit()メソッドを使用する場合はdataオプションで指定します。

//専用メソッドを使用する場合
this.$inertia.post('/Bookmark/store',{
    title: 'Yahoo!', 
    url: 'https://www.yahoo.co.jp/',                    
});

//visitメソッドを使用する場合
this.$inertia.visit('/Bookmark/store',{
    method: 'post',
    data:{
        title: 'Yahoo!', 
        url: 'https://www.yahoo.co.jp/',   
    }
});

履歴管理

Inertia.jsは、異なるURLへのリクエストが行われたとき、自動的にそのURLをブラウザ履歴に追加しますが、replaceオプションをtrueにセットすると、ページ移動の際に履歴を追加するのではなく、現在の履歴を書き換えるようにすることができます。

this.$inertia.visit('/Bookmark/search/google',{ replace: true });

状態の保持

デフォルトではInertiaリクエストによって新たなページが読み込まれると、フォームに入力された値やスクロール位置などはリセットされます。preserveStateオプションをtrueにセットすると、それらの状態を維持してリクエストを行うことができます。

例えばフォーム送信する際に、バリデーションの結果エラーがあった場合などフォームの内容をクリアせずにおきたいときなどに使用します。

post, put, patch, delete, reloadの各メソッドではデフォルトでpreserveStateオプションはtrueにセットされています。

this.$inertia.visit(route('bookmark.search', 'google'),{ preserveState: true });

レスポンスに応じてpreserveStateを後からセットすることもできます。エラーがある場合はフォームの内容を残しておき、エラーがなければクリアするなどの用途に使えます。

//検索結果が見つからない場合はフォーム入力値をそのまま残す
this.$inertia.visit(route('bookmark.search', this.searchWord),{ 
    preserveState: function(page){ 
			return Object.keys(page.props.bookmarks).length == 0 
		   }, 
});

スクロールポジションの保持

ページ間の移動をする場合、デフォルトの動作はスクロールポジションがリセットされ、ページ最上部へと移動します。これをpreserveScrollオプションをtrueにセットすることで防げます。

this.$inertia.visit(route('bookmark.search', 'google'),{ preserveScroll: true });

部分的なデータ読み込み

そのページに含まれるデータのうち、ある特定のデータだけを読み込めばいい場合、onlyオプションでその読み込むデータを指定できます。

//bookmarksだけを読み込みたい場合
this.$inertia.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>

//Vue側でのpost送信
computed: {
    bookmarkError() {
        return this.$page.props.errors['addBookmark'] || {}
    },
},
methods: {
    addBookmark: function(event){
        this.$inertia.visit(route('bookmark.store'),{
            method: 'post',
            data:{
                title: this.newTitle, 
                url: this.newUrl,
            },
            errorBag: '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オプションで指定します。

this.$inertia.post('/users', data, {
  headers: {
    'Custom-Header': 'value',
  },
})

リクエストのキャンセル

Inertiaリクエストを行った後で、中止したい場合は発行されたキャンセルトークンのcancel()メソッドを呼び出すことでリクエストをキャンセルできます。キャンセルされた場合onCancel()onFinish()イベントコールバックが呼び出されます。

キャンセルトークンは、onCancelToken()コールバックで取得できます。

通常のリクエストではキャンセルする間もなくリクエストが完了するので、時間のかかる処理やファイルアップロードなどをキャンセルする時に使えます。

//キャンセルボタンを表示
<button v-if="cancelToken" class="border border-red-400 m-1 p-1 text-sm">キャンセル</button>

//VueのDataとしてcancelTokenを定義
data:function(){
    return {
        newTitle: '',
        newUrl: '',
        cancelToken: null,
    }
}

//InertiaVisitを行うメソッド
this.$inertia.visit(route('bookmark.store'),{
    method: 'post',
    data:{
        title: this.newTitle, 
        url: this.newUrl,
    },
    onCancelToken: (cancelToken) => (this.cancelToken = cancelToken),
    onCancel: () => { console.log("canceled")},
});

イベントコールバック

Inertiaリクエストの進行状況に合わせてイベントコールバックが呼び出されます。

onBefore : リクエスト直前

onBefore()コールバックはリクエストを行う直前に呼び出されます。falseを返すとリクエストがキャンセルされます。例えば、confirmダイアログを表示して確認を行えます。

Inertia.delete(`/users/${user.id}`, {
  onBefore: () => confirm('本当に削除しますか?'),
})

onStart : リクエスト開始時

Inertiaリクエストが開始されたときにonStart()が呼び出されます。コールバックの引数でInertia.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()は呼び出されます。

Inertia.post(url, {
  onSuccess: () => {
    return Promise.all([
      this.doThing(),
      this.doAnotherThing()
    ])
  }
  onFinish: visit => {
    // ここは doThing() と doAnotherThing() の処理が終わってから呼び出されます。
  },
})

実例

//各イベントコールバックの状態をlogに出力します。
this.$inertia.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' )},
});

サンプル

今回使用したInertia.visit()を使ったサンプルです。すべてのソースコードはGitHubにも掲載しています。

<template>
    <layout>
        <h2 class="text-lg border-b p-1">
            <inertia-link :href="route('bookmark.index')" class="text-blue-700 underline">
            ブックマーク一覧
            </inertia-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>
                <inertia-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">削除</inertia-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" v-bind:class="{'border-red-300': bookmarkError['title']}" />
            </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" v-bind:class="{'border-red-300': bookmarkError['url']}" />
            </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" v-on:click="cancelToken.cancel()" class="border border-red-300 m-1 p-1 text-sm">キャンセル</button>
        
    
    </layout>
</template>

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

    export default {
        components: {
            Layout,
        },
        data:function(){
            return {
                newTitle: '',
                newUrl: '',
                status: null,
                cancelToken: null,
            }
        },
        props:{
            bookmarks: {
                type: Array,
            },
            errors: {
                type: Object,
            }
        },
        mounted: function(){
            document.title = "ブックマーク一覧";
        },
        computed: {
            bookmarkError() {
                return this.$page.props.errors['addBookmark'] || {}
            },
        },
        methods: {
            addBookmark: function(event){
                this.$inertia.visit(route('bookmark.store'),{
                    method: 'post', //POSTメソッドで送信
                    data:{
                        title: this.newTitle, //送信データを指定
                        url: this.newUrl,
                    },
                    errorBag: 'addBookmark',    //エラーバッグ名を指定
                    preserveState: function(page){  //フォームの内容を保持するかどうか
                        return Object.keys(page.props.errors).length != 0 //エラーがあればフォームの内容を保持する
                    },
                    preserveScroll: true,   //スクロールポジションを維持する
                    onCancelToken: (cancelToken) => (this.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 => {this.cancelToken = null; console.log( 'onFinish' )}, //リクエスト完了時。成功、エラー、キャンセルそれぞれのコールバック実行後に呼び出される
                });
            },
        }
    }
</script>
<?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', $parameters = [], $status = 303, $headers = []);
    }
    
    //削除メソッド
    public function destroy ($id) 
    {
        Bookmark::destroy($id);
        return redirect()->route('bookmark.index', $parameters = [], $status = 303, $headers = []);
    }
}
表示例
今回のまとめ
  • Inertia.visit()を使ってXHRリクエストを行える
  • 各種オプションを指定することで動作やイベントコールバックを利用できる

次回はInertia.jsにおけるフォームとリダイレクトを取り上げます。

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を使った際のバリデーションエラーの表示について解説します。

Comments

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