僕なりの Inertia 備忘録

# サーバーサイド側の設定

公式に従ってサーバーサイドのInertiaの設定をしていく

> https://inertiajs.com/client-side-setup
>
> https://laravel.com/docs/10.x/starter-kits を使わずに個別でインストールしていく



## 依存関係をインストール

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



## ルートテンプレートの作成

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" />
    @vite('resources/js/app.js')
    @inertiaHead
  </head>
  <body>
    @inertia
  </body>
</html>
```

- `@inertiaHead`

    Inertia.jsを使用する上で必要なメタデータやリンク等を動的に挿入するタグ。

- `@inertia`

    フロントエンドのエントリポイント作成のためのタグ。ここに アプリケーションのメインjavascriptファイル（*app.js*等） が読み込まれ実行される。

## ミドルウェアの設定

ArtisanコマンドでHandleInertiaRequestsミドルウェアをアプリケーションに公開する。

```bash
sail php artisan inertia:middleware
```

ミドルウェアが公開されたら、Web ミドルウェア グループの最後に、HandleInertiaRequests ミドルウェアを  に登録する。

```php
# /app/Http/Kernel.php

'web' => [
    // ...
    \App\Http\Middleware\HandleInertiaRequests::class,
],
```



## レスポンスの作成

ルーターでやる場合

```php
# /route/web.php

Route::get('/news/{news}', function () {
    return Inertia::render('News/Show');
});
```



コントローラーでやる場合

```php
# /app/Http/Controllers/NewsController.php

use Inertia\Inertia;

class NewsController extends Controller
{
    /**
     * Display the specified resource.
     */
    public function show(News $news)
    {
        return Inertia::render('News/Show', [
            'news' => $news->only(
                'id',
                'title',
                'body',
            ),
        ]);
    }
}
```



# フロントエンド側の設定

ViteとVue3で実装します。

Laravel10では、Viteが標準のフロントエンドビルドツールとして採用されておりプロジェクトを作成すると、Viteに関連する設定ファイルが初めから含まれている。



## ViteのVueプラグインをインストール

`@vitejs/plugin-vue`をインストールすると、それ自体でVueを依存関係として持っているため、Vueも自動的にインストールされる。

> [Asset Bundling (Vite) - Laravel 10.x - The PHP Framework For Web Artisans](https://laravel.com/docs/10.x/vite#vue)

```bash
sail npm install @vitejs/plugin-vue --save-dev
```

```js
// /vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'; //←追加

export default defineConfig({
    plugins: [
        vue(), //←追加
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});
```



## InertiaとVue3の依存関係のためのライブラリをインストール

```bash
sail npm install @inertiajs/vue3
```



## Inertiaアプリの初期化

```js
// /resources/js/app.js
import './bootstrap';

import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'

createInertiaApp({
    resolve: (name) => {
        const pages = import.meta.glob("./Pages/**/*.vue", { eager: true });
        return pages[`./Pages/${name}.vue`];
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el);
    },
});
```

- `resolve`

    ページコンポーネントの名前を受け取り、対応するVueコンポーネントを返す。ここでは`import.meta.glob`を使用して、*./Pages/*内の全Vueファイルを動的にインポートして返している。

- `setup`

    アプリケーションが起動する際に呼び出される。

    `resolve`で返されるコンポーネントを現在のページとしてレンダリングされる。従来のVueアプリケーションで*App.vue*が固定のルートコンポーネントとして機能するのとは異なり、現在のルートに応じてルートコンポーネントが動的に変わる。

    

## コンポーネントの作成

```vue
# /resources/js/Pages/News/Show.vue

<template>
	<div>
        Hello World!
    </div>
</template>
```



## フロントエンドサーバーを起動してアクセス

Viteのフロントエンドサーバーを起動して

```bash
sail npm run dev
```

[http://localhost/news/1](http://localhost/news/1) にアクセス



# CRUDの実装

## 共通

CRUD用のルートに修正する。

```php
# /routes/web.php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\NewsController; //←追加

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

// ↓削除
// Route::get('/news/{news}', function () {
//     return Inertia::render('News/Show');
// });

// ↓以下6行追加
Route::resource('news', NewsController::class)
    ->except(['store', 'update']);
Route::post('news/create', [NewsController::class, 'store'])
    ->name('news.store');
Route::put('news/{news}/edit', [NewsController::class, 'update'])
    ->name('news.update');
