見出し画像

intertia.js + filepond + spatie media lib決定版 (1) ー 準備


とりあえずlaravel11を上げる

これから作るよ

https://gitlab.com/catatsumuri/laravel11-starter.git

リポジトリのREADMEにも書いてあるけど

cp .env.example .env
docker run --rm -it -v $(pwd):/app composer install --ignore-platform-reqs
./vendor/bin/sail up
./vendor/bin/sail artisan key:generate
./vendor/bin/sail artisan migrate

を行う。

breezeのinstall

./vendor/bin/sail composer require laravel/breeze
./vendor/bin/sail composer require laravel/breeze
./vendor/bin/sail artisan breeze:install react

npm

install

初回は最初から行われていると思うけど一応

./vendor/bin/sail npm install

viteの問題

まあなんとか過去記事見て頑張って。だめならnpm run buildで

一応簡単に書くと

./vendor/bin/sail npm install dotenv

dotenv入れて。sail upし直してVITE_HMR_HOSTとか適当なキーを作って

vite.config.js 

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
import dotenv from 'dotenv';

dotenv.config();
export default defineConfig({
    server: {
        hmr: {
            host: process.env.VITE_HMR_HOST,
        }
    },
    plugins: [
        laravel({
            input: 'resources/js/app.jsx',
            refresh: true,
        }),
        react(),
    ],
});
./vendor/bin/sail npm run dev

一応diff

% git --no-pager diff
diff --git a/.env.example b/.env.example
index 3080702..4a23319 100644
--- a/.env.example
+++ b/.env.example
@@ -63,3 +63,4 @@ AWS_BUCKET=
 AWS_USE_PATH_STYLE_ENDPOINT=false

 VITE_APP_NAME="${APP_NAME}"
+VITE_HMR_HOST=localhost
diff --git a/package-lock.json b/package-lock.json
index 73f9dc5..54f1c8b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,9 @@
     "requires": true,
     "packages": {
         "": {
+            "dependencies": {
+                "dotenv": "^16.4.5"
+            },
             "devDependencies": {
                 "@headlessui/react": "^2.0.0",
                 "@inertiajs/react": "^1.0.0",
@@ -1901,6 +1904,18 @@
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/dotenv": {
+            "version": "16.4.5",
+            "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+            "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+            "license": "BSD-2-Clause",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://dotenvx.com"
+            }
+        },
         "node_modules/eastasianwidth": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
diff --git a/package.json b/package.json
index 30c3879..aae3b6b 100644
--- a/package.json
+++ b/package.json
@@ -18,5 +18,8 @@
         "react-dom": "^18.2.0",
         "tailwindcss": "^3.2.1",
         "vite": "^5.0"
+    },
+    "dependencies": {
+        "dotenv": "^16.4.5"
     }
 }
diff --git a/vite.config.js b/vite.config.js
index 19f2908..82a7a94 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,8 +1,14 @@
 import { defineConfig } from 'vite';
 import laravel from 'laravel-vite-plugin';
 import react from '@vitejs/plugin-react';
+import dotenv from 'dotenv';
+dotenv.config();

 export default defineConfig({
+    server: {
+        hmr: {
+            host: process.env.VITE_HMR_HOST,
+        }
+    },
     plugins: [
         laravel({
             input: 'resources/js/app.jsx',

ようやくスタート。開発までの時間が長過ぎるのがwebアプリの難点だぜ…

設計

ここでは何かpostを作ってそれにファイルを添付できるようにする。postは新規投稿時にファイルを添付できるし、編集もできるようにする。そしてファイルは複数添付できるようにする。この章はとりあえずファイルの添付を行う前まで到達する事を目標とする。

まずログインを確認する

breezeはログインをサポートしているので、そいつの機能が動くかどうか確認してみよう。

database/seeders/DatabaseSeeder.php  ここに

<?php

namespace Database\Seeders;

use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // User::factory(10)->create();

        User::factory()->create([
            'name' => 'Test User',
            'email' => 'test@example.com',
        ]);
    }
}

とか書いてあるので一度seedingする

./vendor/bin/sail artisan migrate:fresh --seed

これでtest@example.com / passwordでログインできるかと思う


ログイン直後

post関連リソースの作成

でまあ、dashboardに書いていってもいいんだけど、なんか統合するの面倒だからここはpostを別に分けて作っちゃう。

% ./vendor/bin/sail php artisan make:model Post -mrsf

すると

% ./vendor/bin/sail php artisan make:model Post -mrsf


   INFO  Model [app/Models/Post.php] created successfully.

   INFO  Factory [database/factories/PostFactory.php] created successfully.

   INFO  Migration [database/migrations/2024_07_18_010056_create_posts_table.php] created successfully.

   INFO  Seeder [database/seeders/PostSeeder.php] created successfully.

   INFO  Controller [app/Http/Controllers/PostController.php] created successfully.

となる。まずdatabase/migrations/xxxx_create_posts_table.php とかを編集しよう。

    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('content'); // これ 
            $table->timestamps();
        });
    }

