つづき。
VILT スタック は API を使わないで、システムを構築するものです。
ではどうやって通信を行うのでしょうか。
フォームの作成
よくある場面としては、データの更新でしょうか。
フォームに入力して POST 通信を送る場面を考えてみます。
以下の記事を参考に、今回利用する DTO クラスの作成を行ってください。
PostData PostTagData
そして PostData を更新する処理を書きます。
PostData には、ユーザー選択の機能があるので users も渡してあげます。
実際の環境では
all()は使わないでlimit()やpaginate()で制御してくださいね。
初心者がやりがちなんですが、時限爆弾を埋め込むことになります。。。
Route::get('/test', [TestController::class, 'index'])->name('tests.index');
Route::post('/test', [TestController::class, 'store'])->name('tests.store');
namespace App\Http\Controllers;
use App\Data\PostData;
use App\Models\User;
use Inertia\Inertia;
class TestController extends Controller
{
public function index()
{
return Inertia::render('Test', [
'posts' => fn () => Post::all(),
'users' => fn () => User::all(),
]);
}
public function store(PostData $data)
{
/** @var Post */
$post = Post::create($data->except('tags')->all());
foreach ($data->tags as $tag) {
$post->tags()->create($tag);
}
return redirect()->back();
}
}
Inertia の処理は redirect() を返却するのが基本です。
back() をすることで先ほどの画面に戻って、自動的に defineProps に書かれているデータのリロード行われます。なので、フロントに再取得処理などは書かなくて大丈夫です。
フロント側の画面はこんな感じで作ってみます。
PostData を更新するためのフォームと、下部に記事一覧を表示しています。
UI には PrimeVue と呼ばれるものを使っていますが、使い慣れたものでOKです。
自作でもOK。
<template>
<div class="m-4">
<div class="w-md">
<form class="grid grid-cols-[auto_1fr] items-center gap-2" @submit.prevent>
<label>ユーザーID</label>
<Select
v-model="form.user_id"
:options="users"
option-value="id"
option-label="name"
/>
<label>タイトル</label>
<InputText v-model="form.title" />
<label class="self-start pt-2">コンテンツ</label>
<Textarea v-model="form.body" rows="3" />
<label class="self-start pt-2">タグ</label>
<div class="grid grid-cols-1 gap-2">
<div
v-for="(tag, idx) of form.tags"
:key="idx"
class="flex items-center gap-2"
>
<InputText
:model-value="tag.name"
class="grow"
@update:model-value="form.tags[idx].name = $event ?? ''"
/>
<ColorPicker
:model-value="tag.color"
@update:model-value="form.tags[idx].color = $event"
/>
<Button label="-" severity="danger" @click="onRemoveTag(idx)" />
</div>
<div>
<Button label="+ 追加" @click="onAppendTag"/>
</div>
</div>
<div class="col-span-2 justify-self-end">
<Button
type="submit"
label="保存"
@click="onSubmit"
/>
</div>
</form>
</div>
<div>
<label>Posts: ({{ posts.length }})</label>
<pre class="text-xs">{{ posts }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import { useForm, usePage } from '@inertiajs/vue3'
import Button from 'primevue/button'
import ColorPicker from 'primevue/colorpicker'
import InputText from 'primevue/inputtext'
import Select from 'primevue/select'
import Textarea from 'primevue/textarea'
import TestRoute from '@/routes/tests'
const form = useForm<App.Data.PostData>({
user_id: 0,
title: '',
body: undefined,
tags: [{ name: '', color: '' }]
})
defineProps<{
posts: App.Models.Post[],
users: App.Models.User[],
}>()
const onAppendTag = () => { form.tags.push({ name: '' }) }
const onRemoveTag = (idx: number) => { form.tags.splice(idx, 1) }
const onSubmit = () => {
form.submit(TestRoute.store())
}
</script>
当たり前のように出てきますが、モデル型やリクエストの型は自動生成させます。
素晴らしすぎるぅ、、、 ![]()
この画面で保存ボタンを押すと...?
保存されました~。しかも下部に表示されているデータは更新されているという現象になります。
これが Inertia.js の面白い仕組みなんです。
Inertia.js は SPA のように振る舞うため、同じ画面へ遷移する場合は props のデータのみ更新がかかります。
便利ですねぇ。
▼ エラーハンドリングは??
安心してください。
もし検証に失敗すると、自動で form.errors にいつものバリデーションエラーが格納されます。
それを画面に表示すればOK。
{{ form.errors.user_id }}
// 配列の場合はこんな感じで index で指定してあげる
{{ form.errors?.[`tags.${idx}.name`] }}
▼ 保存に成功したらフォームをリセットしたいんだけど
できます。form.submit() には各種イベントをキャッチできる仕組みが存在します。
その中の onSuccess に、実行したいクロージャーを渡すことで、更新に成功したらフォームの値をリセットすることができます。
const onSubmit = () => {
form.submit(TestRoute.store(), {
onSuccess: () => form.reset()
})
}
ちなみに、値は一番初めに
useForm({ ここ })で指定した値となります。もし変更したい場合は
defaults({ ここ })に書くことで上書きすることができます。
これはダイアログなどで、選択したレコードを更新するときに便利です。const selectedPost = { user_id: 2, title: '更新対象のポスト', body: '更新するよ~', tags: [{ name: 'タグだよ', color: '00FF00' }] // etc... } form.defaults({ user_id: selectedPost.user_id, title: selectedPost.title, body: selectedPost.body, tags: selectedPost.tags, })
補足として、この仕組みを使えば画面のローディングなども制御できますね。
▼ すべてのデータが自動で変わると通信量が増えない?
今回で言うと posts と users が自動的に再取得されるので、DBへのアクセスが発生します。もし複雑な SQL を組んでいると更新に時間がかかる可能性があります...。
(実は share() で指定したものも併せて更新されちゃいます)
そんな時は only パラメタを使います!
const onSubmit = () => {
form.submit(TestRoute.store(), {
only: ['posts'],
onSuccess: () => form.reset(),
})
}
すると返却値がこんなに小さくなります!
これでポイントになってくるのが、コントローラー側のクロージャーです。
public function index()
{
return Inertia::render('Test', [
'posts' => fn () => Post::with('tags')->get(), // ※ここだけ実行される!
'users' => fn () => User::all(),
]);
}
クロージャーの外に書いてあるとどんな通信でも実行してしまうのですが、属性値を fn() で囲っておくと、only を利用した際に、未指定の属性の関数は実行されません!
今までの Laravel とは違った思考が必要かもしれませんね。
なので、基本的には全部クロージャーでくくっておくと最適化ができます。
Inertia.js は複雑に絡み合っているから、一つ一つの説明が難しい。。。
▼ 味気ないから成功時に Toast を送りたいな
前章で書いた share() を利用します。
ここに toast を増やして、セッションに記録されたトーストを持ってくるようにします。この時必ず fn () でくくってください。
public function share(Request $request): array
{
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
return [
...parent::share($request),
'name' => config('app.name'),
'quote' => ['message' => trim($message), 'author' => trim($author)],
'auth' => [
'user' => $request->user(),
],
+ 'toast' => fn() => request()->session()->get('toast'),
];
}
これを TypeScript の型にも記載します。
export type AppPageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
name: string;
quote: { message: string; author: string };
auth: Auth;
+ toast?: string
};
そしてコントローラーで、リダイレクトをかける際に with() でメッセージを書き込みます。
public function store(PostData $data)
{
/** @var Post */
$post = Post::create($data->except('tags')->all());
foreach ($data->tags as $tag) {
$post->tags()->create($tag->all());
}
- return redirect()->back();
+ return redirect()->back()
+ ->with('toast', '保存しました。');
}
フロントでは、この toast が変化した際に、トーストを発行します。
仮で windows.alert() で実装してみます。
const page = usePage()
watch(() => page.props.toast, () => {
window.alert(page.props.toast)
})
すると、ちゃんとアラートが表示されました。
この watch() をレイアウトなどに仕込んでおけば、好きにトーストを発行できます。
もし、表示されないんやが?という人は only 設定を見てみてください。
実はこの only 、share データにも影響を与えるので、こちらに toast をついかするひつようがあるんですねぇ。奥深い。
form.submit(TestRoute.store(), {
- only: ['posts'],
+ only: ['posts', 'toast'],
onSuccess: () => form.reset(),
})
終わりに
このあたりが扱えれば Form 作りには何も困らないと思いますね。
もしユーザーのページネートがしたければ、この only を使って、ユーザーデータだけ持ってきたりできるってわけですね。
ただその時は、 Ajax 通信をしたほうが良いかもしれない。。。
ちなみに useForm の値の挙動がわかんない。。というときは以下の記事を参考にしてください。
最近は <Form> タグ上で useForm の仕組みを扱うこともできるようになったので、シンプルなお問い合わせページであれば userForm を使うまでもなく簡単に実装できます!





