“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!

LIVE MENTORSHIP ONLY 5 SPOTS

Laravel Mastery
Coaching Class Program

KritiMyantra

Transform from beginner to Laravel expert with our personalized Coaching Class starting June 20, 2025. Limited enrollment ensures focused attention.

Daily Sessions

1-hour personalized coaching

Real Projects

Build portfolio applications

Best Practices

Industry-standard techniques

Career Support

Interview prep & job guidance

Total Investment
$200
Duration
30 hours
1h/day

Enrollment Closes In

Days
Hours
Minutes
Seconds
Spots Available 5 of 10 remaining
Next cohort starts:
June 20, 2025

Join the Program

Complete your application to secure your spot

Application Submitted!

Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.

What happens next?

  • Confirmation email with program details
  • WhatsApp message from our team
  • Onboarding call to discuss your goals

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts