“Laravel 12 + Livewire Todo App: A Beginner-Friendly CRUD Tutorial

Author

Kritim Yantra

Jun 08, 2025

“Laravel 12 + Livewire Todo App: A Beginner-Friendly CRUD Tutorial

In this comprehensive tutorial, we'll walk through building a modern, interactive Todo application using Laravel 12 and Livewire. This combination allows us to create dynamic, reactive interfaces while maintaining the simplicity of server-side PHP development.

Why Laravel + Livewire?

  • Laravel: The elegant PHP framework that makes web development enjoyable
  • Livewire: Brings reactivity to Laravel without writing JavaScript
  • Perfect Pair: Together they offer a full-stack solution with minimal complexity

Prerequisites

Before we begin, ensure you have:

  • PHP 8.2+ installed
  • Composer (for dependency management)
  • Node.js (for optional frontend assets)
  • Basic Laravel knowledge

Step 1: Setting Up the Project

First, let's create a new Laravel 12 project:

composer create-project laravel/laravel laravel-livewire-todo
cd laravel-livewire-todo

Next, install Livewire:

composer require livewire/livewire

Step 2: Database Setup

Configure your .env file with database credentials:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=todo_app
DB_USERNAME=root
DB_PASSWORD=

Create a migration for our Todo model:

php artisan make:migration create_todos_table

Edit the migration file:

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

Run the migration:

php artisan migrate

Step 3: Creating the Todo Model

Generate the Todo model:

php artisan make:model Todo

Add fillable fields to app/Models/Todo.php:

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

Step 4: Building the Livewire Component

Create a new Livewire component:

php artisan make:livewire TodoList

This creates two files:

  1. app/Http/Livewire/TodoList.php
  2. resources/views/livewire/todo-list.blade.php

Step 5: Implementing the TodoList Component

Edit app/Http/Livewire/TodoList.php:

<?php

namespace App\Http\Livewire;

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

class TodoList extends Component
{
    public $todos;
    public $newTodoTitle = '';
    public $newTodoDescription = '';
    
    protected $rules = [
        'newTodoTitle' => 'required|min:3',
        'newTodoDescription' => 'nullable|string'
    ];

    public function mount()
    {
        $this->todos = Todo::latest()->get();
    }

    public function addTodo()
    {
        $this->validate();
        
        Todo::create([
            'title' => $this->newTodoTitle,
            'description' => $this->newTodoDescription,
            'completed' => false
        ]);
        
        $this->newTodoTitle = '';
        $this->newTodoDescription = '';
        $this->todos = Todo::latest()->get();
    }

    public function toggleComplete($id)
    {
        $todo = Todo::find($id);
        $todo->completed = !$todo->completed;
        $todo->save();
        $this->todos = Todo::latest()->get();
    }

    public function deleteTodo($id)
    {
        Todo::find($id)->delete();
        $this->todos = Todo::latest()->get();
    }

    public function render()
    {
        return view('livewire.todo-list');
    }
}

Step 6: Creating the Todo List View

Edit resources/views/livewire/todo-list.blade.php:

<div class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl p-6">
    <h1 class="text-2xl font-bold text-gray-800 mb-6">Todo Application</h1>
    
    <!-- Add Todo Form -->
    <div class="mb-6">
        <div class="mb-4">
            <input 
                wire:model="newTodoTitle"
                type="text" 
                placeholder="What needs to be done?"
                class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            >
            @error('newTodoTitle') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
        </div>
        <div class="mb-4">
            <textarea 
                wire:model="newTodoDescription"
                placeholder="Description (optional)"
                class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                rows="2"
            ></textarea>
        </div>
        <button 
            wire:click="addTodo"
            class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
        >
            Add Todo
        </button>
    </div>
    
    <!-- Todo List -->
    <div class="space-y-4">
        @forelse($todos as $todo)
            <div class="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50">
                <div class="flex items-center space-x-4">
                    <input 
                        type="checkbox" 
                        wire:change="toggleComplete({{ $todo->id }})"
                        {{ $todo->completed ? 'checked' : '' }}
                        class="h-5 w-5 text-blue-500 rounded focus:ring-blue-400"
                    >
                    <div class="{{ $todo->completed ? 'line-through text-gray-400' : 'text-gray-800' }}">
                        <h3 class="font-medium">{{ $todo->title }}</h3>
                        @if($todo->description)
                            <p class="text-sm text-gray-600">{{ $todo->description }}</p>
                        @endif
                    </div>
                </div>
                <button 
                    wire:click="deleteTodo({{ $todo->id }})"
                    class="text-red-500 hover:text-red-700"
                >
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                    </svg>
                </button>
            </div>
        @empty
            <p class="text-gray-500 text-center py-8">No todos yet. Add one above!</p>
        @endforelse
    </div>