```

- `resource`

    以下のルートを作成してくれる。

    > 参考： [Laravel 10.x コントローラ](https://readouble.com/laravel/10.x/ja/controllers.html#:~:text=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AB%E3%82%88%E3%82%8A%E5%87%A6%E7%90%86%E3%81%95%E3%82%8C%E3%82%8B%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)

    | 動詞      | URI                 | アクション | ルート名     |
    | --------- | ------------------- | ---------- | ------------ |
    | GET       | `/news`             | index      | news.index   |
    | GET       | `/news/create`      | create     | news.create  |
    | POST      | `/news`             | store      | news.store   |
    | GET       | `/news/{news}`      | show       | news.show    |
    | GET       | `/news/{news}/edit` | edit       | news.edit    |
    | PUT/PATCH | `/news/{news}`      | update     | news.update  |
    | DELETE    | `/news/{news}`      | destroy    | news.destroy |

- `Route::post`、`Route::put`

    *store*と*update*を除外して別途ルートを設定している。sailでの開発環境でvalidationエラー時に、元いた画面ではなくindexにリダイレクトされてしまい、どうしても期待通りに動かなかった。なぜこうなるか、有識者の方教えてくださると幸いです。

    表のstoreとupdateのURI部分が以下のように変更される。

    | 動詞      | URI                 | アクション | ルート名    |
    | --------- | ------------------- | ---------- | ----------- |
    | POST      | `/news/create`      | store      | news.store  |
    | PUT/PATCH | `/news/{news}/edit` | update     | news.update |

[Ziggy](https://github.com/tighten/ziggy#spas-or-separate-repos)をインストールする。

*Ziggy は、Laravelの`route()`と同じように動作するjavascript の`Route()`関数を提供しており、JavaScript で名前付きの Laravel ルートを簡単に使用できるようになる。*

Breezeをインストールすると自動的に入ってくるが、今回はBreezeをインストールせずに入れてみる。

> https://github.com/tighten/ziggy?tab=readme-ov-file#vue
>
> https://iwasherefirst2.medium.com/how-to-setup-ziggy-on-laravel-inertija-vite-vue-19385f50848e

```bash
sail composer require tightenco/ziggy
```

```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" />
    @vite('resources/js/app.js')
    @routes //←追加
    @inertiaHead
  </head>
  <body>
    @inertia
  </body>
</html>
```

```js
// vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import path from 'path'; //←追加

export default defineConfig({
    // ↓５行追加
    resolve: {
        alias: {
            'ziggy-js': path.resolve('vendor/tightenco/ziggy'),
        }
    },
    plugins: [
        vue(),
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});
```

```js
// resources/js/app.js

import './bootstrap';

import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { ZiggyVue } from 'ziggy-js'; //←追加

createInertiaApp({
    resolve: (name) => {
        const pages = import.meta.glob("./Pages/**/*.vue", { eager: true });
        return pages[`./Pages/${name}.vue`];
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue) //←追加
            .mount(el);
    },
});
```



## Read

一覧画面と詳細画面とそのそれぞれの処理を追加していく。

```php
# /app/Http/Controllers/NewsController.php

class NewsController extends Controller
{

    public function index()
    {
        $news = News::all();
        return Inertia::render('News/Index', [
            'news' => $news,
        ]);
    }
    
    ...省略...

    public function show(News $news)
    {
        return Inertia::render('News/Show', [
            'news' => $news->only(
                'title',
                'body',
            ),
        ]);
    }
    
    ...省略...
```

```vue
// resources/js/Pages/News/Index.vue

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

defineProps({
    news: Object,
})
</script>

<template>
    <div v-if="news">
        <div v-for="n in news">
            <h1>{{ n.title }}</h1>
            <p>{{ n.body }}</p>
            <Link :href="route('news.show', n)">詳細</Link>
        </div>
    </div>
</template>
```

[http://localhost/news](http://localhost/news)

```vue
 <!-- resources/js/Pages/News/Show.vue -->

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

defineProps({
    news: Object,
})
</script>

<template>

    <div>
        <h1>{{ news.title }}</h1>
        <p>{{ news.body }}</p>
    </div>
    <Link :href="route('news.index')">一覧に戻る</Link>

</template>

```

[http://localhost/news/1](http://localhost/news/1)

## Create

新規作成画面とその処理を追加していく。

一覧画面に「新規作成」へのリンクを追加。

```vue
// resources/js/Pages/News/Index.vue

...省略...
</script>

<template>
    <Link :href="route('news.create')">新規作成</Link> <!-- ←追加 -->
    <div v-if="news">
        <div v-for="n in news">
            <h1>{{ n.title }}</h1>
            <p>{{ n.body }}</p>
            <Link :href="route('news.show', n)">詳細</Link>
        </div>
    </div>
</template>
```

```php
# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{
    ...省略...

