LoginSignup
20
15

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-01-13

はじめに

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

参考記事:Laravel-AdminLTEの付属コンポーネントで楽にフォームを作ろうぜ

目次

  1. 環境構築
  2. モデルとマイグレーション作成
  3. シーダー作成
  4. 投稿機能作成
  5. 書き込んでみよう
  6. 編集機能作成
  7. おわりに
  8. 参考文献

環境構築

Laravel8をインストール。

composer create-project --prefer-dist laravel/laravel sample "8.*"

livewireをインストール、ついでにLaravel-AdminLTEも入れます。

 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

config/adminlte.php内のlivewireを適応させます。
image.png
画面上部に@livewireStyleがテキストとして表示されてしまう場合は
resources/views/vendor/adminlte/master.blade.php内の

@livewireStyles
↓
@livewireStyles()

@livewireScripts
↓
@livewireScripts()
に書き換えます。

モデルとマイグレーション作成

model作ります、ついでにマイグレーションファイルも作成。

php artisan make:model Board -m
php artisan make:model Tag -m
php artisan make:model BoardTag -m 

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

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');
    }
}

シーダー作成

php artisan make:seeder TagsTableSeeder

タグ作るついでに投稿を1つ作ります

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]);
    }
}
database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

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

投稿機能作成

掲示板を表示する親コンポーネントと、投稿用の子コンポーネントを作ります

php artisan make:livewire Board/Index
php artisan make:livewire Board/Create

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

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も書いてきます

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>

ルーティングも書きます

routes/web.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/',App\Http\Livewire\Board\Index::class);

マイグレーション&シーダーコマンドを叩いて画面を見てみましょう

php artisan migrate:fresh --seed

はい1件だけ表示されました。
image.png

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

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コンポーネントで書いてきます。

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を引っ付けています。
参考記事:【JavaScript】event.preventDefault()が何をするのか

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

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

参考記事:Livewire 2.x プロパティ

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

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

これを

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

こう。

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

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

書き込んでみよう

モーダルを開いて何も選択せずに登録ボタンを押してみましょう

あぁ~...エラー表示処理書かなくてもエラー出してくれるの良いわぁ...便利やわぁ

適当に書き込んでみましょう。

書き込んだら「閉じる」をクリック。
image.png
書き込まれてますね、良きかな。

編集機能作成

編集用のコンポーネントを作ります

php artisan make:livewire Board/Edit
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
この状態から食べ物タグをクリックすると
image.png
食べ物のIDだけstringになっています。タグを追加するだけなら問題無いのですが、タグを解除しようとすると
image.png
偽物タグをクリックしても消えてくれません。再度偽物タグをクリックすると?
image.png
はいstring型のIDが追加されました。もうグチャグチャです。
めちゃくちゃ気持ち悪い挙動になるので気になった人は配列の中身をstring型にせずに弄ってみてください。

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がどの投稿がどのコンポーネントかを分かるようにするためにコンポーネントのキーをセット。

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
適当な位置に編集ボタンを置きましたがまぁ良いでしょう。編集ボタンを押して動かしてみます。

image.png
はい、更新されました。

おわりに

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

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

参考文献

20
15
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
15