Laravel8とLivewireでToDoアプリを作ってみた

Table of Content

はじめに

Laravel8の認証機能はJetstreamが追加されました。Jetstreamをインストールする際にモダンUIとしてLivewireかInertiaを選択する必要があります。
本記事では Livewireを選択しToDoアプリを作成してCRUD処理を作ってみます。
なお、Laravelは「VSCode+Remote Containers」で構築していきたいと思います。

出来上がりはこんな感じで出来上がります。

Laravel ToDo CRUD

ToDoのCRUDを作成すのにここを参照しました。

Laravelプロジェクトの作成

「composer create-project」でLaravelプロジェクトを作成します。

composer create-project --prefer-dist laravel/laravel .

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

artisanでToDoモデルを作成します。「-m」でマイグレーションファイルも合わせて作成します。

php artisan make:model Todo -m

「database/migrations/create_todos_table.php」を次のように編集します。

public function up()
{
    Schema::create('todos', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description');
        $table->timestamps();
    });
}

「app/Models/Todo.php」を次にように編集します。

<?php

namespace App\Models;

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

class Todo extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'description'
    ];
}

JetstreamとLivewireのインストール

Jetstreamをインストールします。

composer require laravel/jetstream

livewireを選択してインストールします。

php artisan jetstream:install livewire

“npm install && npm run dev”でJavascriptを構築します。

npm install && npm run dev

データベースをマイグレーションします。

php artisan migrate

Livewireコンポーネントの作成

Livewareのコンポーネントを作成します。

php artisan make:livewire todos

コンポーネントを作成すると次のファイルが作成されます。

app/Http/Livewire/Todos.php
resources/views/livewire/Todos.blade.php

ルーティング作成

コンポーネントを表示するように「routes/web.php」を編集します。

use App\Http\Livewire\Todos;
Route::get('todos', Todos::class);

コンポーネントの更新

「php artisan make:livewire todos」で作成した「app/Http/Livewire/Todos.php」にCRUDを実装します。

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Todo;

class Todos extends Component
{
    public $todos, $title, $description, $todo_id;
    public $isOpen=false;

    public function render()
    {
        $this->todos = Todo::all();
        return view('livewire.todos');
    }

    public function create()
    {
        $this->resetInputFields();
        $this->openModal();
    }

    public function openModal()
    {
        $this->isOpen = true;
    }

    public function closeModal()
    {
        $this->isOpen = false;
    }

    public function resetInputFields()
    {
        $this->title = '';
        $this->description = '';
        $this->todo_id = null;
    }

    public function store()
    {
        $this->validate([
            'title' => 'required',
            'description' => 'required'
        ]);
        Todo::updateOrCreate(['id' => $this->todo_id], [
            'title' => $this->title,
            'description' => $this->description
        ]);
        session()->flash('message', 
            $this->todo_id ? 'Todo Updated Successfully.' : 'Todo Created Successfully.');
        $this->closeModal();
        $this->resetInputFields();
    }

    public function edit($id)
    {
        $todo = Todo::findOrFail($id);
        $this->todo_id = $todo->id;
        $this->title = $todo->title;
        $this->description = $todo->description;
        $this->openModal();
    }

    public function delete($id)
    {
        Todo::find($id)->delete();
        session()->flash('message', 'Todo Deleted Successfully.');
    }

ToDoを追加・更新するbladeファイル作成

「php artisan make:livewire todos」で作成した「resources/livewire/todos.blade.php」を次のように編集します。

<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        Manage todos (Laravel 8 Jetstream Livewire CRUD Example
    </h2>
</x-slot>
<div class="py-12">
    <div class="max-w-7x1 mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-x1 sm rounded-lg px-4 py-4">
            @if(session()->has('message'))
                <div class="bg-teal-100 border-lt-4 border teal-500 rounded-b text-teal-900 px-4 py-3 shadow-md my-3" role="alert">
                    <div class="flex">
                        <div>
                            <p class="text-sm">{{ session('message') }}</p>
                        </div>
                    </div>
                </div>
            @endif
            <button wire:click="create()" class="bg-blue-500 hover bg-blue-700 text-white font-bold py-2 px-4 rounded my-3">Create Todo</button>
            @if($isOpen)
            @include('livewire.create')
            @endif
            <table class="table-fixed w-full">
                <thead>
                    <tr class="bg-gray-1000">
                        <th class="px-4 py2 w-20">No.</th>
                        <th class="px-4 py-2">Title</th>
                        <th class="px-4 py-2">Desc</th>
                        <th class="px-4 py-4">Action</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach($todos as $todo)
                        <tr>
                            <td class="border px-4 py-2">{{ $todo->id }}</td>
                            <td class="border px-4 py-2">{{ $todo->title }}</td>
                            <td class="border px-4 py-2">{{ $todo->description }}</td>
                            <td class="border px-4 py2">
                                <button wire:click="edit({{ $todo->id }})" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Edit</button>
                                <button wire:click="delete({{ $todo->id }})" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Delete</button>
                            </td>
                        </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>
</div>

ToDoを作成するダイアログ「resources/views/create.blade.php」を次のように作成します。

<div class="fixed z-10 inset-0 overflow-y-auto ease-out duration-400">
    <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
        <div class="fixed inset-0 tansition-opacity">
            <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
        </div>
        <span class="hidden sm:inline-block sm:aligh-middle sm:h-screen"></span>
        <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" role="dialog" aria-modal="true" aria-labelledby="modal-headline">
            <form>
                <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
                    <div class="">
                        <div class="mb-4">
                            <label for="title" class="block text-gray-700 text-sm font-bold mb-2">Title</label>
                            <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="title" placeholder="Enter Title" wire:model="title">
                            @error('title') <span class="text-red-500">{{ $message }}</span>@enderror       
                        </div>
                        <div class="mb-4">
                            <label for="description" class="block text-gray-700 text-sm font-bold mb-2">Description</label>
                            <textarea  id="description" cols="30" rows="10" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="Enter Discription" wire:model="description"></textarea>
                            @error('description') <span class="text-red-500">{{ $message }}</span>@enderror       
                        </div>
                    </div>
                </div>
                <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
                    <span class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
                        <button wire:click.prevent="store()" type="button" class="inline-flex justify-center w-full rounded-md border border-transparent px-4 py-2 bg-green-600 text-base leading-6 font-medium text-white shadow-sm hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green transition ease-in-out duration-150 sm:text-sm sm:leading-5">
                            Save
                        </button>
                    </span>
                    <span class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
                        <button wire:click="closeModal()" type="button" class="inline-flex justify-center w-full rounded-md border border-transparent px-4 py-2 bg-green-600 text-base leading-6 font-medium text-white shadow-sm hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green transition ease-in-out duration-150 sm:text-sm sm:leading-5">
                            Cancel
                        </button>
                    </span>
                </div>
            </form>
        </div>
    </div>
</div>

動作確認

http://localhost/register」にアクセスし、ユーザ登録をしてログインをしておきます。

http://localhost/todos」にアクセスしてToDoの動作確認をします。

Laravel ToDo CRUD

最後に

駆け足ですがLaravel8&LivewireでToDoを作成してみました。
Javascriptを一切書くことなくモダンアプリケーションが作成できるのは良いですね。

しかし…「tailwind」はCSSを知ってれば有効ですが、デザインできないエンジニアには理解が難しい…