Laravelのバージョン8から登場したInertia.jsを利用してデータの一覧表示、データ作成、更新、削除をどのように行うのか知りたいという人もいるかと思います。新しい技術にはまだ手を出したくないと尻込みしている人もInertia.jsの基本的なCRUDの処理方法が理解できればInertia.jsを使ったアプリケーション作成にチャレンジしてみようと思うかもしれません。ぜひ本書を利用してInertia.jsのCRUD操作を学び今後Inertia.jsの技術を習得するべきかどうかの判断に利用してください。

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

最新バージョンのLaravel10を利用して動作確認を行っています(初回公開時はLaravel8)

環境構築

Laravelプロジェクトの作成

動作確認はmacOSを利用して行っています。

Laravelをインストール時に一緒にInertia.jsをインストールを行いたい場合はlaravel newコマンドに–jetオプションを付与して実行します。ここではLaravelのプロジェクト名をlaravel_jetsteam_inertiaにしていますが、任意の名前をつけてください。実行後inertiaかlivewireの選択が出てくるのでinetiaを選択してください。


 % laravel new laravel_jetstream_inertia --jet


    |     |         |
    |,---.|--- ,---.|--- ,---.,---.,---.,-.-.
    ||---'|    `---.|    |    |---',---|| | |
`---'`---'`---'`---'`---'`    `---'`---^` ' '


Which Jetstream stack do you prefer?
  [0] livewire
  [1] inertia

teamのインストールも確認されますが、どちらを選択しても構いませんが本文書では”no”を選択して進めています。


 Will your application use teams? (yes/no) [no]:

“no”を選択するとLaravelのインストールが開始されます。–jetオプションをつけてインストールした場合はJavaScriptライブラリのインストールとビルドまで実行してくれます。

Laravelインストール後にJetstreamをインストールすることも可能です。その場合はJetstreamパッケージをインストールしてphp artisan inertiaコマンドを実行します。


% composer require laravel/jetstream
% php artisan jetstream:install inertia

Jetstreamインストール後にnpm install && npm run devを実行するようにメッセージが表示されるので実行してください。

Laravelインストール時にJetstreamをインストールした場合もLaravelインストール後にJetstremをインストールした場合もphp artisan serveコマンドを実行してブラウザでアクセスすると以下のエラーが表示されます。

接続時のエラーメッセージ
接続時のエラーメッセージ

SQLSTATEで”Connection refused”と表示されていますがデータベースへの接続が拒否されたわけではなくデータベースが存在しないために表示されているエラーです。JetstreamのInertia.jsを利用するためにはデータベースの作成が必須となります。

データベースの作成

本文書では簡易的なsqliteデータベースを利用します。.envファイルのDBに関する環境変数を変更します。

.envファイルではDB_CONNECTIONをmysqlからsqliteに変更し、その他のDBに関連する環境変数を削除します。削除する環境変数は先頭にDB_がついているものです。


DB_CONNECTION=sqlite

これでデータベースの設定が完了できたので、php artisan migrateコマンドを実行してください。SQLiteはファイルベースのデータベースなのでファイルが存在しない場合には警告が表示され、ファイルを作成するか聞かれるので”Yes”を選択します。実行が完了すると6つのテーブルが作成されます。


 % php artisan migrate

   WARN  The SQLite database does not exist: database/database.sqlite.  

 ┌ Would you like to create it? ────────────────────────────────┐
 │ Yes                                                          │
 └──────────────────────────────────────────────────────────────┘

   INFO  Preparing database.  

  Creating migration table ................................. 8ms DONE

   INFO  Running migrations.  

  2014_10_12_000000_create_users_table ...................... 3ms DONE
  2014_10_12_100000_create_password_reset_tokens_table ...... 1ms DONE
  2014_10_12_200000_add_two_factor_columns_to_users_table ... 3ms DONE
  2019_08_19_000000_create_failed_jobs_table ................ 2ms DONE
  2019_12_14_000001_create_personal_access_tokens_table ..... 3ms DONE
  2024_01_17_041021_create_sessions_table ................... 2ms DONE

php artisan serveコマンドで開発サーバを起動し、アクセスを行うと初期画面が表示されます。

Laravel10のトップ画面
Laravel10のトップ画面

ここまでで動作確認を行うためのLaravel環境の構築は完了です。インストールしたLaravelのバージョンはphp artisan -Vコマンドで確認することができます。


