こちらは スタジオ・アルカナ Advent Calendar 2024 の12日めの記事になります!
はじめに
前回の記事では Inertia を使ってBladeを書こう!というお話をしました。
そこで Formを利用したデータのやり取り
について書くと予告して終わりました。
LaravelでInertiaを使わずともじつはReactやVueJSを使うことはできます。できるのですが、Inertiaを使うと色々楽できるということなのです。
そのうちの一つがフォームを使ったデータのやり取りになります。
今回は例として「食べたおやつのカロリーをただ足し合わせて算出する」サービスを作ったとします。
セレクトボックスから選べるおやつは「閉鎖したアルカナ朝霞オフィス(旧アンテクオフィス)周辺で夕方によく買ってたおやつ」です。
新宿になってからはおやつはオフィス内でも買えるようになったので外にあまり買いに行かなくなりました。。。
外に買いに行っても良いんですけど、なまじ新宿になってからその気になれば何でも買えるってなると逆に絞れなくなりますよね。
今回使う例はGitHubに置いてあるので、参考にしてください。
https://github.com/sa-gimayama/example2024
Inertiaじゃない場合(普通のblade)
Inertiaじゃない場合、普通にフォームリクエストになります。 <form>
タグ使うあれですね。
普通のHTMLなら普通にフォームからpostすればいいのでこれが当然一番簡単になりますよね。
example.ateOyatsu.blade.php
<head>
</head>
<body>
<h1>おやつを食べる</h1>
<form action="{{route('example.ateOyatsu.blade.update')}}" method="post">
@csrf
<select name="oyatsu_id">
@foreach($oyatsus as $oyatsu)
<option value="{{ $oyatsu->id }}">{{ $oyatsu->name }}({{ $oyatsu->calory }}kcal)</option>
@endforeach
</select>
<input type="date" name="ate_at">
<button type="submit">送信</button>
</form>
<hr>
<h2>食べたおやつ</h2>
<table>
<tr>
<th>名前</th>
<th>カロリー</th>
<th>食べた日</th>
</tr>
@foreach($ateOyatsus as $ateOyatsu)
<tr>
<td>{{ $ateOyatsu->oyatsu->name }}</td>
<td>{{ $ateOyatsu->oyatsu->calory }}</td>
<td>{{ $ateOyatsu->ate_at }}</td>
</tr>
@endforeach
</table>
<h2>総カロリー</h2>
<p>{{ $totalCalory }}</p>
</body>
<head>
</head>
<body>
<h1>おやつを食べる</h1>
<form action="{{route('example.ateOyatsu.blade.update')}}" method="post">
@csrf
<select name="oyatsu_id">
@foreach($oyatsus as $oyatsu)
<option value="{{ $oyatsu->id }}">{{ $oyatsu->name }}({{ $oyatsu->calory }}kcal)</option>
@endforeach
</select>
<input type="date" name="ate_at">
<button type="submit">送信</button>
</form>
<hr>
<h2>食べたおやつ</h2>
<table>
<tr>
<th>名前</th>
<th>カロリー</th>
<th>食べた日</th>
</tr>
@foreach($ateOyatsus as $ateOyatsu)
<tr>
<td>{{ $ateOyatsu->oyatsu->name }}</td>
<td>{{ $ateOyatsu->oyatsu->calory }}</td>
<td>{{ $ateOyatsu->ate_at }}</td>
</tr>
@endforeach
</table>
<h2>総カロリー</h2>
<p>{{ $totalCalory }}</p>
</body>
で、普通に同期通信してるので、「送信」ボタンを押すと画面がチラッてするんですよね。
ダメじゃないんですけど、古めかしいと言うか、イマイチな感じがして「んー」ってなりますよね。
Inertiaじゃない場合(ajax使う場合)
そうなるとやっぱ非同期通信ですよね。
非同期通信といえば俺達のjQueryの出番です。。。って言おうと思ったんですがめんどくさいのでAlpineJS&axiosにしました。
しかしAlpineJS使ったとて思ったより大変になってしまいました。
(だいぶ忘れてたのもありますが)
おやつだからではないですが、だいぶハイカロリーでした。
example.ateOyatsuAjax.blade.php
@routes
<head>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div x-data="Oyatsu()">
<h1>おやつを食べる</h1>
<form>
<select name="oyatsu_id" x-model="selectedOyatsu">
<option value="">選択してください</option>
<template x-for="oyatsu in oyatsus">
<option :value="oyatsu.id" x-text="`${oyatsu.name} (${oyatsu.calory}kcal)`"></option>
</template>
</select>
<input type="date" name="ate_at" x-model="ateAt">
<button type="button" @click="submitOyatsu">送信</button>
</form>
<hr>
<h2>食べたおやつ</h2>
<table>
<tr>
<th>名前</th>
<th>カロリー</th>
<th>食べた日</th>
</tr>
<template x-for="ateOyatsu in ateOyatsus">
<tr>
<td x-text="ateOyatsu.oyatsu.name"></td>
<td x-text="ateOyatsu.oyatsu.calory"></td>
<td x-text="ateOyatsu.ate_at"></td>
</tr>
</template>
</table>
<h2>総カロリー</h2>
<p x-text="totalCalory"></p>
</div>
<script defer>
function Oyatsu() {
return {
oyatsus: @json($oyatsus),
ateOyatsus: @json($ateOyatsus),
selectedOyatsu: null,
ateAt: null,
totalCalory() {
return this.ateOyatsus.reduce((acc, ateOyatsu) => acc + ateOyatsu.oyatsu.calory, 0);
},
submitOyatsu() {
axios.post(route('example.ateOyatsu.bladeAjax.update'), {
oyatsu_id: this.selectedOyatsu,
ate_at: this.ateAt
}).then(response => {
if (response.data.status === 'success') {
this.ateOyatsus.push(response.data.ateOyatsu);
}
});
}
}
}
</script>
</body>
@routes
<head>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div x-data="Oyatsu()">
<h1>おやつを食べる</h1>
<form>
<select name="oyatsu_id" x-model="selectedOyatsu">
<option value="">選択してください</option>
<template x-for="oyatsu in oyatsus">
<option :value="oyatsu.id" x-text="`${oyatsu.name} (${oyatsu.calory}kcal)`"></option>
</template>
</select>
<input type="date" name="ate_at" x-model="ateAt">
<button type="button" @click="submitOyatsu">送信</button>
</form>
<hr>
<h2>食べたおやつ</h2>
<table>
<tr>
<th>名前</th>
<th>カロリー</th>
<th>食べた日</th>
</tr>
<template x-for="ateOyatsu in ateOyatsus">
<tr>
<td x-text="ateOyatsu.oyatsu.name"></td>
<td x-text="ateOyatsu.oyatsu.calory"></td>
<td x-text="ateOyatsu.ate_at"></td>
</tr>
</template>
</table>
<h2>総カロリー</h2>
<p x-text="totalCalory"></p>
</div>
<script defer>
function Oyatsu() {
return {
oyatsus: @json($oyatsus),
ateOyatsus: @json($ateOyatsus),
selectedOyatsu: null,
ateAt: null,
totalCalory() {
return this.ateOyatsus.reduce((acc, ateOyatsu) => acc + ateOyatsu.oyatsu.calory, 0);
},
submitOyatsu() {
axios.post(route('example.ateOyatsu.bladeAjax.update'), {
oyatsu_id: this.selectedOyatsu,
ate_at: this.ateAt
}).then(response => {
if (response.data.status === 'success') {
this.ateOyatsus.push(response.data.ateOyatsu);
}
});
}
}
}
</script>
</body>
で、注目はここですよね。
axios.post(route('example.ateOyatsu.bladeAjax.update'), {
oyatsu_id: this.selectedOyatsu,
ate_at: this.ateAt
}).then(response => {
if (response.data.status === 'success') {
this.ateOyatsus.push(response.data.ateOyatsu);
}
});
axios.post(route('example.ateOyatsu.bladeAjax.update'), {
oyatsu_id: this.selectedOyatsu,
ate_at: this.ateAt
}).then(response => {
if (response.data.status === 'success') {
this.ateOyatsus.push(response.data.ateOyatsu);
}
});
ここがまさにAPIを呼んでいるところであり、めんどくさいポイントになります。
パッと見、コード量的には大した事なさそうに見えますが、リクエストの形どうするのか、成功したら何返すのか、バリデーションエラー出たらどうするのかと考えることはとても多いのです。
Inertia(React)の場合
ざっとこんな感じになります。
ちゃんとスタイリングしないと見た目が変わっちゃうのですが、機能的には同じなので良しとしましょう。
Pages/Example/ateOyatsu.tsx
import {useForm} from "@inertiajs/react";
type Oyatsu = {
id: number;
name: string;
calory: number;
};
type AteOyatsu = {
id: number;
oyatsu_id: number;
ate_at: string;
oyatsu: Oyatsu;
};
type Props = {
oyatsus: Oyatsu[];
ateOyatsus: AteOyatsu[];
}
export default function AteOyatsu({oyatsus, ateOyatsus}: Props) {
const {data, setData, post, progress, processing} = useForm({
oyatsu_id: '',
ate_at: '',
});
const totalCalory = ateOyatsus.reduce((acc, ateOyatsu) => acc + ateOyatsu.oyatsu.calory, 0);
const submitOyatsu = () => {
if (!processing) {
post(route('example.ateOyatsu.inertia.update'));
}
};
return (
<div>
<h1>おやつを食べる</h1>
<form>
<select name="oyatsu_id" onChange={(e) => setData('oyatsu_id', e.target.value)}>
<option value="">選択してください</option>
{oyatsus.map((oyatsu, index) => (
<option value={oyatsu.id} key={`option${index}`}>{oyatsu.name} ({oyatsu.calory}kcal)</option>
))}
</select>
<input type="date" name="ate_at" onChange={(e) => setData('ate_at', e.target.value)}/>
<button type="button" onClick={submitOyatsu}>送信</button>
</form>
<hr/>
<h2>食べたおやつ</h2>
<table>
<thead>
<tr>
<th>名前</th>
<th>カロリー</th>
<th>食べた日</th>
</tr>
</thead>
<tbody>
{ateOyatsus.map((ateOyatsu, index) => (
<tr key={`oyatsuRaw${index}`}>
<td>{ateOyatsu.oyatsu.name}</td>
<td>{ateOyatsu.oyatsu.calory}</td>
<td>{ateOyatsu.ate_at}</td>
</tr>
))}
</tbody>
</table>
<h2>総カロリー</h2>
<p>{totalCalory}</p>
</div>
)
}
import {useForm} from "@inertiajs/react";
type Oyatsu = {
id: number;
name: string;
calory: number;
};
type AteOyatsu = {
id: number;
oyatsu_id: number;
ate_at: string;
oyatsu: Oyatsu;
};
type Props = {
oyatsus: Oyatsu[];
ateOyatsus: AteOyatsu[];
}
export default function AteOyatsu({oyatsus, ateOyatsus}: Props) {
const {data, setData, post, progress, processing} = useForm({
oyatsu_id: '',
ate_at: '',
});
const totalCalory = ateOyatsus.reduce((acc, ateOyatsu) => acc + ateOyatsu.oyatsu.calory, 0);
const submitOyatsu = () => {
if (!processing) {
post(route('example.ateOyatsu.inertia.update'));
}
};
return (
<div>
<h1>おやつを食べる</h1>
<form>
<select name="oyatsu_id" onChange={(e) => setData('oyatsu_id', e.target.value)}>
<option value="">選択してください</option>
{oyatsus.map((oyatsu, index) => (
<option value={oyatsu.id} key={`option${index}`}>{oyatsu.name} ({oyatsu.calory}kcal)</option>
))}
</select>
<input type="date" name="ate_at" onChange={(e) => setData('ate_at', e.target.value)}/>
<button type="button" onClick={submitOyatsu}>送信</button>
</form>
<hr/>
<h2>食べたおやつ</h2>
<table>
<thead>
<tr>
<th>名前</th>
<th>カロリー</th>
<th>食べた日</th>
</tr>
</thead>
<tbody>
{ateOyatsus.map((ateOyatsu, index) => (
<tr key={`oyatsuRaw${index}`}>
<td>{ateOyatsu.oyatsu.name}</td>
<td>{ateOyatsu.oyatsu.calory}</td>
<td>{ateOyatsu.ate_at}</td>
</tr>
))}
</tbody>
</table>
<h2>総カロリー</h2>
<p>{totalCalory}</p>
</div>
)
}
ポイントとしてはここですね。
const {data, setData, post, processing} = useForm({
oyatsu_id: '',
ate_at: '',
});
// 略
const submitOyatsu = () => {
if (!processing) {
post(route('example.ateOyatsu.inertia.update'));
}
};
const {data, setData, post, processing} = useForm({
oyatsu_id: '',
ate_at: '',
});
// 略
const submitOyatsu = () => {
if (!processing) {
post(route('example.ateOyatsu.inertia.update'));
}
};
普通のReactでやるときはフォーム側で設定した値を useState
とかで状態管理する必要があるのですが、Inertia用Reactでは useForm
という専用のフックがあって、それを使って諸々の事ができるようになっています。
useForm
すると以下の変数と関数が取れます。
他にも色々取れるのですけど、今回使う分だけ紹介します。
なので、最初に useForm
で必要な変数関数を貰って、setData
でデータをセットして、 post
するだけでサーバーにデータを送ることができます。
Ajaxでは Axios.post
で送信したところの続きに then
でコールバック処理を書いてましたが、そんな質めんどくさいことも不要です。
受け側のControllerの戻りを redirect()
にして、戻り先をもとのページにするだけで画面遷移することなく画面をリフレッシュできます。
なので、APIとかいちいち用意しなくても非同期通信ができて、いい感じにできるという話でした!