Laravel8から導入されたInertia.jsを利用してデータの一覧表示、データ作成、更新、削除をどのように行うのか知りたいという人もいるかと思います。Inertia.jsのCRUDが理解できればInertia.jsでのアプリケーション開発を行うことができます。

本文書ではLaravel8から新たに追加されたJetStream+Inertia.jsを利用してできるだけシンプルなコードを使ってCRUD処理(Create, Read, Update, Delete)の方法を説明します。またJetStreamに含まれるProfileやTeamのページで実際に利用されるコンポーネントを最大限活用しています。

これからの作業は、Laravel8へのJetStreamはインストール済の状態から開始していくので事前にLaravel8の環境の構築を完了しておいてください。

環境構築

Blog用のモデル・コントローラー作成

動作確認を行うために利用するモデルはtitle, contentの2つの列を持つシンプルなBlog(ブログ)を想定しています。まずBlogのモデル、コントローラーの作成を行います。

Blogモデルの作成はphp artisan make:modelコマンドで行います。実行するとapp¥ModelsにBlog.phpファイルとdatabase¥migrationsの下にマイグレーションファイルが作成されます。


 % php artisan make:model Blog -m 
Model created successfully.
Created Migration: 2020_11_10_073153_create_blogs_table

次にphp artisan make:controllerコマンドを利用してBlogモデル用のコントローラーを作成します。


 % php artisan make:controller BlogController --resource
Controller created successfully.

app¥Http¥Controllerの下にBlogController.phpファイルが作成されます。

Blogテーブルの作成

blogテーブルにはtitleとcontent列のみ作成します。titleはブログのタイトルなのでstring, contentはブログの内容を保存するためtextを設定しています。

先ほどphp artisan mak:modelコマンドで作成したマイグレーションファイルを開いて以下を設定してください。


public function up()
{
    Schema::create('blogs', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('content');
        $table->timestamps();
    });
}

php artisan migrateコマンドを実行してblogsテーブルを作成してください。


 % php artisan migrate 
Migrating: 2020_11_10_073153_create_blogs_table
Migrated:  2020_11_10_073153_create_blogs_table (5.48ms)

テーブル作成後テーブルへのデータ登録ができるようにBlog.phpファイルに追加した列の情報を追加します。


class Blog extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'content'
    ];
}

ルーティングの追加

/blogsにアクセスするとブログ一覧が表示されるようにルーティングファイルのweb.phpにルーティングを追加します。下記のルーティングの設定ではブラウザから/blogsにアクセスするとBlogController.phpファイルのindexメソッドが実行されます。ルーティングにはnameメソッドで名前blog.indexをつけており、この名前を利用することでこのルーティングにアクセスを行うことができます。


//略
use App\Http\Controllers\BlogController;
//略
Route::get('/blogs', [BlogController::class, 'index'])
    ->name('blog.index');
nameメソッドでルーティングに名前をつけるのは必須ではありませんが、名前を使った場合はもしURLを/blogsから/admin/blogsに変更した場合もvueファイル側での変更を行う必要がありません。

php artisan serveコマンドで開発サーバを起動して、/blogsにアクセスしてください。ここまでの設定であればブラウザには真っ白の画面が表示されます。indexメソッドに何も設定を行っていないため何も問題はありません。

アプリケーションのユーザ登録が完了しているユーザのみアクセスできるようにミドルウェアの設定を行います。アクセス制限を行うことができます。


//略
use App\Http\Controllers\BlogController;
//略
Route::get('/blogs', [BlogController::class, 'index'])
    ->name('blog.index')
    ->middleware('auth');

ミドルウェアのauthを設定して再度/blogsにアクセスするとログイン画面が表示されます。ユーザの登録が完了していない場合はユーザ登録を行ってください。ユーザの登録・ログインは初期画面の右上から行うことができます。

Laravelの初期画面
Laravelの初期画面

登録したユーザでログインすると下記の画面が表示されます。

ユーザ登録後のdashboard画面
ユーザ登録後のdashboard画面

リンクの設定

ログインすると/dashboardにリダイレクトされます。/dashboardで表示されるページの内容はresources¥js¥Pagesの下に保存されているDashboard.vueファイルに記述されています。その理由はweb.phpファイルのdashboardへのルーティングを見ることで確認することができます。


Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () {
    return Inertia\Inertia::render('Dashboard');
})->name('dashboard');
Inertia.jsではrenderメソッドで指定するvueファイルはresources¥js¥Pagesに保存されています。これまでのLaravelで利用されているviewのBladeファイルはresources¥viewの下が保存場所です。

DashBoard.vueファイルを見るとAppLayout.vueを利用してページのレイアウトが設定されていることがわかります。


<template>
    <app-layout> //ここがAppLayout.vue
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Dashboard
            </h2>
        </template>
Laravel8ではCSSにtailwindcssが利用されています。

AppLayout.vueファイルを開いて、Blogページへのリンクを設定します。routeメソッドに設定しているblog.indexはルーティングで設定したnameの値です。


<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
    <jet-nav-link :href="route('dashboard')" :active="route().current('dashboard')">
        Dashboard
    </jet-nav-link>

//以下を追加
    <jet-nav-link :href="route('blog.index')" :active="route().current('blog.index')">
        Blog
    </jet-nav-link>
</div>

Blogページへのリンクを追加して保存してブラウザで再度Dashboardにアクセスしても追加したリンクは反映されません。Inertis.jsはPHPとは異なり、更新後はビルドを行う必要があるのでnpm run watchコマンドを実行しておく必要があります。ビルドが完了して再度アクセスするとBlogのリンクが表示されます。しかしリンクをクリックしてもまだページが作成できていないので何も起こりません。

Blogのリンクを追加
Blogのリンクを追加
ビルド完了後も更新した内容が反映されない場合はブラウザでキャッシュの削除を行ってください。

indexページの作成

blogsにアクセスした場合に表示されるページをBlogController.phpファイルのindexメソッドで指定します。Inertiaのrenderメソッドで表示するページを設定します。resources¥js¥Pagesの下にBlogディレクトリを作成してください。その後DashBoard.vueファイルを複製して保存し、名前をIndex.vueに変更してください。


use Inertia\Inertia; //必要

class BlogController extends Controller
{
    public function index(){
        return Inertia::render('Blog/Index');
    }

作成したIndex.vueファイルを開いてwelcomeコンポーネントを削除し、template #headerにあるDashboardをBlogに変更してください。


<template>
    <app-layout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Blog
            </h2>
        </template>
    </app-layout>
</template>

<script>
    import AppLayout from '@/Layouts/AppLayout'

    export default {
        components: {
            AppLayout,
        },
    }
</script>

blogsにアクセスすると下記のページが表示されます。

BlogのIndexページ
BlogのIndexページ

Seedingによるデータの挿入

Index.vueにアクセスした際にブログの一覧を表示させるためにはblogテーブルにデータを登録する必要があります。

Seeding機能を利用することでBlogテーブルに複数のダミーデータを短時間で簡単に挿入することができます。

php artisan make:factoryコマンドを利用してテーブルに挿入するデータの構造を記述したFactoryファイルを作成します。


 % php artisan make:factory BlogFactory --model=Blog
Factory created successfully.

database¥factoriesディレクトリにBlogFactory.phpファイルが作成されるのでtitle, content列に挿入するダミーデータの定義を行います。


public function definition()
{
    return [
        'title' => $this->faker->sentence,
        'content' => $this->faker->text,
    ];
}

次にdatabase¥seeders下にあるDatabaseSeeder.phpファイルを開いて下記を設定してください。User用の設定が行っているので行を複製してUserをBlogに変更してください。factoryメソッドの10は10件のデータを作成することを表しています。


public function run()
{
    // \App\Models\User::factory(10)->create();
    \App\Models\Blog::factory(10)->create();
}

これでSeeding機能の設定は完了です。最後に実際にデータを挿入するphp artisan db:seedコマンドを実行します。successfullyのメッセージが表示されればデータの挿入は完了しています。


 % php artisan db:seed
Database seeding completed successfully.

これでInertia.jsを利用してCRUDの動作確認を行う準備は完了です。ここからCRUDの設定を行っていきます。

Blogデータの一覧表示

Seederを利用して挿入したデータをBlogController.php、Index.vueファイルを利用してブラウザに表示します。

BlogController.phpのindexメソッド内でBlogモデルを利用して保存された全データを取得します。データが保存されているかどうかddを利用して確認することができます。


//略
use Inertia\Inertia;
use App\Models\Blog;

class BlogController extends Controller
{
    public function index(){
        dd(Blog::all());
        return Inertia::render('Blog/Index',['blogs' => Blog::all()]);
    }
//略

blogsにアクセスして10件のデータが保存されているか確認してください。

テーブルへのデータの確認にはデータベースの管理ソフト、mysqlコマンド、php artisan tinkerなど複数あるので一番やりやすい方法で確認を行ってください。

データがテーブルに保存されていることが確認できたら実際にIndex.vueファイルを利用してデータを表示させてみましょう。

Index.vueファイルではBlogController.phpから渡された変数blogsのデータを受け取るためにpropsを利用します。propsで取得したデータはv-forを利用して展開します。


<template>
  <app-layout>
    <template #header>
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            Blog
        </h2>
    </template>
    <div>
      <div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
        <table>
            <thead>
            <tr>
                <th>タイトル</th>
                <th>コンテンツ</th>
            </tr>
            </thead>
            <tbody>
                <tr v-for="blog in blogs" :key="blog.id">
                    <td class="border px-4 py-2">{{ blog.title }}</td>
                    <td class="border px-4 py-2">{{ blog.content }}</td>
                </tr>
            </tbody>
        </table>
      </div>
    </div>
  </app-layout>
</template>

<script>
    import AppLayout from '@/Layouts/AppLayout'

