【Laravel】Laravel-AdminLTEとlivewireを布教したい

# はじめに
普段小規模な案件を主にやっていて、よく管理画面を作るんですが
Laravel-AdminLTEとlivewireを採用した所かなり効率が良かったので布教したい。
特にLaravel-AdminLTEに付属してるコンポーネントが便利だったのでそれを使いながら
チュートリアルがてらLaravel-AdminLTE＋livewireで非同期掲示板を作っていきます。

参考記事:<a href="https://qiita.com/sgrs38/items/c3eec061f0850046443d">Laravel-AdminLTEの付属コンポーネントで楽にフォームを作ろうぜ</a>
# 目次
<!-- タイトルとアンカー名を編集 -->
1. [環境構築](#環境構築)
1. [モデルとマイグレーション作成](#モデルとマイグレーション作成)
1. [シーダー作成](#シーダー作成)
1. [投稿機能作成](#投稿機能作成)
1. [書き込んでみよう](#書き込んでみよう)
1. [編集機能作成](#編集機能作成)
1. [おわりに](#おわりに)
1. [参考文献](#reference)

<!-- 各チャプター -->
<a id="#環境構築"></a>
# 環境構築
Laravel8をインストール。
<pre>composer create-project --prefer-dist laravel/laravel sample "8.*"</pre>
livewireをインストール、ついでにLaravel-AdminLTEも入れます。
<pre> cd sample
sample> composer require livewire/livewire
sample> composer require jeroennoten/laravel-adminlte
sample> php artisan adminlte:install
sample> php artisan adminlte:install --only=main_views
</pre>
``config/adminlte.php``内のlivewireを適応させます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/6cba6620-94b0-9b0c-5b15-47220751536c.png)
画面上部に@livewireStyleがテキストとして表示されてしまう場合は
``resources/views/vendor/adminlte/master.blade.php``内の
<pre>@livewireStyles
↓
@livewireStyles()

@livewireScripts
↓
@livewireScripts()</pre>に書き換えます。

<a id="#モデルとマイグレーション作成"></a>
# モデルとマイグレーション作成
model作ります、ついでにマイグレーションファイルも作成。
<pre>php artisan make:model Board -m
php artisan make:model Tag -m
php artisan make:model BoardTag -m 
</pre><img width="47%"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/fecc8f67-a8b9-8503-2c19-bf45ab35f4ca.png"> <img width="47%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/c45b56c9-6bf8-6f48-90fc-68f9e897b64d.png"> <img width="45%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/74fbcba5-1493-f5d1-de95-f78187018c46.png">

投稿に付けるタグにリレーションをセット

```php:app/Models/Board.php
<?php

namespace App\Models;

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

class Board extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'contents',
    ];

    public function tags(){
        return $this->belongsToMany(Tag::class,'board_tags');
    }
}
```
<a id="#シーダー作成"></a>
# シーダー作成

<pre>php artisan make:seeder TagsTableSeeder</pre>
タグ作るついでに投稿を1つ作ります

```php:database/seeders/TagsTableSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Board;
use App\Models\Tag;
use Illuminate\Database\Seeder;

class TagsTableSeeder extends Seeder
{
    public function run()
    {
        Tag::insert([
            ['name' => '食べ物'],
            ['name' => '投げ物'],
            ['name' => '偽物'],
            ['name' => '煮物'],
        ]);

        $board = Board::create([
            'name' => '名無しさん',
            'contents' => 'こんちゃ'
        ]);
        
        $board->tags()->attach([1,2]);
    }
}
```

```php:database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call(TagsTableSeeder::class);
    }
}
```

<a id="#投稿機能作成"></a>
# 投稿機能作成
掲示板を表示する親コンポーネントと、投稿用の子コンポーネントを作ります
<pre>php artisan make:livewire Board/Index
php artisan make:livewire Board/Create</pre>

テンプレートを継承して投稿データを渡します

```php:app/Http/Livewire/Board/Index.php
<?php

namespace App\Http\Livewire\Board;

use App\Models\Board;
use Livewire\Component;

class Index extends Component
{
    //追記：他コンポーネントから'RefreshBoard'を発火されるとマジックメソッド'$refresh'が発動！！！再レンダリングだ！
    protected $listeners = ['RefreshBoard' => '$refresh'];

    public function render()
    {
        $boards = Board::all();
        return view('livewire.board.index',compact('boards'))
            ->extends('adminlte::page')
            ->section('content');
    }
}
```

bladeも書いてきます

```html:resources/views/livewire/board/index.blade.php
<div>
    <div class="col-12 pt-5">
        @foreach($boards as $board)
            <div class="card col-5 mx-auto">
                <div class="card-body">
                    <h5 class="card-title">{{ $board->name }}</h5>
                    <p class="card-text">{{ $board->contents }}</p>
                    @foreach($board->tags as $tag)
                        <span class="badge badge-info">{{ $tag->name }}</span>
                    @endforeach
                </div>
            </div>
        @endforeach
    </div>
</div>

```
ルーティングも書きます

```php:routes/web.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/',App\Http\Livewire\Board\Index::class);
```
マイグレーション&シーダーコマンドを叩いて画面を見てみましょう
<pre>php artisan migrate:fresh --seed</pre>

はい1件だけ表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/1d123001-1ac1-4da3-5078-3a964e7ca4d9.png)

書き込み用のコンポーネントも書いていきます

```php:app/Http/Livewire/Board/Create.php
<?php

namespace App\Http\Livewire\Board;

use App\Models\Board;
use App\Models\Tag;
use Livewire\Component;

class Create extends Component
{
    public $tags;
    public $select_tags = [];
    public $name;
    public $contents;

    protected $rules = [
        'name' => 'required',
        'contents' => 'required',
    ];

    protected $validationAttributes = [
        'name' => 'なまえ',
        'contents' => 'ないよう'
    ];

    //バリデーションの日本語ファイル用意するのが面倒だったので'required'だけ書き換え
    protected $messages = [
        'required' => ':attributeは必ず指定してください。',
    ];

    //コンストラクタ的な役割、プロパティの初期化とかに使う
    public function mount()
    {
        $this->tags = Tag::all();
    }

    //こいつがメイン、更新が入るとこいつが走る
    public function render()
    {
        return view('livewire.board.create');
    }

    //モーダル展開用
    public function openModal()
    {
        //jsのイベントを発火させる
        $this->dispatchBrowserEvent('show_create_modal');
    }

    //書き込み処理
    public function create()
    {
        //バリデーション発動、ひっかかったらここで止まります
        $this->validate();

        //投稿情報作成
        $board = Board::create([
            'name' => $this->name,
            'contents' => $this->contents
        ]);

        //タグ紐づけ
        $board->tags()->attach($this->select_tags);

        //フラッシュメッセージ
        session()->flash('createMessage', 'かきこんだよ');

        //全てのプロパティを初期化。mountで設定した値も消える
        $this->reset(['name','contents','select_tags']);

        //親コンポーネントのrefreshイベント発火
        $this->emitUp('RefreshBoard');
    }
}

```
mountはページが開いた時に一度だけ動きます。

blade側をLaravel-AdminLTEのbladeコンポーネントで書いてきます。

```html:resources/views/livewire/board/create.blade.php
<div>
    <form wire:submit.prevent="create">
        <x-adminlte-modal wire:ignore.self id="createModal" title="書きこむ" size="md" theme="teal" v-centered
                          static-backdrop scrollable>
            <div class="card">
                <div class="card-body">
                    @if (session()->has('createMessage'))
                        <x-adminlte-alert theme="success" title="Success">
                            {{ session('createMessage') }}
                        </x-adminlte-alert>
                    @else
                        <x-adminlte-input name="name" label="なまえ" wire:model.lazy="name"/>

                        <x-adminlte-textarea name="contents" label="ないよう" rows=5 wire:model.lazy="contents">
                        </x-adminlte-textarea>

                        @foreach($tags as $tag)
                            <div class="custom-control custom-checkbox custom-control-inline">
                                <input type="checkbox" class="custom-control-input" id="{{ $tag->id }}"
                                       value="{{ $tag->id }}" wire:model.lazy="select_tags">
                                <label class="custom-control-label" for="{{ $tag->id }}">{{ $tag->name }}</label>
                            </div>
                        @endforeach
                    @endif
                </div>
            </div>
            <x-slot name="footerSlot">
                <div class="col">
                    <div class="row">
                        <div class="col-6">
                            @if (!session()->has('createMessage'))
                                <x-adminlte-button type="submit" class="btn-lg btn-block" label="登録" theme="success"/>
                            @endif
                        </div>
                        <div class="col-6">
                            <x-adminlte-button theme="secondary" class="btn-lg btn-block" label="閉じる"
                                               data-dismiss="modal"/>
                        </div>
                    </div>
                </div>
            </x-slot>
        </x-adminlte-modal>
    </form>

    <x-adminlte-button label="書きこむ" data-toggle="modal" class="bg-teal mb-3" wire:click="openModal()"/>

    <script>
        //モーダル展開用
        window.addEventListener('show_create_modal', event => {
            $('#createModal').modal('show');
        });
    </script>
</div>
```

 ``<form wire:submit.prevent="create">``
ページをリロードさせない為にpreventを引っ付けています。
参考記事:<a href="https://qiita.com/yokoto/items/27c56ebc4b818167ef9e">【JavaScript】event.preventDefault()が何をするのか</a>

``<x-adminlte-input name="name" label="なまえ" wire:model.lazy="name"/>``
``wire:model``という記述でプロパティとバインドをさせています。
バインドするタイミングをある程度調整できるので、
1文字1文字のリアルタイムの検索やバリデーションをする時は修飾子を書かず、サブミット時にバインドさせたければ``.defer``を使う、など。
今回``.lazy``を使っていますが基本フォームの作成などは``.defer``で良いです。

|修飾子  |バインドするタイミング  |
|---|---|
|無し|入力時 |
|.lazy|入力エリア外にフォーカスされた時 |
|.defer |次のネットワークリクエスト |

参考記事:<a href="https://readouble.com/livewire/2.x/ja/properties.html">Livewire 2.x プロパティ</a>

Laravel-AdminLTEのbladfeコンポーネントがlivewireを想定していないせいか
フォームのエラー表示がされないので少しコンポーネントを改造します。
29行目辺りのif文を書き換えます。

```php:resources/views/vendor/adminlte/components/form/input-group-component.blade.php
    @if($isInvalid() && ! isset($disableFeedback))
        <span class="invalid-feedback d-block" role="alert">
            <strong>{{ $errors->first($errorKey) }}</strong>
        </span>
    @endif
```
これを

```php:resources/views/vendor/adminlte/components/form/input-group-component.blade.php
    @if($errors->has($errorKey) && ! isset($disableFeedback))
        <span class="invalid-feedback d-block" role="alert">
            <strong>{{ $errors->first($errorKey) }}</strong>
        </span>
    @endif
```
こう。

書き込み用の子コンポーネントを親コンポーネントに埋め込みます

```html:resources/views/livewire/board/index.blade.php
<div>
    <div class="col-12 pt-5">
        <!-- 追記 -->
        <div class="col-5 mx-auto">
            <livewire:board.create/>
        </div>
        <!-- 追記ここまで -->
        @foreach($boards as $board)
            <div class="card col-5 mx-auto">
                <div class="card-body">
                    <h5 class="card-title">{{ $board->name }}</h5>
                    <p class="card-text">{{ $board->contents }}</p>
                    @foreach($board->tags as $tag)
                        <span class="badge badge-info">{{ $tag->name }}</span>
                    @endforeach
                </div>
            </div>
        @endforeach
    </div>
</div>
```

モーダル用のボタンが表示されました。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/d696f8e6-bce6-cc4e-08ee-82daad675711.png)

<a id="#書き込んでみよう"></a>
# 書き込んでみよう
モーダルを開いて何も選択せずに登録ボタンを押してみましょう
<img width="48%"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/809db999-d9cb-3ba1-cbd9-2990f3b1e9ef.png"> <img width="48%"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/efa1e142-c8de-33b3-0dc4-a04420f9b7f4.png">
あぁ～...エラー表示処理書かなくてもエラー出してくれるの良いわぁ...便利やわぁ

適当に書き込んでみましょう。
<img width="48%"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/4dddcf1b-2d08-a3c6-a5d0-c77c47134332.png"> <img width="48%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/2baed16a-95c6-daaf-506a-a40467b779b9.png">

書き込んだら「閉じる」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/f6d9fbf2-e671-4337-8fe6-bc8e7c456ac9.png)
書き込まれてますね、良きかな。

<a id="#編集機能作成"></a>
# 編集機能作成
編集用のコンポーネントを作ります
<pre>php artisan make:livewire Board/Edit</pre>

```php:app/Http/Livewire/Board/Edit.php
<?php

namespace App\Http\Livewire\Board;

use App\Models\Tag;
use Livewire\Component;

class Edit extends Component
{
    public $board;
    public $tags;
    public $select_tags = [];

    protected $rules = [
        'board.name' => 'required',
        'board.contents' => 'required',
    ];

    protected $validationAttributes = [
        'board.name' => 'なまえ',
        'board.contents' => 'ないよう'
    ];

    public function mount()
    {
        $this->tags = Tag::all();
        //紐づいてるタグのIDを配列として取得
        $this->select_tags = array_map('strval',
            $this->board->tags()->get()->pluck('id')->toArray());
    }

    public function render()
    {
        return view('livewire.board.edit');
    }

    public function openModal()
    {
        $this->dispatchBrowserEvent('show_edit_modal_'.$this->board->id);
    }

    public function update()
    {
        $this->validate();
        
        $this->board->save();
        $this->board->tags()->detach();
        $this->board->tags()->attach($this->select_tags);

        session()->flash('updateMessage', 'こうしんしたよ');

        $this->emitUp('RefreshBoard');
    }
}
```

※最新のバージョンでは気にしなくて良いようになりました
~~mountメソッド内にある
``$this->select_tags = array_map('strval',$this->board->tags()->get()->pluck('id')->toArray());``
どうして配列の中身をstring型にしてるかと言うと、入力フォームからバインドされる値が文字列なので、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/ebb47adc-0049-2272-37f3-b883db7e33d0.png)
この状態から食べ物タグをクリックすると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/5342a2ce-a0bd-a278-9215-66e0ff8d3b0d.png)
食べ物のIDだけstringになっています。タグを追加するだけなら問題無いのですが、タグを解除しようとすると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/0bc9fe3a-268d-2bbf-3344-1058a5a9b3dd.png)
偽物タグをクリックしても消えてくれません。再度偽物タグをクリックすると？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/7577d15e-1dcc-1275-3369-07769156f082.png)
はいstring型のIDが追加されました。もうグチャグチャです。
めちゃくちゃ気持ち悪い挙動になるので気になった人は配列の中身をstring型にせずに弄ってみてください。~~