    public function create()
    {
        return Inertia::render('News/Create', []);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => ['required'],
            'body' => ['required'],
        ]);

        News::create($validated);

        return redirect()->route('news.index');
    }
    
    ...省略...
```

```vue
<!-- resources/js/Pages/News/Create.vue -->

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

const form = useForm({
    title: null,
    body: null,
})

const submit = () => {
    form.post(route('news.store'));
}
</script>

<template>
    <form @submit.prevent="submit">
        <table>
            <tr>
                <th><label for="title">title:</label></th>
                <td>
                    <div v-if="form.errors.title">{{ form.errors.title }}</div>
                    <input id="title" type="text" v-model="form.title">
                </td>
            </tr>
            <tr>
                <th><label for="body">body:</label></th>
                <td>
                    <div v-if="form.errors.body">{{ form.errors.body }}</div>
                    <textarea id="body" cols="22" rows="20" v-model="form.body"></textarea>
                </td>
            </tr>
            <tr>
                <th></th>
                <td>
                    <button type="submit" :disabled="form.processing">登録</button>
                </td>
            </tr>
        </table>
    </form>
</template>
```

## Update

```vue
<!-- resources/js/Pages/News/Index.vue -->

...省略...
</script>

<template>
    <Link :href="route('news.create')">新規作成</Link>
    <div v-if="news">
        <div v-for="n in news">
            <h1>{{ n.title }}</h1>
            <p>{{ n.body }}</p>
            <div>
                <Link :href="route('news.show', n)">詳細</Link>
            </div>
            <div>
                <Link :href="route('news.edit', n)">編集</Link> <!-- ←追加 -->
            </div>
        </div>
    </div>
</template>
```

```php
# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{
	...省略...

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

    public function update(Request $request, News $news)
    {
        $validated = $request->validate([
            'title' =>'required',
            'body' => 'required',
        ]);

        $news->update($validated);

        return redirect()->route('news.index');
    }
    
    ...省略...
```

```vue
<!-- resources/js/Pages/News/Edit.vue -->

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

const props = defineProps({
    news: Object,
})

const form = useForm({
    title: props.news.title,
    body: props.news.body,
})

const submit = () => {
    form.put(route('news.update', props.news.id ))
}
</script>

<template>
    <form @submit.prevent="submit">
        <table>
            <tr>
                <th><label for="title">title:</label></th>
                <td>
                    <div v-if="form.errors.title">{{ form.errors.title }}</div>
                    <input id="title" type="text" v-model="form.title">
                </td>
            </tr>
            <tr>
                <th><label for="body">body:</label></th>
                <td>
                    <div v-if="form.errors.body">{{ form.errors.body }}</div>
                    <textarea id="body" cols="22" rows="20" v-model="form.body"></textarea>
                </td>
            </tr>
            <tr>
                <th></th>
                <td>
                    <button type="submit" :disabled="form.processing">更新</button>
                </td>
            </tr>
        </table>
    </form>
</template>

```



## Delete

```php
# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{
    public function destroy(News $news)
    {
        $news->delete();

        return redirect()->route('news.index')->with('message', 'News Deleted Successfully');
    }
```

```vue
<!-- resources/js/Pages/News/Index.vue -->

<script setup>
import { Link, useForm } from '@inertiajs/vue3' //←修正

defineProps({
    news: Object,
})
// ↓以下6行追加
const form = useForm({});
const deleteNews = (news) => {
    form.delete(route('news.destroy', news), {
        preserveScroll: true,
    })
}
</script>

<template>
    <Link :href="route('news.create')">新規作成</Link>
    <div v-if="news">
        <div v-for="n in news">
            <h1>{{ n.title }}</h1>
            <p>{{ n.body }}</p>
            <div>
                <Link :href="route('news.show', n)">詳細</Link>
            </div>
            <div>
                <Link :href="route('news.edit', n)">編集</Link>
            </div>
            <div>
                <Link href="#" @click="deleteNews(n)">削除する</Link> <!-- ←追加 -->
            </div>
        </div>
    </div>