% php artisan -V
Laravel Framework 10.41.0

Inertia.jsで利用するVue.jsのバージョンはpackage.jsonファイルから確認することができます。その他にもtailwindcssやaxiosなども確認することができます。


//略
    "devDependencies": {
        "@inertiajs/vue3": "^1.0.0",
        "@tailwindcss/forms": "^0.5.2",
        "@tailwindcss/typography": "^0.5.2",
        "@vitejs/plugin-vue": "^4.5.0",
        "autoprefixer": "^10.4.7",
        "axios": "^1.6.4",
        "laravel-vite-plugin": "^1.0.0",
        "postcss": "^8.4.14",
        "tailwindcss": "^3.1.0",
        "vite": "^5.0.0",
        "vue": "^3.2.31"
    }
}

Blog用のモデルの設定

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

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

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


% php artisan make:model Blog -m 
   INFO  Model [app/Models/Blog.php] created successfully.  

   INFO  Migration [database/migrations/2024_01_17_072541_create_blogs_table.php] created successfully.  

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


% php artisan make:controller BlogController --resource

   INFO  Controller [app/Http/Controllers/BlogController.php] created successfully.  

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

php artisan make:model Blogにオプショ-aをつけて場合はコントローラーファイルも一度に作成することができます。
fukidashi

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

   INFO  Running migrations.  

  2024_01_17_072541_create_blogs_table .............. 6ms DONE

テーブル作成後テーブルへのデータ登録ができるようにapp¥Models¥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ファイル側でルーティングの変更を行う必要がありません。
fukidashi

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

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


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

ミドルウェアのauthを設定して再度/blogsにアクセスするとログイン画面が表示されます。

Jetstreamでのログイン画面
Jetstreamでのログイン画面

ユーザの登録が完了していない場合はユーザ登録を行ってください。ユーザの登録・ログインは初期画面の右上から行うことができます。

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

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

Laravel Jetstream ダッシュボード画面
Laravel Jetstream ダッシュボード画面

リンクの設定

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


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

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


<template>
    <AppLayout title="Dashboard">
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Dashboard
            </h2>
        </template>
CSSにTailwind CSSが利用されています。
fukidashi

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


<!-- Navigation Links -->
<div
    class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"
>
    <NavLink
        :href="route('dashboard')"
        :active="route().current('dashboard')"
    >
        Dashboard
    </NavLink>
</div>
<div
    class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"
>
    <NavLink
        :href="route('blog.index')"
        :active="route().current('blog.index')"
    >
        Blog
    </NavLink>
</div>

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

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

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に変更してください。


<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
</script>

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

/blogsにアクセスすると作成したBlog/Index.vueファイルの内容が表示されます。

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

Seedingによるデータの挿入

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

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

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


 % php artisan make:factory BlogFactory --model=Blog

   INFO  Factory [database/factories/BlogFactory.php] 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

   INFO  Seeding database.  

データが挿入されたかどうかphp artisan tinkerを利用して確認することができます。


% php artisan tinker
Psy Shell v0.12.0 (PHP 8.3.1 — cli) by Justin Hileman
>App\Models\Blog::all()
=> Illuminate\Database\Eloquent\Collection {#4560
     all: [
       App\Models\Blog {#4562
         id: "1",
         title: "Impedit sed perferendis explicabo.",
//略

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

Blogデータの一覧表示

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

BlogController.phpのindexメソッド内でBlogモデルを利用してblogsテーブルに保存された全データを取得します。取得したデータはrenderメソッドの第二引数を利用してvueファイルに渡すことができます。


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

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

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


<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
defineProps({
    blogs: Array,
});
</script>

<template>
    <AppLayout title="Blog">
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Blog
            </h2>
        </template>
        <div class="py-12">
            <div class="max-w-7xl mx-auto 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>
    </AppLayout>
</template>

Inertia.jsを利用しない場合はライフサイクルフックcreatedまたはmountedの中でaxios, fetchを利用してblogsのデータをLaravelに設定したエンドポイントから取得する必要がありますがInertia.jsではコントローラーで取得したデータをpropsを利用してIndex.vueファイルに渡します。
fukidashi

ブラウザで確認するとテーブルに保存されている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¥Componentsの中に保存されています。ボタンのコンポーネントファイルは2つありPrimaryButton.vueとSecondaryButton.vueファイルです。

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

Index.vueのtableタグの上にボタンコンポーネントのタグを追加しています。ボタンコンポーネントはPrimaryButtonという名前でimportを行っています。PrimaryButtonはpropsのtypeでボタンのタイプを設定することができ、デフォルトはsubmitなのでbuttonを設定しています。


<script setup>
import { Link } from "@inertiajs/vue3";
import AppLayout from "@/Layouts/AppLayout.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
defineProps({
    blogs: Array,
});
</script>

<template>
    <AppLayout title="Blog">
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Blog
            </h2>
        </template>
        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <Link :href="route('blog.create')">
                    <PrimaryButton type="button"
                        >Blogを作成</PrimaryButton
                    >
                </Link>
                <table>
//略

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

PrimaryButtonコンポーネントの確認
PrimaryButtonコンポーネントの確認

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を作成します。テーブルなど必要ない情報は削除します。


<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
</script>

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

ブログページから”BLOGを作成”ボタンをクリックすると以下の画面が表示されます。

createページ
createページ

入力フォームの追加

FormSectionコンポーネント

template #headerの閉じタグの下に入力フォームを追加しますがComponentsディレクトリにあるFormSectionコンポーネントを利用します。

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

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


<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import FormSection from "@/Components/FormSection.vue";
</script>

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

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


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

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

title, description表示
title, description表示

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

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


<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import FormSection from "@/Components/FormSection.vue";
import InputLabel from "@/Components/InputLabel.vue";
import TextInput from "@/Components/TextInput.vue";
</script>
//略
<FormSection>
    <template #title>Blog作成</template>
    <template #description>Blogの追加を行います</template>
    <template #form>
        <div class="col-span-6 sm:col-span-4">
            <InputLabel for="title" value="タイトル" />
            <TextInput
                id="title"
                type="text"
                class="mt-1 block w-full"
            />
        </div>
    </template>
</FormSection>
//略

formプロパティ

フォームの処理についてはアプリケーションを構築する際に必須な処理なので簡単にフォームの処理が行えるようにForm Helpderが準備されています。

TextInputタグにv-modelディレクティブを追加してuseForm Hookの引数にtitle, contentを指定して変数formに保存します。v-modelにはform.titleを設定します。


<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import FormSection from "@/Components/FormSection.vue";
import InputLabel from "@/Components/InputLabel.vue";
import TextInput from "@/Components/TextInput.vue";
import { useForm } from "@inertiajs/vue3";

const form = useForm({
    title: "",
});
</script>
//略
                        <TextInput
                            id="title"
                            type="text"
                            class="mt-1 block w-full"
                            v-model="form.title"
                        />
//略

useFromについてはinertia.jsのドキュメントに記載されているのでその内容を参考に設定していきます。

入力フォーム

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

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

BlogモデルのcontentについてはComponentディレクトリにtextareaに関するコンポーネントが存在しないので今回はコンポーネントを利用せずに直接textarea要素を設定します。


//略
const form = useForm({
    title: "",
    content: "",
});
//略
<template #title>Blog作成</template>
<template #description>Blogの追加を行います</template>
<template #form>
    <div class="col-span-6 sm:col-span-4">
        <InputLabel for="title" value="タイトル" />
        <TextInput
            id="title"
            type="text"
            class="mt-1 block w-full"
            v-model="form.title"
        />
    </div>
    <div class="col-span-6 sm:col-span-4">
        <InputLabel for="content" value="コンテント" />
        <textarea
            class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
            v-model="form.content"
        ></textarea>
    </div>
</template>

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

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

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

ボタンはIndex.vueファイルで”BLOGを作成”ボタンを作成する際に利用したComponentsディレクトリののPrimaryButton.vueを再利用します。

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


<script setup>
//略
import JetButton from "@/Jetstream/Button";
//略
</script>
//略
<template #actions>
    <PrimaryButton > 作成 </PrimaryButton>
</template>

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

フォームにボタンを追加
フォームにボタンを追加

submittedイベント

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

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


<script setup>
import { computed, useSlots } from 'vue';
import SectionTitle from './SectionTitle.vue';

defineEmits(['submitted']);

const hasActions = computed(() => !! useSlots().actions);
</script>

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

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

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

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


<FormSection @submitted="createBlog">

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


const 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の一覧画面にリダイレクトされます。


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を指定しています。


const createBlog = () => {
    form.post(route("blog.store"));
};

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

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

入力フォームへの文字列の入力
入力フォームへの文字列の入力

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

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

Inertia.jsを利用してデータを追加することができるようになりました。

バリデーションの追加

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

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

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

title, contentのrequiredのバリデーションを設定しています。


public function store(Request $request)
{

    $validated = $request->validate([
        'title' => 'required',
        'content' => 'required',
    ]);

    Blog::create($validated);

    return redirect()->route('blog.index');
}

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

入力フォームにタイトルのみ入力
入力フォームにタイトルのみ入力

実行すると先ほどのようにエラーメッセージが画面上に表示させることはなくなりましたが何も変化がありません。

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

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


import JetInputError from "@/Components/InputError";

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


<template #form>
    <div class="col-span-6 sm:col-span-4">
        <InputLabel for="title" value="タイトル" />
        <TextInput
            id="title"
            type="text"
            class="mt-1 block w-full"
            v-model="form.title"
        />
        <InputError :message="form.errors.title" class="mt-2" />
    </div>
    <div class="col-span-6 sm:col-span-4">
        <InputLabel for="content" value="コンテント" />
        <textarea
            class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
            v-model="form.content"
        ></textarea>
        <InputError
            :message="form.errors.content"
            class="mt-2"
        />
    </div>
</template>

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

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

Inetial.jsでのバリデーションエラーの表示方法も確認することができました。

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">
                <PrimaryButton type="button">削除</PrimaryButton>
            </td>
        </tr>
    </tbody>
</table>
テーブルはTailwind CSSのtable-fixedで固定し、w-X/12で列の幅設定を行っています。ボ
fukidashi

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

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

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


<PrimaryButton
    type="button"
    @click="deleteBlog(blog.id)"
    >削除</PrimaryButton
>

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


const 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ファイル側で削除のdeleteリクエストの追加を行います。

データ作成時と同様Helper Formを利用します。


import AppLayout from "@/Layouts/AppLayout.vue";
import { Link } from "@inertiajs/inertia-vue3";
import JetButton from "@/Jetstream/Button.vue";
import { useForm } from "@inertiajs/vue3";

const form = useForm({});

defineProps({
    blogs: Array,
});

const deleteBlog = (id) => {
    form.delete(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">
                <PrimaryButton type="button">更新</PrimaryButton>
            </td>
            <td class="border px-4 py-2 text-center">
                <PrimaryButton
                    type="button"
                    @click="deleteBlog(blog.id)"
                    >削除</PrimaryButton
                >
            </td>
        </tr>
    </tbody>
</table>

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

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

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


<td class="border px-4 py-2 text-center">
    <Link :href="route('blog.edit', blog.id)">
        <PrimaryButton type="button"
            >更新</PrimaryButton
        >
    </Link>
</td>

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

Edit.vueファイルの作成

更新ボタンをクリックするとBlogの更新ページを表示する必要があります。更新ページはBlog/Create.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はuseForm Hookの引数の初期値として設定を行います。


const props = defineProps({
    blog: Object,
});

const form = useForm({
    title: props.blog.title,
    content: props.blog.content,
});

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

タイトル、コンテンツを既存のデータで表示
タイトル、コンテンツを既存のデータで表示

update処理

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


<template #actions>
    <PrimaryButton> 更新 </PrimaryButton>
</template>

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


<FormSection @submitted="editBlog">

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


const editBlog = () => {
    form.patch(route("blog.update", props.blog.id));
};

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

BlogController.phpのupdateメソッドで更新処理を追加します。先ほど設定したstoreメソッドとほぼ同じ内容です。


public function update(Request $request, Blog $blog)
{
        $validated = $request->validate([
            'title' => 'required',
            'content' => 'required',
        ]);

        $blog->update($validated);
    
        return redirect()->route('blog.index');
}

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

バリデーションエラーの動作確認
バリデーションエラーの動作確認

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

JetStreamを利用してInertia.jsを設定していることもありJetstream用に事前に作成されたコンポーネントを再利用利る箇所が複数ありましたがInertia.jsを利用したCRUDの設定方法を理解できたのではないでしょうか。useFormなどのHelper関数も利用することができるためフォーム処理もそれほど難しくないことがわかります。

今後Laravelでアプリケーションを構築する際にはぜひInertia.jsでのフロントエンド開発も候補にいれてみてはいかがでしょうか。