    export default {
        props:['blogs'],
        components: {
            AppLayout,
        },
    }
</script>
Inertia.jsを利用しない場合はライフサイクルフックmountedの中でaxios, fetchを利用してblogsのデータをLaravelから取得する必要がありますがInertia.jsではコントローラーで取得したデータをpropsを利用してIndex.vueファイルに渡します。

ブラウザで確認するとテーブルに保存されている10件分のBlogデータが一覧表示されます。

Blogデータをv-forで展開しtableで表示
Blogデータをv-forで展開しtableで表示

Inertia.jsを利用した場合のデータの一覧表示の方法を確認することができました。

Blogデータの作成(create)

先ほどはSeederを利用してデータの挿入を行いましたがここからは入力フォームを利用してBlogデータの作成を行います。

ルーティングの追加

Blogデータの入力フォームページを表示するためのルーティングをweb.phpファイルに新たに追加します。今後更新、削除を行うためのルーティングが必要となるのでそれらのルーティングも一緒に設定を行っておきます。


Route::resource('/blogs', BlogController::class)
                ->names(['index'=>'blog.index',
                        'create' => 'blog.create',
                        'edit' => 'blog.edit',
                        'update' => 'blog.update',
                        'destroy' => 'blog.destroy',
                        'store'=>'blog.store'])
                ->middleware(['auth']);

リンクボタンの作成

Index.vueファイルに”作成”ボタンを追加し、ボタンをクリックすると作成ページに移動できるようにリンクの設定を行います。

ボタンにはLaravelのJetStreamが持つボタンコンポーネントを利用します。JetStreamで利用されているコンポーネントはresource¥js¥JetStreamの中に保存されています。ボタンのコンポーネントファイルはButtom.vueファイルです。

ボタンをコンポーネント化することは必須ではありませんが、ボタンをコンポーネント化することで統一したデザインでアプリケーションを構築することができます。またJetStreamのコンポーネントを利用することは必須ではありません。

Index.vueのtableタグの上にボタンコンポーネントのタグを追加しています。ボタンコンポーネントはJetButtonという名前でimportを行っており、templateタグ内で利用する場合はjet-buttonタグで記述します。


<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
  <div>
    <inertia-link :href="route('blog.create')">
      <jet-button class="bg-blue-700 text-base">Blogを作成</jet-button>
    </inertia-link>
  </div>
  <table>
//略
import AppLayout from "@/Layouts/AppLayout";
import JetButton from "@/Jetstream/Button";

export default {
  props: ["locations"],
  components: {
    AppLayout,
    JetButton,
  },

テーブルの上に”作成”ボタンが表示されますがリンク先のページを設定していないのでクリックしても作成ページがブラウザ上に表示されることはありません。

ブログの作成ボタンが表示されます。
ブログの作成ボタンが表示されます。

Createページの作成

Blogの入力フォームを含むCreateページの作成を行います。BlogController.phpファイルのcreateメソッド内で入力フォームを記述するBlog¥Create.vueファイルを指定します。


    public function create()
    {
        return Inertia::render('Blog/Create');
    }

resources¥js¥Pages¥BlogにあるIndex.vueを複製してCreate.vueを作成します。テーブルなど必要ない情報は削除します。


<template>
  <app-layout>
    <template #header>
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            Blog
        </h2>
    </template>
  </app-layout>
</template>

<script>
    import AppLayout from '@/Layouts/AppLayout'

    export default {
        props:['blogs'],
        components: {
            AppLayout,
        },
    }
</script>

ブラウザで確認すると以下のように表示されます。

createページ
createページ

入力フォームの追加

FormSectionコンポーネント

template #headerの閉じタグの下に入力フォームを追加しますが、入力フォームにはJetStreamのFormSectionコンポーネントを利用します。

FormSectionコンポーネントはtitle, description, form, actionsの4つの名前付きSlotがあります。

まずはtitle, descriptionスロットのみを設定しブラウザ上に表示します。


<template>
  <app-layout>
    <template #header>
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            Blog
        </h2>
    </template>
    <div>
      <div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
          <jet-form-section>
              <template #title>Blog作成</template>
              <template #description>Blogの追加を行います</template>
          </jet-form-section>
      </div>
    </div>
  </app-layout>
</template>

<script>
    import AppLayout from '@/Layouts/AppLayout'
    import JetFormSection from "@/Jetstream/FormSection";

    export default {
        props:['blogs'],
        components: {
            AppLayout,
            JetFormSection,
        },
    }
</script>

template #titleタグの中に記述した内容は、FormSection.vueファイルのslotタグのname属性にtitleを設定している場所に挿入されます。


<slot name="title"></slot>
FormSection.vueファイルにslotタグのname属性にはtitle, description, form, actionsが設定されているものが存在します。

ブラウザで確認すると設定したtitleとdescriptionが画面の左側に表示されます。

title, description表示
title, description表示

次はFormSectionコンポーネントのformスロットにBlogデータのtitleに関するlabel要素とinput要素を追加しますが、JetStreamのコンポーネントにはlabel用のLabel.vue, input用のInput.vueがあるのでそれを利用します。

template #descriptionの閉じタグの下にtemplate #formタグを追加します。


<template #form>
    <div class="col-span-6 sm:col-span-4">
    <jet-label for="title" value="タイトル" />
    <jet-input
        id="title"
        type="text"
        class="mt-1 block w-full"
        v-model="form.title"
    />
    </div>
</template>

scriptタグの中でInput.vueとLabel.vueをimportしてそれぞれの名前をJetInputとJetLabelとしています。templateタグで利用する場合は<jet-input>、<jet-label>と記述して利用します。

formプロパティ

jet-inputタグの中のv-modelでデータバインディングを行っているformプロパティはVue.jsの設定で下記のように設定します。


<script>
  import AppLayout from '@/Layouts/AppLayout'
  import JetFormSection from "@/Jetstream/FormSection";
  import JetInput from "@/Jetstream/Input";
  import JetLabel from "@/Jetstream/Label";

  export default {
    components: {
        AppLayout,
        JetFormSection,
        JetInput,
        JetLabel
    },
    data() {
      return {
        form: this.$inertia.form(
            {
              _method: "POST",
              title: "",
              description: "",
            },
            {
              bag: "blogCreate",
            }
        )
      };
    }
  }
</script>

formプロパティについてはほとんどの人は見慣れない書式だと思います。Laravel Jetsteamの公式ドキュメントに解説が記載されており、そのドキュメンを参考に設定を行っています。

Form/ Validation Helpers
Form/ Validation Helpers

フォームとバリデーションエラーを効率的に処理するためにlaravel-jetstreamというパッケージがJetStreamと一緒にインストールされています。laravel-jetsteamパッケージをインストールすることでデータプロパティのform内の処理はInertia.jsの単独の機能ではなくLaravel Jetstream用に機能追加が行われています。

$inertiaオブジェクトでformメソッドを追加し、引数にdata、optionsを設定することで新たにinertiaFormオブジェクトを作成します。


this.$inertia.form(data,options)

formの引数のdataにはメソッドの設定とルーティングに送信するデータプロパティを設定します。Create.vueではBlogデータを新規で作成を行うのでルーティングに対してPOSTメソッドを実行するため、_methodプロパティにPOSTを設定しています。送信するデータはtitleとcontentなのでtitleプロパティとcontentプロパティを設定しています。


form: this.$inertia.form(
    {
        _method: "POST",
        title: "",
        content: "",
    },

formの引数のoptionsにはbagが設定されています。


form: this.$inertia.form(
    {
        _method: "POST",
        title: "",
        description: "",
    },
    {
        bag: "blogCreate",  //ここ
    }
)

bagに設定している’blogCreate’はバリデーションでエラーが発生時にエラーを識別し、表示する際に利用します。後ほどコントローラーのバリデーションを設定する際に同じ名前を設定する必要があります。

入力フォーム

ここまでの設定で再度ページを確認するとタイトルラベルのinput要素が表示されていることがわかります。

タイトルのinput要素の表示
タイトルのinput要素の表示

コンテントについてはJetStreamにtextareaのコンポーネントが存在しないので今回はコンポーネントを利用せずに直接textareaを設定します。


<template #form>
    <div class="col-span-6 sm:col-span-4">
      <jet-label for="title" value="タイトル" />
      <jet-input
          id="title"
          type="text"
          class="mt-1 block w-full"
          v-model="form.title"
      />
    </div>
    <div class="col-span-6 sm:col-span-4">
      <jet-label for="content" value="コンテント" />
      <textarea v-model="form.content" class="mt-1 block w-full form-input rounded-md shadow-sm"></textarea>
    </div>
</template>

追加後ブラウザで確認するとタイトルとコンテンツの入力欄が表示されます。

input,textareaの入力欄
input,textareaの入力欄

入力フォームを完成するためには”作成”ボタンが必要となります。ボタンはactionsスロットの中で設定を行います。

ボタンはIndex.vueファイルで”BLOGを作成”ボタンを作成する際に利用したJetStreamのButton.vueを再利用します。

template #form閉じタグの下にtemplate #actionsを追加します。


<template #actions>
  <jet-button
    class="bg-blue-700"
  >
    作成
  </jet-button>
</template>

ブラウザで確認すると”作成”ボタンが入力フォームの右下に表示されますが作成ボタンをクリックしても何も起こりません。

作成ボタンの表示
作成ボタンの表示

submittedイベント

FormSectionコンポーネントについてはtitle, description, form, actionsの4つの名前付きSlotがあり、ここまでの流れですべてのSlotを設定しました。

FormSection.vueを開いてさらに内容を確認します。FormSection.vueの中にはformタグも含まれておりフォーム内でボタンをクリックするとsubmitイベントにより$emitが実行され引数に設定されているsubmittedイベントが親コンポーネントに送られるように設定されています。


<template>
  <div class="md:grid md:grid-cols-3 md:gap-6">
    <jet-section-title>
      <template #title><slot name="title"></slot></template>
      <template #description><slot name="description"></slot></template>
    </jet-section-title>

    <div class="mt-5 md:mt-0 md:col-span-2">
      <form @submit.prevent="$emit('submitted')"> //ここ
        <div class="shadow overflow-hidden sm:rounded-md">
          <div class="px-4 py-5 bg-white sm:p-6">
            <div class="grid grid-cols-6 gap-6">
              <slot name="form"></slot>
            </div>
          </div>

FormSection.vueコンポーネントの親コンポーネントであるCreate.vueファイルでsubmittedイベントを受け取る処理を追加します。

jet-form-sectionタグに@submittedを追加し、submittedイベントを受け取ったら、createBlogメソッドを実行するように設定を行います。


<jet-form-section @submitted="createBlog">

submittedイベントを受け取って、createBlogメソッドが実行されるのか動作確認を行うため、createBlogメソッドが実行できたらコンソールにsubmittedを表示させます。


methods:{
  createBlog(){
    console.log('submitted')
  }
}

”作成”ボタンをクリックしてブラウザのデベロッパーツールのコンソールに”submitted”が表示されたらFormSectionから送られてきたsubmittedイベントが親コンポーネントで正常に受け取れていることがわかります。

createBlogメソッドの中の処理はconsole.logからブログが登録できるルーティングへのPOSTリクエストを行う処理に変更します。

ルーティングの追加

createBlogメソッド内から送信されるPOSTリクエストの送信先はweb.phpへのルーティングに登録済なので追加作業は必要ありません。


Route::resource('/blogs', BlogController::class)
                ->names(['index'=>'blog.index',
                        'create' => 'blog.create',
                        'edit' => 'blog.edit',
                        'update' => 'blog.update',
                        'destroy' => 'blog.destroy',
                        'store'=>'blog.store']) //このルーティングを利用
                ->middleware(['auth']);

データの作成処理(store)

BlogController.phpファイルのstoreメソッドにCreate.vueのcreateBlogメソッドから送信されてくるBlogデータを作成する処理を追加します。

入力フォームで入力したデータを取り出し、Blogモデルのcreateメソッドを利用してBlogデータを新規作成しています。Blogデータ作成後はBlogの一覧画面にリダイレクトされます。


//略
use Illuminate\Support\Facades\Redirect;
//略
public function store(Request $request)
{
    $input = $request->all();

    Blog::create($input);

    return Redirect::route('blog.index');
}

コントローラー側のデータ作成処理の追加が完了したらCreate.vueファイル側でPOSTリクエストの処理を追加します。POSTリクエストの処理は、”作成”ボタンをクリックした際に実行されるcreateBlogメソッドの中に追加します。routeメソッドの引数にはルーティングで設定したnameのblog.storeを指定しています。


methods:{
  createBlog(){
  this.form.post(route("blog.store"));
  }
}

設定が完了したので実際にBlog作成ページから入力したデータが保存されるか確認を行います。

Blog作成ページのタイトルとコンテントを入力して”作成”ボタンをクリックします。

入力フォームに情報を入力
入力フォームに情報を入力

クリックするとブログ一覧のページ(Index.vue)へのリダイレクトが行われ入力したBlog情報が表示されることが確認できます。

追加されたBlog情報
追加されたBlog情報

バリデーションの追加

ここまでのstoreメソッドの設定では、タイトルとコンテンツを空欄に実行するとNot Null制約によりデータは登録できません。

Not Null制約エラー
Not Null制約エラー

データベースへのSQLの実行時ではなくデータ登録前に送信されてきたデータが正しいものなのかバリデーション機能を利用してチェックを行います。

title, contentのrequiredのバリデーションを設定しています。どちらの項目も入力が必須となります。validateWithBagメソッドでバリデーションを実行しますが、メソッドの引数にblogCreateの文字列を入れています。これがCreate.vueのthis.$inertia.formのオプションで設定したbagの値と一致します。


public function store(Request $request)
{
    $input = $request->all();

    Validator::make($input, [
        'title' => ['required'],
        'content' => ['required']
    ])->validateWithBag('blogCreate');

    Blog::create($input);

    return Redirect::route('blog.index');
}

バリデーションの追加を行ったのでタイトルだけ入力して”作成”ボタンをクリックします。

コンテント空白で作成
コンテント空白で作成

実行するとエラーの表示がない上、入力したタイトルも消えてしまいます。

エラー表示もなく入力した値も消える
エラー表示もなく入力した値も消える

まず入力した値が保持できるようにthis.$inertia.formのオプションにresetOnSucessを追加し、falseを設定します。


form: this.$inertia.form(
    {
        _method: "POST",
        title: "",
        content: "",
    },
    {
        bag: "blogCreate",
        resetOnSuccess: false,
    }
)

設定後再度タイトルのみ入力して”作成”ボタンを押してください。下記のように入力した値が保持されていればresetOnSuccessの設定は反映されています。

タイトルの値を保持
タイトルの値を保持

バリデーションエラーが表示されない理由はエラーを表示する設定を全く行っていないためです。バリデーションエラーを表示するためのコンポーネントInputError.vueがJetSteamにあるのでそれを利用します。

Create.vueファイルのscriptタグでInputError.vueファイルをimportとしてコンポーネントに登録します。


<script>
  import AppLayout from '@/Layouts/AppLayout'
  import JetFormSection from "@/Jetstream/FormSection";
  import JetInput from "@/Jetstream/Input";
  import JetLabel from "@/Jetstream/Label";
  import JetButton from "@/Jetstream/Button";
  import JetInputError from "@/Jetstream/InputError";

  export default {
    props:['blogs'],
    components: {
        AppLayout,
        JetFormSection,
        JetInput,
        JetLabel,
        JetButton,
        JetInputError,
    },

追加したJetInputErrorを各入力項目の下に追加します。form.errorの引数には各項目の名前を入れてください。


<template #form>
    <div class="col-span-6 sm:col-span-4">
      <jet-label for="title" value="タイトル" />
      <jet-input
          id="title"
          type="text"
          class="mt-1 block w-full"
          v-model="form.title"
      />
    <jet-input-error
      :message="form.error('title')"
      class="mt-2"
    />
    </div>
    <div class="col-span-6 sm:col-span-4">
      <jet-label for="content" value="コンテント" />
      <textarea v-model="form.content" class="mt-1 block w-full form-input rounded-md shadow-sm"></textarea>
      <jet-input-error
        :message="form.error('content')"
        class="mt-2"
      />
    </div>
</template>

これでバリデーションエラー表示の設定は完了です。何も入れずに”作成”ボタンを押すとエラーが表示されます。

バリデーションエラー表示
バリデーションエラー表示

Blogデータの削除

Blogデータの作成方法が確認できたので作成したデータの削除方法を確認します。Index.vueファイルのブログ一覧の各行に削除ボタンを追加します。


<table class="table-fixed">
    <thead>
    <tr>
        <th class="w-3/12">タイトル</th>
        <th class="w-7/12">コンテンツ</th>
        <th class="w-2/12">削除</th>
    </tr>
    </thead>
    <tbody>
        <tr v-for="blog in blogs" :key="blog.id">
            <td class="border px-4 py-2">{{ blog.title }}</td>
            <td class="border px-4 py-2">{{ blog.content }}</td>
            <td class="border px-4 py-2 text-center">
              <jet-button class="bg-red-700 text-base">削除</jet-button>
            </td>
        </tr>
    </tbody>
</table>
テーブルはtailwindcssのtable-fixedで固定し、w-X/12で列の幅設定を行っています。

ブラウザで確認すると各行に削除ボタンが追加されます。

削除ボタンの追加
削除ボタンの追加

”削除”ボタンをクリックすると削除処理が実行されるように新たにクリックイベントとdeleteBlogメソッドを追加します。引数には削除する行を識別できるようにblog.idを設定しています。


<jet-button class="bg-red-700 text-base" @click.native="deleteBlog(blog.id)">削除</jet-button>

動作確認のためdeleteBlogメソッドではコンソールにクリックした行のblogのidを表示します。


methods:{
    deleteBlog(id){
     console.log(id);
    }
}

設定後、”削除”ボタンをクリックするとブラウザのデベロッパーツールのコンソールにクリックしたBlogデータのidが表示されます。

ルーティング

Blogデータを削除するためにはweb.phpへ削除処理を実行するルーティングを追加する必要がありますが、すでに設定済なのでルーティングの追加作業はありません。


Route::resource('/blogs', BlogController::class)
                ->names(['index'=>'blog.index',
                        'create' => 'blog.create',
                        'edit' => 'blog.edit',
                        'update' => 'blog.update',
                        'destroy' => 'blog.destroy',//ここ
                        'store'=>'blog.store']) 
                ->middleware(['auth']);

データの削除処理(destroy)

データの削除処理をBlogController.phpファイルのdestroyメソッドに追加します。

Index.vueファイルからDELETEメソッドでBlogのidが送信されてきますが、モデルバインディングによりidを持つ$blogデータが取得できるため、deleteメソッドを利用して削除を行っています。削除後はBlog一覧の画面にリダイレクトされます。


public function destroy(Blog $blog)
{
    $blog->delete();

    return Redirect::route('blog.index');
}

BlogController.phpファイルで削除処理の追加が完了したら、Index.vueファイル側で削除のPOSTリクエストの追加を行います。

データ作成時と同様にthis.$inertia.formを利用します。今回は削除を行うので_methodにはDELETEを指定します。完了後、”削除”ボタンをクリックすると”削除”ボタンを押した行が削除されます。


export default {
  props:['blogs'],
  components: {
    AppLayout,
    JetButton,
  },
  data() {
    return {
      form: this.$inertia.form(
        {
          _method: "DELETE",
        }
      ),
    };
  },
  methods:{
    deleteBlog(id){
      this.form.post(route("blog.destroy", id), {
        preserveScroll: true,
      });
    }
  }
}

preserveScrollがtrueと設定されていますが、これを設定しない場合画面をスクロールして削除ボタンを押すと削除が完了してリダイレクトした際にページの上部からブラウザに表示されます。trueに設定した場合は、リダイレクトした際にページの上部から表示されるのではなく削除ボタンをクリックした時に表示されていた画面が表示されます。

Blogデータの更新処理(edit, update)

データの作成、削除方法が確認できたので次は作成したデータの更新処理を行う方法を確認します。

Index.vueファイルのブログ一覧の各行の”削除”ボタンの横に”更新”ボタンを追加します。


<table class="table-fixed">
    <thead>
    <tr>
        <th class="w-3/12">タイトル</th>
        <th class="w-5/12">コンテンツ</th>
        <th class="w-2/12">更新</th>
        <th class="w-2/12">削除</th>
    </tr>
    </thead>
    <tbody>
        <tr v-for="blog in blogs" :key="blog.id">
            <td class="border px-4 py-2">{{ blog.title }}</td>
            <td class="border px-4 py-2">{{ blog.content }}</td>
            <td class="border px-4 py-2 text-center">
              <jet-button class="bg-green-500 text-base">更新</jet-button>
            </td>
            <td class="border px-4 py-2 text-center">
              <jet-button class="bg-red-700 text-base" @click.native="deleteBlog(blog.id)">削除</jet-button>
            </td>
        </tr>
    </tbody>
</table>

ブラウザで確認すると更新ボタンが表示されます。

更新ボタンの追加
更新ボタンの追加

更新ボタンをクリックすると更新ページに移動する必要があるため、更新ボタンにリンクを設定します。


<inertia-link :href="route('blog.edit', blog.id)">
    <jet-button class="bg-green-500 text-base">更新</jet-button>
</inertia-link>

blog.editのルーティングは設定済ですがBlogController.phpファイルにeditメソッドが何も記述されていないため更新ボタンをクリックしても移動することはできません。

Edit.vueファイルの作成

更新ボタンをクリックするとBlogの更新ページを表示する必要があります。更新ページはIndex.vueファイルを複製してEdit.vueファイルとします。

Edit.vueファイルが作成できたら、BlogController.phpファイルのeditメソッドからEdit.vueファイルを表示できるように下記の設定を行います。更新を行いたいBlogデータは更新ボタンのリンクで渡されたidとモデルバインディングを利用して取得しています。また取得したデータ$blogをEdit.vueに渡しています。


public function edit(Blog $blog){
    return Inertia::render('Blog/Edit',['blog' => $blog]);
}

Edit.vueファイルを開いてtitleとdescriptionを変更します。


<template #title>Blog更新</template>
<template #description>Blogの更新を行います</template>

変更後、ブログ一覧にある”更新”ボタンをクリックし下記の画面が表示されればここまでの設定に問題はありません。

更新ページへのアクセス
更新ページへのアクセス

propsの設定

変数を$blogが渡されていますが、タイトル、コンテンツには何も表示されていません。コントローラーから渡されたデータはvueファイルではpropsを使って取得します。

propsで取得したblogはthis.$inertia.formのデータプロパティの初期値として設定を行います。また_methodは更新なのでPUTを設定しています。


  export default {
    props:['blog'],
    components: {
        //略
    },
    data() {
      return {
        form: this.$inertia.form(
            {
              _method: "PUT",
              title: this.blog.title,
              content: this.blog.content,
            },
            {
              bag: "blogUpdate",
              resetOnSuccess: false,
            }
        )
      }
    },

この設定が完了後、再度ブラウザで確認するとタイトルとコンテンツに既存のデータが表示されます。

タイトルとコンテンツに既存データ
タイトルとコンテンツに既存データ

update処理

”作成”ボタンの名前を”更新”ボタンに変更します。


<template #actions>
    <jet-button
        class="bg-blue-700"
    >
        更新
    </jet-button>
</template>

更新ボタンをクリックすると作成と同様にFormSection.vueからsubmittedイベントが送られてきました。submittedイベントを受け取った後に実行するメソッドをupdateBlogに変更します。変更はjet-form-sectionタグで行います。


<jet-form-section @submitted="editBlog">

editBlogメソッドで実行しているpostリクエストの送信先もblog.storeからblog.updateに変更します。


methods:{
    editBlog(){
      this.form.post(route("blog.update",this.blog.id));
    }
}

blog.updateのルーティングは設定済ですが、BlogCotroller.phpのupdateメソッドでは何も行っていないのでブラウザ上で更新ボタンをクリックしても何も起こりません。

BlogController.phpのupdateメソッドで更新処理を追加します。先ほど設定したstoreメソッドとほぼ同じ内容です。validateWithBagメソッドの引数のblogUpdateはEdit.vueファイルのthis.$inertia.formのbagオプションの値と同じにする必要があります。


public function update(Request $request, Blog $blog)
{
    $input = $request->all();

    Validator::make($input, [
        'title' => ['required'],
        'content' => ['required']
    ])->validateWithBag('blogUpdate');

    $blog->update($input);

    return Redirect::route('blog.index');
}

設定後、更新が行えることを確認してください。更新が行えることが確認できたら、タイトルからコンテンツの値を削除して空にして、エラーメッセージが表示されるのか確認を行ってください。

バリデーションエラーの表示
バリデーションエラーの表示

更新ができ、エラーメッセージが表示されれば更新機能の設定は完了です。

ここまでの設定が完了できればInertia.jsでCRUDを実装するためにはどのような処理が必要になるのか理解できたかと思います。