</template>
```



# Session

## flash

```php
//コントローラーの削除メソッド
    public function destroy ($id) 
    {
        Bookmark::destroy($id);
        return to_route('bookmark.index')->with('message', '削除しました'); //削除後ブックマーク一覧ページへリダイレクト
    }
```

```php
# app/Http/Middleware/HandleInertiaRequests.php

//(略)
    public function share(Request $request)
    {
        return array_merge(parent::share($request), [
            //(略)
            'flash' => [
                'message' => fn () => $request->session()->get('message')
            ],
        ]);
    }
    //(略)
```

```vue
import { usePage } from '@inertiajs/vue3'

const page = usePage()
console.log(page.props.flash.message)

            <div v-if="page.props.flash.message" class="bg-green-200 p-2 m-1">
                {{ page.props.flash.message }}
            </div>
```



# File upload

バックエンドはLaravelで。

## create/store

```vue
<script setup>
const form = useForm({
    title: null,
    body: null,
    image: null,
})

const submit = () => {
    form.post(route('news.store'));
}    
</script>

<template>
<div>
    <input type="file" id="image" @input="form.image = $event.target.files[0]">
    <!-- inertiaはこっち -->
    <img :src="'/upload/'+ form.image" alt="">
    <!-- bladeはこっち -->
    <img src="{{ asset($user->img_path) }}" >
</div>
</template>
```

```php
public function store(Request $request)
{
    $formData = $request->validate([
        'title' => ['required'],
        'body' => ['required'],
    ]);

    if ($image = $request->file('image')) {
        $imageName = $image->getFilename(). '.'. $image->getClientOriginalExtension();
        $image->storeAs('public/upload', $imageName);
        $formData['image'] = $imageName;
        // or
        // $formData['image'] = $image->store('public/upload');
    }

    News::create($formData);

}
```

※ `$image->getClientOriginalExtension()`は使わない方がいいらしい。

> https://laravel.com/docs/10.x/filesystem#other-uploaded-file-information



## edit/update

```vue
<script setup>
const form = useForm({
    title: props.news.title,
    body: props.news.body,
    image: null,
    registerd_image: props.news.image
})

const submit = () => {
    router.post(route('admin.ramen.update', props.ramen.id), {
        _method: 'put',
        title: form.title,
        body: form.body,
        image: form.image,
        registerd_image: form.registerd_image,
    });
}    
</script>

<template>

    <input type="file" name="image" @input="form.image = $event.target.files[0]">
	<img :src="`/${form.registerd_image}`" alt="">

</template>
```

※ `form.put(route('news.update', news.id))` はできない。

> https://inertiajs.com/file-uploads

```php
public function update(Request $request, News $news)
{
    $formData = $request->validate([
        'title' =>'required',
        'body' => 'required',
        'registerd_image' => 'nullable'
    ]);

    if ($image = $request->file('image')) {
        $imageName = $image->getFilename(). '.'. $image->getClientOriginalExtension();
        $image->storeAs('public/upload', $imageName);
        $formData['image'] = $imageName;
        // or
        // $formData['image'] = $image->store('upload')

        if (!empty($formData['registerd_image'])) {
            unlink(storage_path('app/public/upload/'). $formData['registerd_image']);
            // or
            // unlink(storage_path('app/'). $validated['registerd_image']);
        }
    }

    unset($formData['registerd_image']);

    $ramen->update($formData);
}
```



## preview

```vue
<script setup>
    
const form = useForm({
    name:       props.ramen.name,
    address:    props.ramen.address,
    type:       props.ramen.type,
    taste:      props.ramen.taste,
    image:      null,
    old_image:  props.ramen.image,
    time_open:  props.ramen.time_open,
    time_close: props.ramen.time_close,
    date_open:  props.ramen.date_open,
    day_close:  props.ramen.day_close,
})

const imagePreview = ref('');
const handleFileChange = (event) => {
    const file = event.target.files[0];
    form.image = file;
    if (file) {
        const reader = new FileReader();
        reader.onload = (e) => {
            imagePreview.value = e.target.result;
        }
        reader.readAsDataURL(file);
    }
}

</script>

<template>
<input type="file" name="image" @input="handleFileChange">
<div>
    <img v-if="imagePreview" :src="imagePreview" alt="">
    <img v-else-if="form.old_image" :src="'/upload/'+ form.old_image" alt="">
    <img v-else src="/image/default.jpg" alt="">
    </div>
</template>
```