Kritim Yantra
Jun 08, 2025
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.
Before we begin, ensure you have:
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
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
Generate the Todo model:
php artisan make:model Todo
Add fillable fields to app/Models/Todo.php
:
protected $fillable = [
'title',
'description',
'completed'
];
Create a new Livewire component:
php artisan make:livewire TodoList
This creates two files:
app/Http/Livewire/TodoList.php
resources/views/livewire/todo-list.blade.php
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');
}
}
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>
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>
Start the development server:
php artisan serve
Visit http://localhost:8000
in your browser. You should see your Todo application with:
Let's enhance our application with some additional features:
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
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>
To deploy your application:
.env
php artisan config:cache
Congratulations! You've built a fully functional Todo application with Laravel 12 and Livewire. This application demonstrates:
Happy coding!
Transform from beginner to Laravel expert with our personalized Coaching Class starting June 20, 2025. Limited enrollment ensures focused attention.
1-hour personalized coaching
Build portfolio applications
Industry-standard techniques
Interview prep & job guidance
Complete your application to secure your spot
Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google