```html:resources/views/livewire/board/edit.blade.php
<div>
    <form wire:submit.prevent="update">
        <x-adminlte-modal wire:ignore.self id="updateModal_{{ $board->id }}" title="書きこむ" size="md" theme="primary"
                          v-centered static-backdrop scrollable>
            <div class="card">
                <div class="card-body">
                    @if (session()->has('updateMessage'))
                        <x-adminlte-alert theme="success" title="Success">
                            {{ session('updateMessage') }}
                        </x-adminlte-alert>
                    @else
                        <x-adminlte-input name="board.name" label="なまえ" wire:model.lazy="board.name"/>

                        <x-adminlte-textarea name="board.contents" label="ないよう" rows=5 wire:model.lazy="board.contents">
                        </x-adminlte-textarea>

                        @foreach($tags as $tag)
                            <div class="custom-control custom-checkbox custom-control-inline">
                                <input type="checkbox" class="custom-control-input" id="{{ $tag->id }}_{{ $board->id }}"
                                       value="{{ $tag->id }}" wire:model.lazy="select_tags">
                                <label class="custom-control-label"
                                       for="{{ $tag->id }}_{{ $board->id }}">{{ $tag->name }}</label>
                            </div>
                        @endforeach
                    @endif
                </div>
            </div>
            <x-slot name="footerSlot">
                <div class="col">
                    <div class="row">
                        <div class="col-6">
                            @if (!session()->has('updateMessage'))
                                <x-adminlte-button type="submit" class="btn-lg btn-block" label="更新" theme="success"/>
                            @endif
                        </div>
                        <div class="col-6">
                            <x-adminlte-button theme="secondary" class="btn-lg btn-block" label="閉じる"
                                               data-dismiss="modal"/>
                        </div>
                    </div>
                </div>
            </x-slot>
        </x-adminlte-modal>
    </form>

    <x-adminlte-button label="編集" data-toggle="modal" class="bg-primary mb-3" wire:click="openModal()"/>

    <script>
        //モーダル展開用
        window.addEventListener('show_edit_modal_{{ $board->id }}', event => {
            $('#updateModal_{{ $board->id }}').modal('show');
        });
    </script>
</div>
```
``board.name``のようにname用のプロパティを作らなくてもネストして書けます。便利ですな。


