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>

とかすると

bodyのtextareaが追加されたが…

となったりするので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'],
        ]);


bodyだけエラーの時にTitleの入力値は維持される

このように、正しく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されるので使わないものは捨てていいと思いますよ


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