blade系package(3-1) form系 (protonemedia/laravel-form-components)
まあアイコンとか何かかんかみてきたけどやっぱりbladeでやるならフォームがないとサマにならんだろうという事で
この2つがある。
まあhttps://packagist.org/packages/protonemedia/laravel-form-components の方がメジャーっぽいからそれを使ってみましょう。bootstrap4とtailwind cssに対応しているという事でござる、が、bootstrap5にも普通に対応しているので安心していいと思う。
なお今回はtailwind cssを使ってみるから、自分のレポジトリから以下のようにcloneした
% git clone https://gitlab.com/catatsumuri/laravel10-starter.git -b breeze-blade
なんだかんだでbreeze:installしてtailwindが使える環境を作る必要がある。
install
composer require protonemedia/laravel-form-components
わいの作業ログ
% ./vendor/bin/sail composer require protonemedia/laravel-form-components
./composer.json has been updated
Running composer update protonemedia/laravel-form-components
Loading composer repositories with package information
Updating dependencies
Nothing to modify in lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Downloading protonemedia/laravel-form-components (3.8.0)
- Installing protonemedia/laravel-form-components (3.8.0): Extracting archive
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
INFO Discovering packages.
laravel/breeze ........................................................ DONE
laravel/sail .......................................................... DONE
laravel/sanctum ....................................................... DONE
laravel/tinker ........................................................ DONE
nesbot/carbon ......................................................... DONE
nunomaduro/collision .................................................. DONE
nunomaduro/termwind ................................................... DONE
protonemedia/laravel-form-components .................................. DONE
spatie/laravel-ignition ............................................... DONE
83 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> @php artisan vendor:publish --tag=laravel-assets --ansi --force
INFO No publishable resources for tag [laravel-assets].
No security vulnerability advisories found
Using version ^3.8 for protonemedia/laravel-form-components
さくっとなんか作ってみる
まあよくあるPostsテーブルとか
% ./vendor/bin/sail artisan make:model Post -mrc
INFO Model [app/Models/Post.php] created successfully.
INFO Migration [database/migrations/2023_08_28_204738_create_posts_table.php] created successfully.
INFO Controller [app/Http/Controllers/PostController.php] created successfully.
定義
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
});
routes/web.php
use App\Http\Controllers\PostController;
// ...
Route::resource('posts', PostController::class);
認証無しってことで。
これで http://localhost/posts みたいな奴でアクセスできるはずだ。今回は面倒なので、createに飛ばさずここにformを作ってみる。
app/Http/Controllers/PostController.php
use Illuminate\View\View;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): View
{
return view('posts.index');
}
で、resources/views/auth/login.blade.php を参考に組み立てる。まずはコピペソース
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button class="ml-3">
{{ __('Log in') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>
まず、このcomponentを使わずに書いてみる
まあ、やってみよう。適当にコピペしすぎて色々間違っておりEmailじゃなくて実際Titleだし、Loginでもないのでその辺を書き換えていく。
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<div>
<x-input-label for="title" :value="__('Title')" />
<x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title')" />
<x-input-error :messages="$errors->get('title')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button class="ml-3">
{{ __('Post') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>
まあ実際これもこれでbreezeが置いていったcomponentを利用しているわけだが、とはいえ、これで投稿できる。
public function store(Request $request)
{
dd($request->all());
}
実際の保存処理
もうちょい真面目にしていこう。
app/Models/Post.php にfillableを書いて
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title'
];
}
controllerで保存
use Illuminate\Http\RedirectResponse;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): View
{
$posts = Post::latest()->get();
return view('posts.index', [
'posts' => $posts,
];
}
// ...
public function store(Request $request): RedirectResponse
{
$data = $request->all();
Post::create($data);
return redirect(route('posts.index'))
->with('status', __('New post created'));
;
}
view
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<div>
<x-input-label for="title" :value="__('Title')" />
<x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title')" />
<x-input-error :messages="$errors->get('title')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button class="ml-3">
{{ __('Post') }}
</x-primary-button>
</div>
</form>
<ul>
@foreach ($posts as $post)
<li>
{{ $post->title }}
({{ $post->created_at }})
</li>
@endforeach
</ul>
</x-guest-layout>
極めてダセエが、まあデモとしては十分だろう、とりあえず書けてはいるって感じだね。
パッケージを置き換えてみる
こういうのは、どうしても長ったらしくなってあれなんだけど…まあinstallしておけばいきなり使えていたりする
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
{{--
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<div>
<x-input-label for="title" :value="__('Title')" />
<x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title')" />
<x-input-error :messages="$errors->get('title')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button class="ml-3">
{{ __('Post') }}
</x-primary-button>
</div>
</form>
--}}
<x-form>
</x-form>
<ul>
@foreach ($posts as $post)
<li>
{{ $post->title }}
({{ $post->created_at }})
</li>
@endforeach
</ul>
</x-guest-layout>
とかって<x-form></x-form>に置き換えたりすると
<form method="POST">
<input type="hidden" name="_token" value="BAdy6rv5lEM6BceMp1ZceOTI3Aw4xboBrihAmnSd">
</form>
などというhtmlが出力されている。これに従ってどんどん書き換えてみる
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
{{--
<x-input-label for="title" :value="__('Title')" />
<x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title')" />
<x-input-error :messages="$errors->get('title')" class="mt-2" />
--}}
<x-form action="{{ route('posts.store') }}">
<div>
<x-form-input name="title" label="{{ __('Title') }}" />
</div>
<div class="flex items-center justify-end mt-4">
<x-form-submit class="ml-3">
{{ __('Post') }}
</x-form-submit>
</div>
</x-form>
<ul>
@foreach ($posts as $post)
<li>
{{ $post->title }}
({{ $post->created_at }})
</li>
@endforeach
</ul>
</x-guest-layout>
すると
まあこんなような見た目になる。ラベルがinputと分離していないのが特徴といえば特徴である。
エラーとか
今、エラーの部分を書いていないのでサクっと書いちまおう。Titleを空にするとDBのnot nullエラーが出ているはずなので簡単にvalidationする
public function store(Request $request): RedirectResponse
{
$request->validate([
'title' => ['required'],
]);
$data = $request->all();
Post::create($data);
return redirect(route('posts.index'))
->with('status', __('New post created'));
;
}
そうすると、このように出してはくれる
デザイン面
見ての通り、とりわけtailwindを使う場合はカスタムしないと結構しんどい。bootstrapならまあこれでもいいんだろうけど、たとえば
<div>
<x-form-input name="title" label="{{ __('Title') }}"
class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" />
</div>
こういう風に鬼のinlineクラスを書いてもいいんだろうけど、毎回書くんですか?って話になっちゃうから、tailwindの場合viewのカスタムは必然的に発生する予感がする。
ってわけで
php artisan vendor:publish --provider="ProtoneMedia\LaravelFormComponents\Support\ServiceProvider"
とかやるとimportされてくる
% ./vendor/bin/sail artisan vendor:publish --provider="ProtoneMedia\LaravelFormComponents\Support\ServiceProvider"
INFO Publishing assets.
Copying file [vendor/protonemedia/laravel-form-components/config/config.php] to [config/form-components.php] DONE
Copying directory [vendor/protonemedia/laravel-form-components/resources/views] to [resources/views/vendor/form-components] DONE
configを見ると
/** tailwind | tailwind-2 | tailwind-forms-simple | bootstrap-4 | bootstrap-5 */
'framework' => 'tailwind',
などあって、今はtailwind3なのでついていけてないのだが、まあここはtailwind2にして何とかしてみるかと
'framework' => 'tailwind-2',
resources/views/vendor/form-components/tailwind-2/form-input.blade.php
<div class="@if($type === 'hidden') hidden @else mt-4 @endif">
<label class="block">
<x-form-label :label="$label" />
<input {!! $attributes->merge([
'class' => 'block w-full ' . ($label ? 'mt-1' : '')
]) !!}
@if($isWired())
wire:model{!! $wireModifier() !!}="{{ $name }}"
@else
value="{{ $value }}"
@endif
name="{{ $name }}"
type="{{ $type }}" />
</label>
@if($hasErrorAndShow($name))
<x-form-errors :name="$name" />
@endif
</div>
こんな感じになっているから
<div class="@if($type === 'hidden') hidden @else mt-4 @endif">
<label class="block">
<x-form-label :label="$label" />
<input {!! $attributes->merge([
'class' => 'block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm ' . ($label ? 'mt-1' : '')
]) !!}
@if($isWired())
wire:model{!! $wireModifier() !!}="{{ $name }}"
@else
value="{{ $value }}"
@endif
name="{{ $name }}"
type="{{ $type }}" />
</label>
@if($hasErrorAndShow($name))
<x-form-errors :name="$name" />
@endif
</div>
こんな感じにしたり、errorを
@error($name, $bag)
<p {!! $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) !!}>
{{ $message }}
</p>
@enderror
こんな感じにしたりしている
もう1つ入力要素を追加
今だとちょっとold inputがわかり辛いので…
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
artisan migrate:fresh --seed
とかしてDBをresetして
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<x-form action="{{ route('posts.store') }}">
<x-form-input name="title" label="{{ __('Title') }}" />
<x-form-textarea name="body" label="{{ __('Body') }}" />
<div class="flex items-center justify-end mt-4">
<x-form-submit class="ml-3">
{{ __('Post') }}
</x-form-submit>
</div>
</x-form>
<ul>
@foreach ($posts as $post)
<li>
{{ $post->title }}
({{ $post->created_at }})
</li>
@endforeach
</ul>
</x-guest-layout>
とかすると
となったりするのでtextareaの色とかをちょっと変更する。最初の作りこみだけが面倒くさいっちゃそう。
resources/views/vendor/form-components/tailwind-2/form-textarea.blade.php
<div class="mt-4">
<label class="block">
<x-form-label :label="$label" />
<textarea
@if($isWired())
wire:model{!! $wireModifier() !!}="{{ $name }}"
@endif
name="{{ $name }}"
{!! $attributes->merge(['class' => 'block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm' . ($label ? 'mt-1' : '')]) !!}
>@unless($isWired()){!! $value !!}@endunless</textarea>
</label>
@if($hasErrorAndShow($name))
<x-form-errors :name="$name" />
@endif
</div>
validationも2つ作る
public function store(Request $request): RedirectResponse
{
$request->validate([
'title' => ['required'],
'body' => ['required'],
]);
このように、正しくold inputが出ていればok(この辺も本当はtest書いとくといいんすけどね)
ボタンに関して丸くしたければ
resources/views/vendor/form-components/tailwind-2/form-submit.blade.php
<div class="mt-6 flex items-center justify-between">
<button {!! $attributes->merge([
'class' => 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 focus:outline-none focus:shadow-outline rounded',
'type' => 'submit'
]) !!}>
{!! trim($slot) ?: __('Submit') !!}
</button>
</div>
などなど
次回もうちょい、いじり倒していこう。編集とかも検証が必要だろうし。
ただ、
% ls resources/views/vendor/form-components
bootstrap-4 bootstrap-5 tailwind tailwind-2 tailwind-forms-simple
このように漏れ無く全部inportされるので使わないものは捨てていいと思いますよ
この記事が気に入ったらサポートをしてみませんか?