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!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google