contentを加えただけ。postでもいいけどpostsテーブルのpostってややこしいかなとかいうそれだけ。ログインセッションあるならuser_idも書いておけよっていう気もするけどリレーション作るのとか地味に解説面倒だからやりたい人は各自頑張って

開発フェーズは構造変えたらいちいちfreshしちゃうのが楽と思うよ

./vendor/bin/sail artisan migrate:fresh --seed

contentリソースの作成

routes/web.php 

use App\Http\Controllers\PostController;
// ...
Route::get('/dashboard', function () {
    return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

// ↓ これ
Route::resource('posts', PostController::class);

レイアウトに動線を

resources/js/Layouts/AuthenticatedLayout.jsx 
これ一応レスポンシブとそうでないの2つメニューがあるので

<NavLink href={route('dashboard')} active={route().current('dashboard')}>
    Dashboard
</NavLink>
<NavLink href={route('posts.index')} active={route().current('posts.*')}>
    Post
</NavLink>

<ResponsiveNavLink href={route('posts.index')} active={route().current('posts.*')}>
    Post
</ResponsiveNavLink>

これ。両方書いておく。まあ別にfilepondの確認だけならResponsiveは必要ないかもだけど。

posts.indexの作成

use Inertia\Inertia;
use Inertia\Response;
class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): Response
    {
        return Inertia::render('Posts/Index', [
        ]);
    }

こんな風にしといての

resources/js/Pages/Posts/Index.jsx 

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

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

      <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">
              test
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
}

まほとんどdashboardのフルコピーみたいなもんすね。

投稿フォームの作成

ここ本題じゃないからサクサクいくよ

resources/js/Pages/Posts/Index.jsx 

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';

export default function PostIndex({ auth }) {
  const { data, setData, post, processing, errors, reset } = useForm({
    content: '',
  });
  const submit = (e) => {
    e.preventDefault();
    post(route('posts.store'), {
      onSuccess: () => reset(),
    });
  };


  return (
    <AuthenticatedLayout
    user={auth.user}
    header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Post</h2>}
  >
      <Head title="Post" />

      <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">


              <form onSubmit={submit}>
                <div>
                  <InputLabel htmlFor="content" value="Content" />

                  <TextInput
                  id="content"
                  type="text"
                  name="content"
                  value={data.content}
                  className="mt-1 block w-full"
                  isFocused={true}
                  onChange={(e) => setData('content', e.target.value)}
                />

                  <InputError message={errors.content} className="mt-2" />
                </div>

                <div className="flex items-center justify-end mt-4">
                  <PrimaryButton className="ms-4" disabled={processing}>
                    Post
                  </PrimaryButton>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
}

こういうフォームの雛形はresources/js/Pages/Auth/Login.jsx とかを参考に組むといいと思います。onSuccessをひっかけてresetしておかないとこのパターンは投稿がフォームに残り続けるので必須。

とりあえず適当に作ったform

保存

validationした方がいいんだろうけど、ここは本題じゃないのでとにかくアバウトに保存
app/Http/Controllers/PostController.php 

use Illuminate\Http\RedirectResponse;
use App\Model\Post;

// ...
    public function store(Request $request)
    {
        $data = $request->all();
        Post::create($data);

        return redirect(route('posts.index'))
            ->with('success', 'Posted');
    }

これだと保存できないのでモデルを変更する
app/Models/Post.php 

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'content'
    ];
}

一覧を取り出して表示(posts.index)

この辺を作りこむのはこの記事にあってないので、全体的にザックリいくよ

    public function index(): Response
    {
        $posts = Post::latest()->get();
        return Inertia::render('Posts/Index', [
            'posts' => $posts
        ]);
    }

そしたら  resources/js/Pages/Posts/Index.jsx で

export default function PostIndex({ auth, posts }) {

で受けて表示する。

時間表示というか経過表示にdate-fnsを使うので入れておくよ

 ./vendor/bin/sail npm install date-fns 

resources/js/Pages/Posts/Index.jsx 

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm, router } from '@inertiajs/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import { formatDistanceToNow } from 'date-fns';
import { ja } from 'date-fns/locale';

