見出し画像

inertia.jsでinline編集


完成図

これはlaravel bootcampでも見られたものであるが


まあ、やってみよう

事前準備

 artisan make:model Demo -mrcf

とか。今回はdemosテーブルにbodyというテーブルを1つ作って編集するだけ

    public function up(): void
    {
        Schema::create('demos', function (Blueprint $table) {
            $table->id();
            $table->text('body');
            $table->timestamps();
        });
    }

factory 

    public function definition(): array
    {
        return [
            'body' => fake()->realText(100),
        ];
    }

seed。ポイントとしては2う以上作る。2つ以上なら何個でもいいが今回は3とした。

        \App\Models\Demo::factory(3)->create();

テキストデーターの表示

今回はdemos.indexで全てやることにする。一応resourceルートになっているのでroutes.webは以下のようにしてあるもんとしよう

    Route::resource('demos', DemoController::class);

DemoController@index

inertiaなのでこんな感じだろう。

use Inertia\Inertia;
use Inertia\Response;
class DemoController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): Response
    {
        $demos = Demo::latest()->get();
        return Inertia::render('Demos/Index', [
            'demos' => $demos,
        ]);
    }

Demos/Index.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import { Head } from '@inertiajs/react'

export default function DemoIndex({ auth, demos }) {
  return (
    <AuthenticatedLayout
      user={auth.user}
      header={
        <h2 className="font-semibold text-xl text-gray-800 leading-tight">
          Demos
        </h2>
      }
    >
      <Head title="Demos" />

      <div className="py-12">
        <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
          <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
            <div className="p-6 text-gray-900">
              {demos.map((demo, index) => (
                <div key={index} className="bg-white border border-gray-300 rounded-lg shadow-sm p-4 mb-4 hover:shadow-md transition-shadow duration-200">
                  <p className="text-gray-900">{demo.body}</p>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  )
}


まこんなもんだろう。

編集ボタンつけたりコンポーネント化したり

このままの段階だとstateが管理できないのだが、まあその管理できなさも含めてとりあえずコンポーネントなしで作っていって後で分けよう。

              {demos.map((demo, index) => (
                <div key={index} className="bg-white border border-gray-300 rounded-lg shadow-sm p-4 mb-4 hover:shadow-md transition-shadow duration-200 relative flex flex-col justify-between">
                  <p className="text-gray-900">
                    {demo.body}
                  </p>
                  <div className="mt-auto flex justify-end">
                    <PrimaryButton>編集</PrimaryButton>
                  </div>
                </div>
              ))}

こんな感じでボタンを付ける

stateを動かす

import React, { useState } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import { Head } from '@inertiajs/react'
import PrimaryButton from '@/Components/PrimaryButton';

export default function DemoIndex({ auth, demos }) {
  const [isEditing, setIsEditing] = useState(false);

などと書いておいてisEditing flagを管理するようにする

                  <p className="text-gray-900">
                    {demo.body}
                  </p>

                  <div className="mt-auto flex justify-end">
                    {!isEditing ? (
                      <PrimaryButton onClick={() => setIsEditing(true)}>編集</PrimaryButton>
                    ) : (
                      <div>
                        <PrimaryButton className="mr-2">保存</PrimaryButton>
                        <PrimaryButton onClick={() => setIsEditing(false)}>キャ>ンセル</PrimaryButton>
                      </div>
                    )}
                  </div>

このようにする。すると以下のようにstateが変化してボタンが変化するだろう

今、投稿ごとにstateを管理していないので全てのボタンに対してflagが適用されている。これを投稿ごとにstate管理する場合通常コンポーネント化する必要がある。

コンポーネント化する

これは resources/js/Components/DemoItem.jsx に作成するものとする

import React, { useState } from 'react';
import PrimaryButton from '@/Components/PrimaryButton';

export default function DemoItem({ demo }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <div className="bg-white border border-gray-300 rounded-lg shadow-sm p-4 mb-4 hover:shadow-md transition-shadow duration-200 relative flex flex-col justify-between">
      <p className="text-gray-900">
        {demo.body}
      </p>
      <div className="mt-auto flex justify-end">
        {!isEditing ? (
          <PrimaryButton onClick={() => setIsEditing(true)}>編集</PrimaryButton>
        ) : (
          <div>
            <PrimaryButton className="mr-2">保存</PrimaryButton>
            <PrimaryButton onClick={() => setIsEditing(false)}>キャンセル</PrimaryButton>
          </div>
        )}
      </div>
    </div>
  );
}

このように切り離したら本体は
resources/js/Pages/Demos/Index.jsx 

import React, { useState } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import { Head } from '@inertiajs/react'
import PrimaryButton from '@/Components/PrimaryButton';
import DemoItem from '@/Components/DemoItem';

export default function DemoIndex({ auth, demos }) {
  const [isEditing, setIsEditing] = useState(false);
  return (
    <AuthenticatedLayout
      user={auth.user}
      header={
        <h2 className="font-semibold text-xl text-gray-800 leading-tight">
          Demos
        </h2>
      }
    >
      <Head title="Demos" />

      <div className="py-12">
        <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
          <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
            <div className="p-6 text-gray-900">
              {demos.map((demo, index) => (
                <DemoItem key={index} demo={demo} />
              ))}
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  )
}

こんな感じになるだろう。

そうすると、投稿ごとにstateが分けられるから


1つずつ処理するのもまた可能ということになるわけだ。

デザインを作りこんでいく

まず、inertia.jsでformを扱う場合はuseFormするので、これをimportしておく。この辺りの話は全てcomponentでの話となる
resources/js/Components/DemoItem.jsx 

import React, { useState } from 'react';
import PrimaryButton from '@/Components/PrimaryButton';
import DangerButton from '@/Components/DangerButton';

import { useForm } from '@inertiajs/react';

export default function DemoItem({ demo }) {
  const [isEditing, setIsEditing] = useState(false);
  const { data, setData, patch, clearErrors, reset, errors } = useForm({
    body: demo.body,
  });
  const submit = (e) => {
    e.preventDefault();
  };

このようにbodyの初期値にはdemo.bodyを投入しておく

あと、キャンセルをDangerButtonとした。submit関数は今は何もしない。

残りのjsxの部分はまあこんな感じ

  return (
    <div className="bg-white border border-gray-300 rounded-lg shadow-sm p-4 mb-4 hover:shadow-md transition-shadow duration-200 relative flex flex-col justify-between">

      {isEditing ?
          <form onSubmit={submit}>
            <textarea value={data.body}
              onChange={e => setData('body', 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>
            <PrimaryButton className="mr-2">保存</PrimaryButton>
          </form>
          : <p className="text-gray-900">{demo.body}</p>
      }

      <div className="mt-auto flex justify-end">
        {!isEditing ? (
          <PrimaryButton onClick={() => setIsEditing(true)}>編集</PrimaryButton>
        ) : (
          <div>
            <DangerButton onClick={() => setIsEditing(false)}>キャンセル</DangerButton>
          </div>
        )}
      </div>
    </div>
  );

まあ一見よさそうだ。

実際に保存していく

まあ保存というかupdateというかapp/Http/Controllers/DemoController.php 

    public function update(Request $request, Demo $demo)
    {
        dd($request->all());
    }

いつものようにrequestを待機しておく。

   const submit = (e) => {
    e.preventDefault();
    patch(route('demos.update', demo.id), { onSuccess: () => setIsEditing(false) });
  };

submitをこんな風にすればupdateにリクエストがぶん投げられるはずだ

全てがokっぽいから更新してredirectしてあげる

    public function update(Request $request, Demo $demo): RedirectResponse
    {
        $demo->body = $request->body;
        $demo->update();
        return redirect(route('demos.index'));
    }

ちょっとviewを微調整したぞい

import React, { useState } from 'react';
import PrimaryButton from '@/Components/PrimaryButton';
import DangerButton from '@/Components/DangerButton';
import { useForm } from '@inertiajs/react';

export default function DemoItem({ demo }) {
  const [isEditing, setIsEditing] = useState(false);
  const { data, setData, patch, clearErrors, reset, errors } = useForm({
    body: demo.body,
  });
  const submit = (e) => {
    e.preventDefault();
    patch(route('demos.update', demo.id), { onSuccess: () => setIsEditing(false) });
  };

  return (
    <div className="bg-white border border-gray-300 rounded-lg shadow-sm p-4 mb-4 hover:shadow-md transition-shadow duration-200 relative flex flex-col justify-between">

      {isEditing ?
          <form onSubmit={submit}>
            <textarea value={data.body}
              onChange={e => setData('body', e.target.value)}
              className="mt-4 w-full h-32 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>
            <PrimaryButton className="mr-2">保存</PrimaryButton>
          </form>
          : <p className="text-gray-900 whitespace-pre-wrap">{demo.body}</p>
      }

      <div className="mt-auto flex justify-end">
        {!isEditing ? (
          <PrimaryButton onClick={() => setIsEditing(true)}>編集</PrimaryButton>
        ) : (
          <div>
            <DangerButton onClick={() => setIsEditing(false)}>キャンセル</DangerButton>
          </div>
        )}
      </div>
    </div>
  );
}

まあ実際はerror処理だとか認証/認可とかいろいろあるとは思います。

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