Inertia.jsでLaravel9 + Vue3環境を手順を追って構築する

# はじめに

先日、[Laravel SailでLaravel 9 (PHP8.1 + node.js 16.x) 環境をサクッと立ち上げる](https://qiita.com/uehatsu/items/63d66d7a37510c6ff1be)という投稿をしました。まとめで「この環境を使って、Laravel Inertiaについて次は書いてみたいなと考えています。」と書いたので、Inertiaについてまとめたいと思います。

# 環境

- ホスト
    - MacBook Pro (16インチ, 2019)
    - macOS Monterey 12.2.1
    - Docker Desktop 4.5.0
    - PHP 8.1.3
    - Composer 2.2.7
- ゲスト
    - Laravel Sail
    - PHP 8.1.3
    - node.js 16.4.0
    - MySQL 8.0.x

# 構築

## 1. Laravelプロジェクトの作成

composerでcreate-projectします。Laravel-CLIが好きな方はそちらで。

```terminal
composer create-project laravel/laravel laravel-inertia-sample
cd laravel-inertia-sample
```

## 2. Laravel Sail

詳しくは以前の投稿（[Laravel SailでLaravel 9 (PHP8.1 + node.js 16.x) 環境をサクッと立ち上げる](https://qiita.com/uehatsu/items/63d66d7a37510c6ff1be)）をご覧ください。

:::note info
実行コマンド
Laravel Sailに則した実行コマンドの記述をしています。Sailを使わない方は適宜読み替えて下さい。
:::

Laravel Sailで開発環境を起動します。

```terminal
sail up
```

## 3. Vue3環境の準備

必要なnode.jsのコンポーネントをインストールします。

```terminal
sail npm install
sail npm install vue@next @vue/compiler-sfc
```

webpack.mix.jsを変更して、Vue3をコンパイル出来るように、Laravel Mixの設定を変更します。

```javascript:./webpack.mix.js
const mix = require('laravel-mix');

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel applications. By default, we are compiling the CSS
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/js/app.js', 'public/js')
    .vue() // ← 追加
    .postCss('resources/css/app.css', 'public/css', [
        //
    ]);

```

`welcome.blade.php`を`app.blade.php`とリネームし、以下のように中身を書き換えます。

```php:./resources/views/app.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Laravel</title>
        <script src="{{ asset('js/app.js') }}" defer="defer"></script>
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>
```

Webルートを書き換えます。

```php:./routes/web.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('app');
});
```

次に`app.js`を書き換えます。

```javascript:./resources/js/app.js
import { createApp } from 'vue'
import TestVue from './Components/Test.vue';

const app = createApp(TestVue)
app.mount('#app')
```

最後にVueファイルを作成します。

```vue:./resources/js/Components/Test.vue
<template>
    <div>Laravel + Vue3</div>
</template>

<script>
export default {
    name: "Test"
}
</script>
```

`app.js`をコンパイルします。

初めてnpm run devをすると、vue-loaderがインストールされるので、再度実行します。そうすると`app.js`と`app.css`が正しくコンパイルされた旨のメッセージがでます。

```terminal
sail npm run dev
sail npm run dev
```

http://localhost/ にアクセスして"Laravel + Vue3"と表示されていればOKです。

## 4. Inertiaのセットアップ

[Inertiaの公式ページ](https://inertiajs.com/)を参考に進めます。まずはサーバサイドです。LaravelとRailsでの表記がありますので、もちろんLaravelのインストール方法を参考にします。

```terminal
sail composer require inertiajs/inertia-laravel
```

`app.blade.php`を以下のように書き換えます。

```php:./resources/views/app.blade.php
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <link href="{{ mix('/css/app.css') }}" rel="stylesheet" />
    <script src="{{ mix('/js/app.js') }}" defer></script>
    @inertiaHead
  </head>
  <body>
    @inertia
  </body>
</html>
```

ミドルウェアの設定を行います。

```terminal
sail artisan inertia:middleware
```

```php:./app/Http/Kernel.php
    /**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,

            \App\Http\Middleware\HandleInertiaRequests::class, // ← 追加
        ],

        'api' => [
            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];
```

モデルを作成します。

```terminal
sail artisan make:model --controller --factory --migration --seed Book
```

```php:./app/Models/Book.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'author',
    ];
}
```

```php:./database/migrations/YYYY_MM_DD_hhmmss_create_book_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('author');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('books');
    }
};
```

```php:./database/factories/BookFactory.php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Book>
 */
class BookFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'title' => $this->faker->words(asText: true),
            'author' => $this->faker->name(),
        ];
    }
}
```

```php:./database/seeders/BookSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Book;
use Illuminate\Database\Seeder;

class BookSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Book::factory(10)->create();
    }
}
```

```php:./database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            BookSeeder::class,
        ]);
    }
}
```

マイグレーションとシーディングを行います。

```terminal
sail artisan migrate --seed 
```

レスポンスを作成します。

```php:./app/Http/Controllers/BookController.php
<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Inertia\Inertia;
use Inertia\Response;

class BookController extends Controller
{
    /**
     * @return Response
     */
    public function index(): Response
    {
        return Inertia::render('Index', [
            'books' => Book::all([
                'id',
                'title',
                'author',
            ]),
        ]);
    }
}
```

続いて、クライアントサイド（Vue3側）の準備をしていきます。

```terminal
sail npm install @inertiajs/inertia @inertiajs/inertia-vue3
```

`app.js`を以下のように書き換えます。

```javascript:./resources/js/app.js
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/inertia-vue3'

createInertiaApp({
  resolve: name => require(`./Pages/${name}`),
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el)
  },
})
```

ついでにプログレスインジケーターも設定してしまいましょう。

```terminal
sail npm install @inertiajs/progress
```

```javascript:./resorces/js/app.js
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/inertia-vue3'
import { InertiaProgress } from '@inertiajs/progress'

createInertiaApp({
    resolve: name => require(`./Pages/${name}`),
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el)
    },
})
InertiaProgress.init()
```

Vueファイルを作成します。

```Vue:./resorces/js/Pages/Index.vue
<template>
    <ul>
        <li v-for="book in books" v-text="book.title"></li>
    </ul>
</template>

<script>
export default {
    props: {
        books: Array,
    },
}
</script>
```

Webルートの設定を変更します。

```php:./routes/web.php
<?php

use App\Http\Controllers\BookController;
use Illuminate\Support\Facades\Route;

Route::get('/', [BookController::class, 'index']);
```

リコンパイルします。変更をwatchしておきたいので、ここでは`run dev`ではなく`run watch`を使います。終了は`Ctrl+C`です。

```terminal
sail npm run watch
```

http://localhost/ にアクセスします。10個の本のタイトル（ランダムなword列）が見えたらOKです。

## 5. BookのCRUDを実装する

現状では一覧のみしか出来ないので、最低限のCRUDを実装します。「一覧画面」「詳細画面＋削除」「新規作成画面」「修正画面」の４画面の構成とします。

まずはルートと画面を準備します。

Webルートを以下のように修正します。

```php:./routes/web.php
<?php

use App\Http\Controllers\BookController;
use Illuminate\Support\Facades\Route;

Route::inertia('/', 'Index');

Route::resource('/books', BookController::class);
```

Vueファイルを準備します。

```vue:./resources/js/Pages/Books/Index.vue
<template>
    <ul>
        <li v-for="book in books" v-text="book.title"></li>
    </ul>
</template>

<script>
export default {
    props: {
        books: Array,
    },
}
</script>
```

```vue:./resources/js/Pages/Books/Show.vue
<template>
    <div>Show</div>
</template>

<script>
export default {
}
</script>
```

```vue:./resources/js/Pages/Books/Create.vue
<template>
    <div>Create</div>
</template>

<script>
export default {
}
</script>
```

```vue:./resources/js/Pages/Books/Edit.vue
<template>
    <div>Edit</div>
</template>

<script>
export default {
}
</script>
```

コントローラーを修正します。

```php:./app/Http/Controllers/BookController.php
<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Inertia\Inertia;
use Inertia\Response;

class BookController extends Controller
{
    /**
     * @return Response
     */
    public function index(): Response
    {
        return Inertia::render('Books/Index', [
            'books' => Book::all([
                'id',
                'title',
                'author',
            ]),
        ]);
    }

    /**
     * @param Book $book
     * @return Response
     */
    public function show(Book $book): Response
    {
        return Inertia::render('Books/Show');
    }

    /**
     * @return Response
     */
    public function create(): Response
    {
        return Inertia::render('Books/Create');
    }

    /**
     * @param Book $book
     * @return Response
     */
    public function edit(Book $book): Response
    {
        return Inertia::render('Books/Edit');
    }
}
```

この状態で、以下のURLにアクセスし正しく表示されるか確認します。

- http://localhost/books
    - 先ほどの http://localhost/ と同じようにタイトルが１０個並んでいる
- http://localhost/books/1
    - "Show"と表示されている
- http://localhost/books/create
    - "Create"と表示されている
- http://localhost/books/1/edit
    - "Edit"と表示されている

トップページにリンクを貼ります。

```vue:./resources/js/Pages/Index.vue
<template>
    <nav>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/books">Books</a></li>
        </ul>
    </nav>
</template>

<script>
export default {
}
</script>
```

この状態でトップページ http://localhost/ からリンクをクリックすると、ページが遷移しますがページのリフレッシュがかかっていて、SPAになっていない事が分かります。以下のようにLinkコンポーネントを使って修正します。

```vue:./resources/js/Pages/Index.vue
<template>
    <nav>
        <ul>
            <li><Link href="/">Home</Link></li>
            <li><Link href="/books">Books</Link></li>
        </ul>
    </nav>
</template>

<script>
import { Link } from '@inertiajs/inertia-vue3';

export default {
    components: { Link },
}
</script>
```

この状態でリンクをクリックすると、ページのリフレッシュはかからずにページ遷移する事を確認出来ます。

このナビゲーションを別のコンポーネント(Components/Nav.vue)にまとめて、全てのページで利用できるようにします。

```vue:./resources/js/Components/Nav.vue
<template>
    <nav>
        <ul>
            <li><Link href="/">Home</Link></li>
            <li><Link href="/books">Books</Link></li>
        </ul>
    </nav>
</template>

<script>
import { Link } from '@inertiajs/inertia-vue3';

export default {
    components: { Link },
}
</script>
```

```vue:./resources/js/Pages/Index.vue
<template>
    <Nav />
</template>

<script>
import Nav from '../Components/Nav';

export default {
    components: { Nav },
}
</script>
```

Navコンポーネントは全体で利用するので、レイアウトを利用します。まずは`Components/Layout`を作成します。

```vue:./resources/js/Components/Layout.vue
<template>
    <Nav />

    <slot />
</template>

<script>
import Nav from './Nav';

export default {
    components: { Nav },
}
</script>
```

`app.js`を修正します。

```javascript:./resources/js/app.js
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/inertia-vue3'
import { InertiaProgress } from '@inertiajs/progress'
import Layout from "./Components/Layout";

createInertiaApp({
    resolve: name => {
        let page = require(`./Pages/${name}`).default;

        page.layout ??= Layout;

        return page;
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el)
    },
})
InertiaProgress.init()
```

Vueファイルを修正します。

```vue:./resources/js/Pages/Index.vue
<template>
</template>
```

この状態で先ほどのURLにそれぞれアクセスして問題無い事を確認しておきます。

では、CRUDを実装していきます。booksのトップページに詳細ページへのリンクと、新規作成ページへのリンク、それからページネーションを追加します。

```vue:./resources/js/Pages/Books/Index.vue
<template>
    <ul>
        <li v-for="book in books.data" key="book.id">
            <Link :href="`/books/${book.id}`" v-text="book.title"/>
        </li>
    </ul>

    <div>
        <ul>
            <li v-for="link in books.links">
                <Link
                    v-if="link.url"
                    :href="link.url"
                    v-html="link.label"
                />
                <span
                    v-if="!link.url"
                    v-html="link.label"
                />
            </li>
        </ul>
    </div>

    <div>
        <Link href="/books/create">Create</Link>
    </div>
</template>

<script>
import Layout from '../../Components/Layout';
import { Link } from '@inertiajs/inertia-vue3';

export default {
    components: { Layout, Link },
    props: {
        books: Object,
    },
}
</script>
```

このままでは動作しないのでコントローラーを以下のように修正します。

```php:./app/Http/Controllers/BookController.php
    /**
     * @return Response
     */
    public function index(): Response
    {
        return Inertia::render('Books/Index', [
            'books' => Book::query()->paginate(5, [
                'id',
                'title',
                'author',
            ]),
        ]);
    }
```

この少ない修正でページネーションが実装できました。

折角なので、Vue3.2から利用出来る`<script setup>`を利用して、Index.vueを書き換えます。記述量がぐっと減り、見通しが良くなりました。

```vue:./resources/js/Pages/Books/Index.vue
<template>
    <ul>
        <li v-for="book in books.data" key="book.id">
            <Link :href="`/books/${book.id}`" v-text="book.title"/>
        </li>
    </ul>

    <div>
        <ul>
            <li v-for="link in books.links">
                <Link
                    v-if="link.url"
                    :href="link.url"
                    v-html="link.label"
                />
                <span
                    v-if="!link.url"
                    v-html="link.label"
                />
            </li>
        </ul>
    </div>

    <div>
        <Link href="/books/create">Create</Link>
    </div>
</template>

<script setup>
import { Link } from '@inertiajs/inertia-vue3';

const props = defineProps({
    books: Object,
});
</script>
```

では、新規作成ページを作ります。

```vue:./resources/js/Pages/Books/Create.vue
<template>
    <form @submit.prevent="submit">
        <div>
            Title:
            <input v-model="form.title" type="text" name="title" id="title">
        </div>
        <div>
            Author:
            <input v-model="form.author" type="text" name="author" id="author">
        </div>
        <div>
            <button type="submit" :disabled="form.processing">Submit</button>
        </div>
    </form>
</template>

<script setup>
import { useForm } from '@inertiajs/inertia-vue3';

let form = useForm({
    title: '',
    author: '',
});

let submit = () => {
    form.post('/books');
};
</script>
```

コントローラーも修正します。`store()`を追加します。

```php:./app/Http/Controllers/BookController.php
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Request;

// （中略）

    /**
     * @return RedirectResponse
     */
    public function store(): RedirectResponse
    {
        $attributes = Request::validate([
            'title'  => ['required', 'max:255'],
            'author' => ['required', 'max:255'],
        ]);

        Book::create($attributes);

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

この状態でBook一覧から`Create`リンクをクリックし、titleとauthorを入力してSubmitボタンをクリックするとBook一覧の最後に追加されるはずです。

詳細ページを作成します。

```vue:./resources/js/Pages/Books/Show.vue
<template>
    <div>
        title: {{ book.title }}
    </div>
    <div>
        author: {{ book.author }}
    </div>

    <div>
        <Link :href="`/books/${book.id}/edit`">Edit</Link>
    </div>
    <div>
        <Link :href="`/books/${book.id}`" method="delete">Delete</Link>
    </div>
</template>

<script setup>
import {Link} from '@inertiajs/inertia-vue3';

const props = defineProps({
    book: Object,
});
</script>
```

コントローラーを修正します。

```php:./app/Http/Controllers/BookController.php
    /**
     * @param Book $book
     * @return Response
     */
    public function show(Book $book): Response
    {
        return Inertia::render('Books/Show', [
            'book' => $book->only([
                'id',
                'title',
                'author',
            ]),
        ]);
    }
```

この状態でBook一覧からタイトルをクリックすると、詳細（タイトルと作者）が表示されるはずです。

修正ページを作成します。

```vue:./resources/js/Pages/Books/Edit.vue
<template>
    <form @submit.prevent="submit">
        <div>
            Title:
            <input v-model="form.title" type="text" name="title" id="title">
        </div>
        <div>
            Author:
            <input v-model="form.author" type="text" name="author" id="author">
        </div>
        <div>
            <button type="submit" :disabled="form.processing">Submit</button>
        </div>
    </form>
</template>

<script setup>
import {useForm} from '@inertiajs/inertia-vue3';

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

let form = useForm({
    title: props.book.title,
    author: props.book.author,
});

let submit = () => {
    form.patch(`/books/${props.book.id}`);
};
</script>
```

コントローラーを修正します。`edit()`を修正、`update()`を追加します。

```php:./app/Http/Controllers/BookController.php
    /**
     * @param Book $book
     * @return Response
     */
    public function edit(Book $book): Response
    {
        return Inertia::render('Books/Edit', [
            'book' => $book->only([
                'id',
                'title',
                'author',
            ]),
        ]);
    }

    /**
     * @param Book $book
     * @return RedirectResponse
     */
    public function update(Book $book): RedirectResponse
    {
        $attributes = Request::validate([
            'title'  => ['required', 'max:255'],
            'author' => ['required', 'max:255'],
        ]);

        $book->update($attributes);

        return redirect()->route('books.show', $book->id);
    }
```

この状態で、詳細から`Edit`リンクをクリックし、titleとauthorを修正してSubmitボタンをクリックすると、詳細ページに戻り修正が確認出来ます。

最後に削除を実装します。削除の画面は詳細ですので、コントローラの修正のみです。`destroy()`を追加します。

```php:./app/Http/Controllers/BookController.php
    /**
     * @param Book $book
     * @return RedirectResponse
     */
    public function destroy(Book $book): RedirectResponse
    {
        $book->delete();

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

この状態で、詳細画面の`Delete`リンクをクリックするとデータが削除されます。今回は簡単にするため削除時の確認モーダルなどは表示せず、いきなり削除しています。

これでCRUDが実装できました。

## 6. TypeScript化

ついでにTypeScript化も行います。

`app.js`を`app.ts`とリネームし、以下のように修正します。

```typescript:./resources/js/app.ts
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/inertia-vue3'
import { InertiaProgress } from '@inertiajs/progress'
import Layout from "./Components/Layout.vue";

createInertiaApp({
    resolve: name => {
        let page = require(`./Pages/${name}`).default;

        page.layout ??= Layout;

        return page;
    },
    setup({ el, app, props, plugin }) {
        createApp({ render: () => h(app, props) })
            .use(plugin)
            .mount(el)
    },
})
InertiaProgress.init()
```

`webpack.mix.js`を以下のように修正します。

```javascript:./webpack.mix.js
const mix = require('laravel-mix');

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel applications. By default, we are compiling the CSS
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.ts('resources/js/app.ts', 'public/js')
    .vue()
    .postCss('resources/css/app.css', 'public/css', [
        //
    ]);
```

`tsconfig.json`をプロジェクトルート直下に配置します。

```json:./tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "module": "es2020",
        "moduleResolution": "node",
        "baseUrl": "./",
        "strict": true,         // Enable strict type-checking options
        "skipLibCheck": true,   // Skip type checking of declaration files
        "noImplicitAny": false  // Bypass raising errors on `any` type
    },
    "include": ["resources/js/**/*"] // Frontend paths pattern
}
```

`shims-vue.d.ts`を`resources/js/@types/shims-vue.d.ts`に作成します。

```typescript:./resources/js/@types/shims-vue.d.ts
declare module "*.vue" {
    import type { DefineComponent } from "vue";
    const component: DefineComponent<{}, {}, any>;
    export default component;
}
```

typescript関係のnpmモジュールをインストールします。

```terminal
sail npm install typescript ts-loader -D
```

Nav.vueを修正します（script setup化が漏れてましたね）。

```vue:./resources/js/Components/Nav.vue
<template>
    <nav>
        <ul>
            <li><Link href="/">Home</Link></li>
            <li><Link href="/books">Books</Link></li>
        </ul>
    </nav>
</template>

<script setup lang="ts">
import { Link } from '@inertiajs/inertia-vue3';
</script>
```

続いてLayout.vueも。

```vue:./resources/js/Components/Layout.vue
<template>
    <Nav />

    <slot />
</template>

<script setup lang="ts">
import Nav from './Nav.vue';
</script>
```

Index.vue, Show.vue, Edit.vue, Create.vueも修正します。

```vue:./resources/js/Pages/Books/Index.vue
<template>
    <ul>
        <li v-for="book in books.data" key="book.id">
            <Link :href="`/books/${book.id}`" v-text="book.title"/>
        </li>
    </ul>

    <div>
        <ul>
            <li v-for="link in books.links">
                <Link
                    v-if="link.url"
                    :href="link.url"
                    v-html="link.label"
                />
                <span
                    v-if="!link.url"
                    v-html="link.label"
                />
            </li>
        </ul>
    </div>

    <div>
        <Link href="/books/create">Create</Link>
    </div>
</template>

<script setup lang="ts">
import { Link } from '@inertiajs/inertia-vue3';

interface BookModel {
    id: number;
    title: string;
    author: string;
}
interface LinkModel {
    url: string;
    label: string;
}

interface Props {
    books: {
        data: Array<BookModel>,
        links: Array<LinkModel>,
    },
}

const props = defineProps<Props>();
</script>
```

```vue:./resources/js/Pages/Books/Show.vue
<template>
    <div>
        title: {{ book.title }}
    </div>
    <div>
        author: {{ book.author }}
    </div>

    <div>
        <Link :href="`/books/${book.id}/edit`">Edit</Link>
    </div>
    <div>
        <Link :href="`/books/${book.id}`" method="delete">Delete</Link>
    </div>
</template>

<script setup lang="ts">
import {Link} from '@inertiajs/inertia-vue3';

interface BookModel {
    id: number;
    title: string;
    author: string;
}

interface Props {
    book: BookModel;
}

const props = defineProps<Props>();
</script>
```

```vue:./resources/js/Pages/Books/Edit.vue
<template>
    <form @submit.prevent="submit">
        <div>
            Title:
            <input v-model="form.title" type="text" name="title" id="title">
        </div>
        <div>
            Author:
            <input v-model="form.author" type="text" name="author" id="author">
        </div>
        <div>
            <button type="submit" :disabled="form.processing">Submit</button>
        </div>
    </form>
</template>

<script setup lang="ts">
import {useForm} from '@inertiajs/inertia-vue3';

interface BookModel {
    id: number;
    title: string;
    author: string;
}

interface Props {
    book: BookModel;
}

const props = defineProps<Props>();

let form = useForm({
    title: props.book.title,
    author: props.book.author,
});

let submit = () => {
    form.patch(`/books/${props.book.id}`);
};
</script>
```

```vue:./resources/js/Pages/Books/Create.vue
<template>
    <form @submit.prevent="submit">
        <div>
            Title:
            <input v-model="form.title" type="text" name="title" id="title">
        </div>
        <div>
            Author:
            <input v-model="form.author" type="text" name="author" id="author">
        </div>
        <div>
            <button type="submit" :disabled="form.processing">Submit</button>
        </div>
    </form>
</template>

<script setup lang="ts">
import { useForm } from '@inertiajs/inertia-vue3';

let form = useForm({
    title: '',
    author: '',
});

let submit = () => {
    form.post('/books');
};
</script>
```

この状態で`npm run watch`を一度`Ctrl+C`で停止してから、改めて実行しエラーが出ないこと、動作が変わりないことを確認します。

コードはこなれておらず、特にTypeScriptの型周りにリファクタリングの余地がありますが今回はここまでとします。

# おわりに

特に大がかりなサイトを作ったわけでは無いのでInertia.jsの利点があまり出ていませんが、それでも通常の「APIを作って、そのI/Fをたたいてレスポンスを、、、」といった事を考えずに、さらっと作れてしまうのは今後のサイト構築の進め方を大きく変える可能性があると感じています。Inertia.jsを使ってサイト構築をしてみたい、そう思って頂ければ幸いです。