export default function PostIndex({ auth, posts }) {
  const { data, setData, post, processing, errors, reset } = useForm({
    content: '',
  });

  const submit = (e) => {
    e.preventDefault();
    router.visit(route('posts.store'), {
      method: 'post',
      data,
      onSuccess: () => reset(),
    });
  };

  return (
    <AuthenticatedLayout
      user={auth.user}
      header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Post</h2>}
    >
      <Head title="Post" />

      <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">
              <form onSubmit={submit}>
                <div>
                  <InputLabel htmlFor="content" value="Content" />
                  <TextInput
                    id="content"
                    type="text"
                    name="content"
                    value={data.content}
                    className="mt-1 block w-full"
                    isFocused={true}
                    onChange={(e) => setData('content', e.target.value)}
                  />
                  <InputError message={errors.content} className="mt-2" />
                </div>
                <div className="flex items-center justify-end mt-4">
                  <PrimaryButton className="ms-4" disabled={processing}>
                    Post
                  </PrimaryButton>
                </div>
              </form>
              <div className="mt-6">
                {posts.map((post) => (
                  <div key={post.id} className="mb-4 p-4 border-b">
                    <div className="text-lg font-semibold">{post.content}</div>
                    <div className="text-gray-500 text-sm">
                      {formatDistanceToNow(new Date(post.created_at), { addSuffix: true, locale: ja })}
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
}

編集

ここまでで大体終わってfilepondに入っていけそうなんだけど実はここでの要件は編集もするってことでしたな。削除は面倒なのでやりまへん。

とりあえずLinkをimportしときます

import { Head, Link, useForm, router } from '@inertiajs/react';

まあこんな感じで

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, useForm, router } from '@inertiajs/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import { formatDistanceToNow } from 'date-fns';
import { ja } from 'date-fns/locale';

export default function PostIndex({ auth, posts }) {
  const { data, setData, post, processing, errors, reset } = useForm({
    content: '',
  });

  const submit = (e) => {
    e.preventDefault();
    router.visit(route('posts.store'), {
      method: 'post',
      data,
      onSuccess: () => reset(),
    });
  };

  return (
    <AuthenticatedLayout
      user={auth.user}
      header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Post</h2>}
    >
      <Head title="Post" />

      <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">
              <form onSubmit={submit}>
                <div>
                  <InputLabel htmlFor="content" value="Content" />
                  <TextInput
                    id="content"
                    type="text"
                    name="content"
                    value={data.content}
                    className="mt-1 block w-full"
                    isFocused={true}
                    onChange={(e) => setData('content', e.target.value)}
                  />
                  <InputError message={errors.content} className="mt-2" />
                </div>
                <div className="flex items-center justify-end mt-4">
                  <PrimaryButton className="ms-4" disabled={processing}>
                    Post
                  </PrimaryButton>
                </div>
              </form>
              <div className="mt-6">
                {posts.map((post) => (
                  <div key={post.id} className="mb-4 p-4 border-b">
                    <div className="text-lg font-semibold">{post.content}</div>
                    <div className="flex justify-end items-center text-gray-500 text-sm space-x-4">
                      <div>
                        {formatDistanceToNow(new Date(post.created_at), { addSuffix: true, locale: ja })}
                      </div>
                      <div>
                        <Link
                          href={route("posts.edit", { post: post.id })}
                          className="text-blue-500 hover:text-blue-700 transition-colors duration-300"
                        >
                          Edit
                        </Link>
                      </div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
}

inlineでeditしちゃってもいいんだけどここでは明確にEditフォームを分けることとする。つまりEditを書かないといけない

編集

app/Http/Controllers/PostController.php 

    public function edit(Post $post): Response
    {
        return Inertia::render('Posts/Edit', [
            'targetedPost' => $post,
        ]);
    }

ここでやらかしてしまいましたねえ…postって名前はusePormのpostって名前と被るので、、targetedPostとしたw すんません、何も考えずに作るもんじゃねえな…
resources/js/Pages/Posts/Edit.jsx 

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, useForm, router } from '@inertiajs/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import { formatDistanceToNow } from 'date-fns';
import { ja } from 'date-fns/locale';

export default function PostEdit({ auth, targetedPost }) {
  const { data, setData, post, put, processing, errors, reset } = useForm({
    content: targetedPost.content,
  });

  const submit = (e) => {
    e.preventDefault();
    put(route('posts.update', {post: targetedPost.id}));
  };

  return (
    <AuthenticatedLayout
      user={auth.user}
      header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Edit Post</h2>}
    >
      <Head title="Edit Post" />

      <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">
              <form onSubmit={submit}>
                <div>
                  <InputLabel htmlFor="content" value="Content" />
                  <TextInput
                    id="content"
                    type="text"
                    name="content"
                    value={data.content}
                    className="mt-1 block w-full"
                    isFocused={true}
                    onChange={(e) => setData('content', e.target.value)}
                  />
                  <InputError message={errors.content} className="mt-2" />
                </div>
                <div className="flex items-center justify-end mt-4">
                  <PrimaryButton className="ms-4" disabled={processing}>
                    Update
                  </PrimaryButton>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
}

ここではput()を使っている、が、しかし最終的にputは使えない。これは次回解説する。

でupdate

    public function update(Request $request, Post $post): RedirectResponse
    {
        $post->update($request->all());
        return redirect(route('posts.index'))
            ->with('success', 'Updated');
    }

これで最低限できましたね。

次回はいよいよfilepondを加えていくよ〜。どうしても準備の前段で1ページ使っちゃうわこれ


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