</div>

Step 7: Creating the Main View

Create resources/views/welcome.blade.php:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Laravel Livewire Todo App</title>
    @livewireStyles
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center p-4">
    <div class="w-full">
        @livewire('todo-list')
    </div>
    @livewireScripts
</body>
</html>

Step 8: Testing the Application

Start the development server:

php artisan serve

Visit http://localhost:8000 in your browser. You should see your Todo application with:

  • A form to add new todos
  • A list displaying all todos
  • Checkboxes to mark todos as complete
  • Delete buttons to remove todos

Step 9: Adding Features (Optional)

Let's enhance our application with some additional features:

1. Edit Todos

Add these methods to your TodoList component:

public $editingTodoId = null;
public $editingTodoTitle = '';
public $editingTodoDescription = '';

public function editTodo($id)
{
    $todo = Todo::find($id);
    $this->editingTodoId = $id;
    $this->editingTodoTitle = $todo->title;
    $this->editingTodoDescription = $todo->description;
}

public function updateTodo()
{
    $this->validate([
        'editingTodoTitle' => 'required|min:3',
        'editingTodoDescription' => 'nullable|string'
    ]);
    
    $todo = Todo::find($this->editingTodoId);
    $todo->update([
        'title' => $this->editingTodoTitle,
        'description' => $this->editingTodoDescription
    ]);
    
    $this->cancelEdit();
    $this->todos = Todo::latest()->get();
}

public function cancelEdit()
{
    $this->editingTodoId = null;
    $this->editingTodoTitle = '';
    $this->editingTodoDescription = '';
}

Update your view to include editing functionality:

<!-- Inside the todo item loop -->
@if($editingTodoId === $todo->id)
    <div class="flex-1">
        <input 
            wire:model="editingTodoTitle"
            type="text"
            class="w-full px-2 py-1 border rounded mb-2"
        >
        <textarea 
            wire:model="editingTodoDescription"
            class="w-full px-2 py-1 border rounded text-sm"
            rows="2"
        ></textarea>
    </div>
    <div class="flex space-x-2">
        <button 
            wire:click="updateTodo"
            class="text-green-500 hover:text-green-700"
        >
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
            </svg>
        </button>
        <button 
            wire:click="cancelEdit"
            class="text-gray-500 hover:text-gray-700"
        >
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
            </svg>
        </button>
    </div>
@else
    <!-- Existing todo display code -->
    <button 
        wire:click="editTodo({{ $todo->id }})"
        class="text-blue-500 hover:text-blue-700 mr-2"
    >
        <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
        </svg>
    </button>
@endif

2. Filtering Todos

Add these to your component:

public $filter = 'all'; // 'all', 'completed', 'active'

public function updatedFilter()
{
    $this->applyFilter();
}

private function applyFilter()
{
    $query = Todo::latest();
    
    if ($this->filter === 'completed') {
        $query->where('completed', true);
    } elseif ($this->filter === 'active') {
        $query->where('completed', false);
    }
    
    $this->todos = $query->get();
}

// Update all methods that modify todos to call $this->applyFilter() instead of $this->todos = Todo::latest()->get();

Add filter buttons to your view:

<div class="flex space-x-4 mb-6">
    <button 
        wire:click="$set('filter', 'all')"
        class="{{ $filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700' }} px-4 py-2 rounded"
    >
        All
    </button>
    <button 
        wire:click="$set('filter', 'active')"
        class="{{ $filter === 'active' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700' }} px-4 py-2 rounded"
    >
        Active
    </button>
    <button 
        wire:click="$set('filter', 'completed')"
        class="{{ $filter === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700' }} px-4 py-2 rounded"
    >
        Completed
    </button>
</div>

Step 10: Deployment

To deploy your application:

  1. Set up production database credentials in .env
  2. Run php artisan config:cache
  3. Deploy to your preferred hosting (Laravel Forge, Heroku, shared hosting with proper PHP version)

Conclusion

Congratulations! You've built a fully functional Todo application with Laravel 12 and Livewire. This application demonstrates:

Happy coding!

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts