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指定はちょっとイカれてると思うので後で直すかもだが、とりあえずこれでボタンは出るようになったはずだ。

Editが出るようになった

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を出すという事になる。

Editボタンを押すと…
このように変化して、Cancelボタンを押すと
元に戻る

ここまで理解できたら次に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が編集モードで見えるようになった。

この上に編集フォームを作成する。まあ、まずはベタっと書いていく。

                {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を押すと

編集できそうなフォームとなった。ここに何故「テスト」と挿入されているかというと

    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を押すと…

ここでは「テスト」を「テスト \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するんか?!って話だけど、するんです。まあそういうもんだと思っといてください。

で、実行結果


改行が入ってないものとなった。これは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>
                }

でまあ、これで大体要件は満たせているが、諸々修正が必要な箇所もあるから、それは後で行っていくことにしよう。


この記事が気に入ったらサポートをしてみませんか?