laravel + inertia + react のbootcampを見る (その4: Chirpsを編集する(重要))
ここが一番bladeと違う所なのでしっかりやろう。それぞれをコンポーネント化したChirpにstate(要するにコンポーネント内変数とでもいいますか)を持たせインライン編集を行う例である。
routeのupdate
まず、今のrouteが
Route::resource('chirps', ChirpController::class)
->only(['index', 'store'])
->middleware(['auth', 'verified']);
こうなっているので、これを
Route::resource('chirps', ChirpController::class)
->only(['index', 'store', 'update'])
->middleware(['auth', 'verified']);
このようにする。このことからわかるように、編集画面を作る予定はない。なぜなら冒頭にも書いたようにインライン編集を行うからである。
Chirp.jsxの変更
ここではIndex.jsxではなくChirpコンポーネントの方に手を入れるので注意
importの変更
現在冒頭はこのようになっている
import React from 'react';
以下のように変更する。
import Dropdown from '@/Components/Dropdown';
import InputError from '@/Components/InputError';
import PrimaryButton from '@/Components/PrimaryButton';
これは見ての通り要するにパーツを引き込んできた感じだ
インライン編集フォームのための準備
ここではシンプルにChirpのuser_idが認証されているユーザのidと同じの場合を所有者と見做し、編集ボタンを与えている。
まず、この条件分岐だけを書いてみよう。という事は、このセッションで認証済みユーザーオブジェクトが必要なわけだが今それが無い。とりあえず現在のComponent全体である。
import React from 'react';
import Dropdown from '@/Components/Dropdown';
import InputError from '@/Components/InputError';
import PrimaryButton from '@/Components/PrimaryButton';
export default function Chirp({ chirp }) {
return (
<div className="p-6 flex space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<div className="flex-1">
<div className="flex justify-between items-center">
<div>
<span className="text-gray-800">{chirp.user.name}</span>
<small className="ml-2 text-sm text-gray-600">{new Date(chirp.created_at).toLocaleString()}</small>
</div>
</div>
<p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
</div>
</div>
);
}
認証済みユーザのIDとchirpsのuser_idを比較するのであった。以下のように行えばよい。
<div className="flex justify-between items-center">
<div>
<span className="text-gray-800">{chirp.user.name}</span>
<small className="ml-2 text-sm text-gray-600">{new Date(chirp.created_at).toLocaleString()}</small>
</div>
</div>
{chirp.user.id === auth.user.id &&
<div>Owner</div>
}
<p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
このようにすれば記事のOwnerであればOwnderと表示されるだろう。まあ今はユーザーが1人しかいないので漏れ無くOwnerになると思うけど。
ここで先程インポートしたDropdownを利用する
<div className="flex-1">
<div className="flex justify-between items-center">
<div>
<span className="text-gray-800">{chirp.user.name}</span>
<small className="ml-2 text-sm text-gray-600">{new Date(chirp.created_at).toLocaleString()}</small>
</div>
{chirp.user.id === auth.user.id &&
<Dropdown>
<Dropdown.Trigger>
<button>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
<path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />
</svg>
</button>
</Dropdown.Trigger>
<Dropdown.Content>
<button className="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out" >
Edit
</button>
</Dropdown.Content>
</Dropdown>
}
</div>
このbuttonのclass指定はちょっとイカれてると思うので後で直すかもだが、とりあえずこれでボタンは出るようになったはずだ。
![](https://assets.st-note.com/img/1691125605141-qJ6Pq3jZwq.png?width=1200)
Editが押された時の処理
今のままではボタンを押しても何も起きない。それはボタンに何も書いていないからである(真理)、というわけでボタンが押されたときの挙動を書く。ここでは以下のようにしている。
<button className="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out" onClick={() => setEditing(true)}>
Edit
</button>
classNameが相変わらず長くてよくわかんないけどキモは
onClick={() => setEditing(true)}
である。これによりボタンが押されるとsetEditingが呼ばれるのだが、今その関数が書いていないのでそれを記述する必要がある。これを行うにはuseStateを利用する。まず使うための準備をする
import React, { useState } from 'react';
import React from 'react';の変わりにこれを使うようにする。これはご覧の通りreact自体の機能である。そして、このように定義する。
export default function Chirp({ chirp }) {
const { auth } = usePage().props;
const [editing, setEditing] = useState(false);
このuseStateは、2つの配列を取り、1つ目はここではeditingという変数を定義し、2つ目にそのeditingを変更する関数を作成する。そして、useState(false)では最初の初期値を設定するというなかなかな仕様になっている。いずれにせよ、これで今editingなのかeditingじゃないのかの状態を得られるようになっており、これはchirpのモジュール内のみで使い回す。
つまりボタンを押されたときのこの呼び出し
onClick={() => setEditing(true)}
により、editingがtrueになるという事になる(わかりますか?)
useStateに慣れるためのコード
ここではボタンを押すとuseState提供のsetEditing()関数によりeditingがtrueになった事を実感するためだけのコードを記す。
いまmessageを表示している所を以下のように書き換えてみる。
{editing
? <button className="mt-4 border border-gray-300 p-2 rounded" onClick={() => { setEditing(false); }}>Cancel</button>
: <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
}
これはつまりeditingがtrueならCancelボタンを出し、trueならmessageを出すという事になる。
![](https://assets.st-note.com/img/1691130303209-s0Gdbpzy4k.png?width=1200)
![](https://assets.st-note.com/img/1691130328415-Nh80tKfA40.png?width=1200)
![](https://assets.st-note.com/img/1691130350598-q6LoYTVp9c.png?width=1200)
ここまで理解できたら次にCancelボタンのところを編集フォームに変更してみよう。
編集フォーム
{editing
? <form onSubmit={submit}>
<PrimaryButton className="mt-4">Save</PrimaryButton>
<button className="mt-4" onClick={() => { setEditing(false); }}>Cancel</button>
</form>
: <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
}
まずビタビタに書いていく。こうすると、submit関数が必要だったりいろいろ問題が出てくるだろう。というわけでsubmit関数を配置する
const submit = (e) => {
e.preventDefault();
patch(route('chirps.update', chirp.id), { onSuccess: () => setEditing(false) });
};
ここでpatchという関数を呼び出しているが、ここに定義がないので、以下のようにしてひっぱってくる。
import React, { useState } from 'react';
//...
export default function Chirp({ chirp }) {
const { auth } = usePage().props;
const [editing, setEditing] = useState(false);
const submit = (e) => {
e.preventDefault();
patch(route('chirps.update', chirp.id), { onSuccess: () => setEditing(false) });
};
const { data, setData, errors, patch } = useForm({
message: chirp.message,
});
ちなみにCancelボタンは面倒なのでSecondaryButtonにした。なのでコードの全体像はこんな感じになっている。
import React, { useState } from 'react';
import { usePage, useForm } from '@inertiajs/react';
import Dropdown from '@/Components/Dropdown';
import InputError from '@/Components/InputError';
import PrimaryButton from '@/Components/PrimaryButton';
import SecondaryButton from '@/Components/SecondaryButton';
export default function Chirp({ chirp }) {
const { auth } = usePage().props;
const [editing, setEditing] = useState(false);
const submit = (e) => {
e.preventDefault();
patch(route('chirps.update', chirp.id), { onSuccess: () => setEditing(false) });
};
const { data, setData, errors, patch } = useForm({
message: chirp.message,
});
return (
<div className="p-6 flex space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-600 -scale-x-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<div className="flex-1">
<div className="flex justify-between items-center">
<div>
<span className="text-gray-800">{chirp.user.name}</span>
<small className="ml-2 text-sm text-gray-600">{new Date(chirp.created_at).toLocaleString()}</small>
</div>
{chirp.user.id === auth.user.id &&
<Dropdown>
<Dropdown.Trigger>
<button>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
<path d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z" />
</svg>
</button>
</Dropdown.Trigger>
<Dropdown.Content>
<button className="block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:bg-gray-100 transition duration-150 ease-in-out" onClick={() => setEditing(true)} >
Edit
</button>
</Dropdown.Content>
</Dropdown>
}
</div>
{editing
? <form onSubmit={submit}>
<PrimaryButton className="mt-4">Save</PrimaryButton>
<SecondaryButton className="mt-4 ml-2" onClick={() => { setEditing(false);}}>Cancel</SecondaryButton>
</form>
: <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
}
</div>
</div>
);
}
これで、今PrimaryButtonとSecondaryButtonが編集モードで見えるようになった。
![](https://assets.st-note.com/img/1691131289596-aXdYyziJ9K.png?width=1200)
この上に編集フォームを作成する。まあ、まずはベタっと書いていく。
{editing
? <form onSubmit={submit}>
<textarea value={data.message} onChange={e => setData('message', e.target.value)} className="mt-4 w-full text-gray-900 border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"></textarea>
<InputError message={errors.message} className="mt-2" />
<PrimaryButton className="mt-4">Save</PrimaryButton>
<SecondaryButton className="mt-4 ml-2" onClick={() => { setEditing(false);}}>Cancel</SecondaryButton>
</form>
: <p className="mt-4 text-lg text-gray-900">{chirp.message}</p>
}
Editを押すと
![](https://assets.st-note.com/img/1691131620563-cVSTgZgc28.png?width=1200)
編集できそうなフォームとなった。ここに何故「テスト」と挿入されているかというと
const { data, setData, errors, patch } = useForm({
message: chirp.message,
});
ここでの定義による。これはdataに初期値としてmessageにchirp.messageつまり現在のchirpのmessageカラムの内容を詰めているからである。その他のsetDataとかerrorsとかはまあとりあえず書いておけという感じで、最後のpatchはpatchリクエストを行うのでここで書いておけという感じになっている。(具体的にはプロパティ(メンバー変数)的なものからひっぱってきている)
編集を行う
ここからは例によってControllerを更新する。
public function update(Request $request, Chirp $chirp)
{
dd($request->all());
}
app/Http/Controllers/ChirpController.php
とりあえずrequestをdumpしておいてsaveを押すと…
![](https://assets.st-note.com/img/1691132110557-JAdfooSNcX.png)
ここでは「テスト」を「テスト \n テスト」みたいに書き換えてある。これをシンプルに保存する
public function update(Request $request, Chirp $chirp)
{
$validated = $request->validate([
'message' => 'required|string|max:255',
]);
$chirp->update($validated);
return redirect(route('chirps.index'));
}
ここでもredirectするんか?!って話だけど、するんです。まあそういうもんだと思っといてください。
で、実行結果
![](https://assets.st-note.com/img/1691132332130-ZkaQHKccYE.png?width=1200)
改行が入ってないものとなった。これはbootcampのドキュメントに入っていないが、以下のように変更しよう
{editing
? <form onSubmit={submit}>
<textarea value={data.message} onChange={e => setData('message', e.target.value)} className="mt-4 w-full text-gray-900 border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"></textarea>
<InputError message={errors.message} className="mt-2" />
<PrimaryButton className="mt-4">Save</PrimaryButton>
<SecondaryButton className="mt-4 ml-2" onClick={() => { setEditing(false);}}>Cancel</SecondaryButton>
</form>
: <p className="mt-4 text-lg text-gray-900">
{chirp.message.split('\n').map((line, i) => (
<React.Fragment key={i}>
{line}
<br />
</React.Fragment>
))}
</p>
}
でまあ、これで大体要件は満たせているが、諸々修正が必要な箇所もあるから、それは後で行っていくことにしよう。
この記事が気に入ったらサポートをしてみませんか?