編集用のコンポーネントを埋め込みます
``<livewire:board.edit :board="$board" :wire:key="$board->id"/>``
app/Http/Livewire/Board/Edit.phpのboardプロパティに$boardを渡して、
livewireがどの投稿がどのコンポーネントかを分かるようにするためにコンポーネントのキーをセット。

```html:resources/views/livewire/board/index.blade.php
<div>
    <div class="col-12 pt-5">
        <div class="col-5 mx-auto">
            <livewire:board.create/>
        </div>
        @foreach($boards as $board)
            <div class="card col-5 mx-auto">
                <div class="card-body">
                    <!-- 追記 -->
                    <livewire:board.edit :board="$board" :wire:key="$board->id"/>
                    <!-- 追記ここまで -->
                    <h5 class="card-title">{{ $board->name }}</h5>
                    <p class="card-text">{{ $board->contents }}</p>
                    @foreach($board->tags as $tag)
                        <span class="badge badge-info">{{ $tag->name }}</span>
                    @endforeach
                </div>
            </div>
        @endforeach
    </div>
</div>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/bc16a4b9-440e-cb47-7805-f0d33a09fa21.png)
適当な位置に編集ボタンを置きましたがまぁ良いでしょう。編集ボタンを押して動かしてみます。
<img width="48%"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/123a9bc1-61ec-f1f2-7d18-428dfbc52d4a.png"> <img width="48%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/83791b18-06b4-d5a0-ceb2-73d72292220f.png">
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282328/1adb5bcd-7803-08ca-a24c-ab0955b21b97.png)
はい、更新されました。

<a id="#おわりに"></a>
# おわりに
かなり雑に走り書きをしましたが、便利さが2ミリ程でも伝われば幸いです。
Laravel-AdminLTE、livewire両方使うとコード量も減らせて入力漏れのバグも減らせてルーティングも減らせたりして
うまみがたくさんです。

livewireの日本語記事や参考文献が少なくて手を付けにくいかもしれませんが、割と直ぐに慣れるので是非使ってみてください。







<a id="#reference"></a>
# 参考文献
- [livewireドキュメント(マジックアクション)](https://laravel-livewire.com/docs/2.x/actions#magic-actions)
- [【JavaScript】event.preventDefault()が何をするのか](https://qiita.com/yokoto/items/27c56ebc4b818167ef9e)
- [Laravel-AdminLTE コンポーネントwiki](https://github.com/jeroennoten/Laravel-AdminLTE/wiki/Components-Categories)