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しておかないとこのパターンは投稿がフォームに残り続けるので必須。
保存
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ページ使っちゃうわこれ
この記事が気に入ったらサポートをしてみませんか?