laravel breeze (react) にavater機能を付ける(1)
laravel breezeの説明はAIに任せて割愛する
導入部分も割愛。確かどこかに書いたような…
git clone https://gitlab.com/catatsumuri/laravel10-starter.git -b breeze-inertia-react
ここに一通り設定したものがあるから適当に持ってくるってことで話を進めますぞ。作業ログ
% cp .env.example .env
% docker run --rm -it -v $(pwd):/app composer install --ignore-platform-reqs
% ./vendor/bin/sail up
% ./vendor/bin/sail npm install
% ./vendor/bin/sail artisan migrate:fresh --seed
Dropping all tables .................................................................................................... 40ms DONE
INFO Preparing database.
Creating migration table ............................................................................................... 38ms DONE
INFO Running migrations.
2014_10_12_000000_create_users_table ................................................................................... 48ms DONE
2014_10_12_100000_create_password_reset_tokens_table ................................................................... 74ms DONE
2019_08_19_000000_create_failed_jobs_table ............................................................................. 54ms DONE
2019_12_14_000001_create_personal_access_tokens_table .................................................................. 81ms DONE
INFO Seeding database.
% ./vendor/bin/sail artisan key:gen
INFO Application key set successfully.
% ./vendor/bin/sail npm run dev
まあここではlaravel sailを使っているけどsailなんて必要ないって人は単純にそこは省いてもらえばokだろう。
ユーザーを1人作る
database/seeders/DatabaseSeeder.php にあるやつをコメント外すくらいでok
<?php
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// \App\Models\User::factory(10)->create();
\App\Models\User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
}
}
artisan migrate:fresh --seed
すればログインできるであろう。パスワードはpasswordである。比較的中級以上向けの文章だから、こんなところで躓いていてはいけない。
breezeの機能を見ていく
この殺風景なDashboardはまあjsxのテンプレートとしても使えるけど、これはともかく
このドロップダウンメニューからのprofileで提供される情報はbreezeが作成するもので基本的に退会機能も付いている。これを潰したいとかいう場合はまあ各々頑張っていただくとしてまあその、こういうもんなのだ。ここではProfile Informationを拡張してAvatarを作る。こういうのもまあAIで適当にでっち上げてしまった
まず当該のProfileのコード部分を確認しよう
まず、Profileの当該部分を見れば明かなようにこれはEditを即座に提示しているので(要するにShowとかじゃないので)当該viewは resources/js/Pages/Profile/Edit.jsx となる。おそらくコードはspace 4インデントになっているが、以降では2に修正する(まあ紙面の関係もありますし)
import { Head } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import DeleteUserForm from './Partials/DeleteUserForm';
import UpdatePasswordForm from './Partials/UpdatePasswordForm';
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';
export default function Edit({ auth, mustVerifyEmail, status }) {
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Profile</h2>}
>
<Head title="Profile" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<UpdateProfileInformationForm
mustVerifyEmail={mustVerifyEmail}
status={status}
className="max-w-xl"
/>
</div>
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<UpdatePasswordForm className="max-w-xl" />
</div>
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<DeleteUserForm className="max-w-xl" />
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
このようにおおきく
UpdateProfileInformationForm
UpdatePasswordForm
DeleteUserForm
に別れていることが理解できる。変更するのは当然UpdateProfileInformationForm という事になるだろうそれは
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';
が示しているように resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx になる。このソースコードを見ていこう。例によってindentは修正済み
UpdateProfileInformationFormのソースコード
import { Link, useForm, usePage } from '@inertiajs/react';
import { Transition } from '@headlessui/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
export default function UpdateProfileInformation({ mustVerifyEmail, status, className = '' }) {
const { user } = usePage().props.auth;
const {
data, setData, patch, errors, processing, recentlySuccessful,
} = useForm({
name: user.name,
email: user.email,
});
const submit = (e) => {
e.preventDefault();
patch(route('profile.update'));
};
return (
<section className={className}>
<header>
<h2 className="text-lg font-medium text-gray-900">Profile Information</h2>
<p className="mt-1 text-sm text-gray-600">
Update your account's profile information and email address.
</p>
</header>
<form onSubmit={submit} className="mt-6 space-y-6">
<div>
<InputLabel htmlFor="name" value="Name" />
<TextInput
id="name"
className="mt-1 block w-full"
value={data.name}
onChange={(e) => setData('name', e.target.value)}
required
isFocused
autoComplete="name"
/>
<InputError className="mt-2" message={errors.name} />
</div>
<div>
<InputLabel htmlFor="email" value="Email" />
<TextInput
id="email"
type="email"
className="mt-1 block w-full"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
required
autoComplete="username"
/>
<InputError className="mt-2" message={errors.email} />
</div>
{mustVerifyEmail && user.email_verified_at === null && (
<div>
<p className="text-sm mt-2 text-gray-800">
Your email address is unverified.
<Link
href={route('verification.send')}
method="post"
as="button"
className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Click here to re-send the verification email.
</Link>
</p>
{status === 'verification-link-sent' && (
<div className="mt-2 font-medium text-sm text-green-600">
A new verification link has been sent to your email address.
</div>
)}
</div>
)}
<div className="flex items-center gap-4">
<PrimaryButton disabled={processing}>Save</PrimaryButton>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600">Saved.</p>
</Transition>
</div>
</form>
</section>
);
}
いよいよアップロードフォームを作成する
まず、シンプルな方法でやってみよう。
まずEmailをコピって、、、といいたいところだが
{mustVerifyEmail && user.email_verified_at === null && (
<div>
<p className="text-sm mt-2 text-gray-800">
Your email address is unverified.
<Link
href={route('verification.send')}
method="post"
as="button"
className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Click here to re-send the verification email.
</Link>
</p>
{status === 'verification-link-sent' && (
<div className="mt-2 font-medium text-sm text-green-600">
A new verification link has been sent to your email address.
</div>
)}
</div>
)}
という余計な構文が見える。これはemailのvalidationが完了していない時に出てくる。こういうのもあるんですわ。まあ今回はこれはどうでもいいんだけど、この上に書くとまずいので下にemailの部分をコピってみよう
<div>
<InputLabel htmlFor="email" value="Email" />
<TextInput
id="email"
type="email"
className="mt-1 block w-full"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
required
autoComplete="username"
/>
<InputError className="mt-2" message={errors.email} />
</div>
{mustVerifyEmail && user.email_verified_at === null && (
<div>
<p className="text-sm mt-2 text-gray-800">
Your email address is unverified.
<Link
href={route('verification.send')}
method="post"
as="button"
className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Click here to re-send the verification email.
</Link>
</p>
{status === 'verification-link-sent' && (
<div className="mt-2 font-medium text-sm text-green-600">
A new verification link has been sent to your email address.
</div>
)}
</div>
)}
{/* コピーされた部分 */}
<div>
<InputLabel htmlFor="email" value="Email" />
<TextInput
id="email"
type="email"
className="mt-1 block w-full"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
required
autoComplete="username"
/>
<InputError className="mt-2" message={errors.email} />
</div>
すると
こういう感じになるので、これを改造していく。
<div>
<InputLabel htmlFor="avatar" value="Avatar" />
<input
id="avatar"
type="file"
className="mt-1 block w-full"
onChange={e => setData('avatar', e.target.files[0])}
/>
<InputError className="mt-2" message={errors.avatar} />
</div>
ファイルの場合はまあこんな感じでやるってことで
このようにclassicalなタイプのアップローダーが出現した。ただ、これではuploadできない。inertia.jsの場合はpostする時のキーをちゃんと来めておかないといけない。
export default function UpdateProfileInformation({ mustVerifyEmail, status, className = '' }) {
const { user } = usePage().props.auth;
const {
data, setData, patch, errors, processing, recentlySuccessful,
} = useForm({
name: user.name,
email: user.email,
avatar: null,
});
このようにavatarを付け加えた
さて、
const submit = (e) => {
e.preventDefault();
patch(route('profile.update'));
};
これによりprofile.updateにpatchされる。
それは app/Http/Controllers/ProfileController.php である。ここで一度リクエストを停止してみよう
public function update(ProfileUpdateRequest $request): RedirectResponse
{
dd($request->all()); // ダンプして停止
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
そうすると、画像を送ってないときは
こうなるのに、画像を送ったときは
[] とかいう空配列になり全データーが壊滅状態になる。これはまずinertia.jsのハマりポイントなので解説する(ドキュメントに書いてあるんですけどね)
修正していく
ここで重要な告知があるが、putとかpatchではファイルをアップロードできない(重要)。
従ってpostをつかう
const {
data, setData, post, errors, processing, recentlySuccessful,
} = useForm({
name: user.name,
email: user.email,
avatar: null,
});
const submit = (e) => {
e.preventDefault();
post(route('profile.update'), {});
};
しかしこの状態では当然
こうなるのでpatchを偽装する
const {
data, setData, post, errors, processing, recentlySuccessful,
} = useForm({
name: user.name,
email: user.email,
avatar: null,
_method: "patch",
});
const submit = (e) => {
e.preventDefault();
post(route('profile.update'));
};
ドキュメントにあるrouter.patchを使ってもいいけどコードが長くなるので、これでも大丈夫
このように正しくdumpできれば後はbackendの作業となる。ま、長くなったので次回に続く、かどうかはおれの気力次第かw
この記事が気に入ったらサポートをしてみませんか?