Kritim Yantra
Apr 04, 2025
In this comprehensive tutorial, we'll walk through building a complete todo list application using Laravel 12 and Bootstrap 5. This guide is perfect for beginners and covers everything from setup to deployment-ready code.
A todo list is a classic beginner project that helps you understand fundamental web development concepts. We'll use:
Before we begin, ensure you have:
Let's start by creating a new Laravel project:
composer create-project laravel/laravel laravel-todo-list
cd laravel-todo-list
Install Bootstrap and its dependencies:
npm install bootstrap @popperjs/core
Open your .env
file and configure your database connection:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_todo
DB_USERNAME=root
DB_PASSWORD=
Let's create our Todo model and its migration file:
php artisan make:model Todo -m
Now, edit the migration file in database/migrations/xxxx_create_todos_table.php
:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
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();
});
}
public function down()
{
Schema::dropIfExists('todos');
}
};
Run the migration:
php artisan migrate
Now, let's set up mass assignment protection in our Todo model (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',
'completed'
];
}
Create a controller to handle our todo operations:
php artisan make:controller TodoController --resource
Now, let's implement all the CRUD methods in app/Http/Controllers/TodoController.php
:
<?php
namespace App\Http\Controllers;
use App\Models\Todo;
use Illuminate\Http\Request;
class TodoController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$todos = Todo::orderBy('created_at', 'desc')->get();
return view('todos.index', compact('todos'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('todos.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string'
]);
Todo::create($validated);
return redirect()->route('todos.index')->with('success', 'Todo created successfully!');
}
/**
* Display the specified resource.
*/
public function show(Todo $todo)
{
return view('todos.show', compact('todo'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Todo $todo)
{
return view('todos.edit', compact('todo'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Todo $todo)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string'
]);
$todo->update($validated);
return redirect()->route('todos.index')->with('success', 'Todo updated successfully!');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Todo $todo)
{
$todo->delete();
return redirect()->route('todos.index')->with('success', 'Todo deleted successfully!');
}
/**
* Update the status of the specified resource.
*/
public function updateStatus(Request $request, Todo $todo)
{
$todo->completed = $request->completed;
$todo->save();
return response()->json(['success' => 'Todo status updated successfully.']);
}
}
Let's define our routes in routes/web.php
:
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TodoController;
Route::get('/', function () {
return redirect()->route('todos.index');
});
Route::resource('todos', TodoController::class);
Route::put('/todos/{todo}/status', [TodoController::class, 'updateStatus'])
->name('todos.updateStatus');
First, let's set up our main layout. Create resources/views/layouts/app.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 Todo List</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">@yield('title')</h3>
</div>
<div class="card-body">
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@yield('content')
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
@stack('scripts')
</body>
</html>
Now, let's create our index view at resources/views/todos/index.blade.php
:
@extends('layouts.app')
@section('title', 'Todo List')
@section('content')
<div class="d-flex justify-content-between mb-4">
<h4>My Todos</h4>
<a href="{{ route('todos.create') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Add New Todo
</a>
</div>
@if($todos->isEmpty())
<div class="alert alert-info">
No todos found. Create your first todo!
</div>
@else
<div class="list-group">
@foreach($todos as $todo)
<div class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between align-items-center">
<div class="form-check">
<input type="checkbox"
class="form-check-input todo-status"
data-todo-id="{{ $todo->id }}"
{{ $todo->completed ? 'checked' : '' }}>
<label class="form-check-label {{ $todo->completed ? 'text-decoration-line-through' : '' }}">
{{ $todo->title }}
</label>
</div>
<div class="btn-group">
<a href="{{ route('todos.show', $todo->id) }}" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i>
</a>
<a href="{{ route('todos.edit', $todo->id) }}" class="btn btn-sm btn-warning">
<i class="fas fa-edit"></i>
</a>
<form action="{{ route('todos.destroy', $todo->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</div>
@if($todo->description)
<p class="mb-0 mt-2 text-muted small">{{ $todo->description }}</p>
@endif
<p class="mb-0 text-muted small">
<i class="far fa-clock"></i> Created: {{ $todo->created_at->diffForHumans() }}
</p>
</div>
@endforeach
</div>
@endif
@endsection
@push('scripts')
<script>
$(document).ready(function() {
$('.todo-status').change(function() {
const todoId = $(this).data('todo-id');
const completed = $(this).is(':checked') ? 1 : 0;
$.ajax({
url: "{{ route('todos.updateStatus', '') }}/" + todoId,
type: 'PUT',
data: {
completed: completed,
_token: "{{ csrf_token() }}"
},
success: function(response) {
location.reload();
},
error: function(xhr) {
alert('Error updating status');
}
});
});
});
</script>
@endpush
Create the create form at resources/views/todos/create.blade.php
:
@extends('layouts.app')
@section('title', 'Create New Todo')
@section('content')
<form action="{{ route('todos.store') }}" method="POST">
@csrf
<div class="mb-3">
<label for="title" class="form-label">Title *</label>
<input type="text" class="form-control @error('title') is-invalid @enderror"
id="title" name="title" value="{{ old('title') }}" required>
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control @error('description') is-invalid @enderror"
id="description" name="description" rows="3">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ route('todos.index') }}" class="btn btn-secondary me-md-2">
<i class="fas fa-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Save Todo
</button>
</div>
</form>
@endsection
Create the edit form at resources/views/todos/edit.blade.php
:
@extends('layouts.app')
@section('title', 'Edit Todo')
@section('content')
<form action="{{ route('todos.update', $todo->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label for="title" class="form-label">Title *</label>
<input type="text" class="form-control @error('title') is-invalid @enderror"
id="title" name="title" value="{{ old('title', $todo->title) }}" required>
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control @error('description') is-invalid @enderror"
id="description" name="description" rows="3">{{ old('description', $todo->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ route('todos.index') }}" class="btn btn-secondary me-md-2">
<i class="fas fa-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Update Todo
</button>
</div>
</form>
@endsection
Create the show view at resources/views/todos/show.blade.php
:
@extends('layouts.app')
@section('title', 'Todo Details')
@section('content')
<div class="mb-3">
<h5>{{ $todo->title }}</h5>
<div class="form-check">
<input type="checkbox"
class="form-check-input todo-status"
data-todo-id="{{ $todo->id }}"
{{ $todo->completed ? 'checked' : '' }}>
<label class="form-check-label {{ $todo->completed ? 'text-decoration-line-through' : '' }}">
{{ $todo->completed ? 'Completed' : 'Pending' }}
</label>
</div>
</div>
@if($todo->description)
<div class="mb-3">
<label class="form-label">Description:</label>
<p class="border p-2 rounded">{{ $todo->description }}</p>
</div>
@endif
<div class="mb-3">
<label class="form-label">Created:</label>
<p>{{ $todo->created_at->format('M d, Y h:i A') }}</p>
</div>
<div class="mb-3">
<label class="form-label">Last Updated:</label>
<p>{{ $todo->updated_at->format('M d, Y h:i A') }}</p>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{{ route('todos.index') }}" class="btn btn-secondary me-md-2">
<i class="fas fa-arrow-left"></i> Back to List
</a>
<a href="{{ route('todos.edit', $todo->id) }}" class="btn btn-warning me-md-2">
<i class="fas fa-edit"></i> Edit
</a>
<form action="{{ route('todos.destroy', $todo->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">
<i class="fas fa-trash"></i> Delete
</button>
</form>
</div>
@endsection
@push('scripts')
<script>
$(document).ready(function() {
$('.todo-status').change(function() {
const todoId = $(this).data('todo-id');
const completed = $(this).is(':checked') ? 1 : 0;
$.ajax({
url: "{{ route('todos.updateStatus', '') }}/" + todoId,
type: 'PUT',
data: {
completed: completed,
_token: "{{ csrf_token() }}"
},
success: function(response) {
location.reload();
},
error: function(xhr) {
alert('Error updating status');
}
});
});
});
</script>
@endpush
We've already added basic validation in our controller's store
and update
methods. Laravel will automatically redirect back with errors if validation fails, and our views are set up to display these errors using the @error
directive.
Our TodoController already implements all CRUD operations:
create()
and store()
methodsindex()
and show()
methodsedit()
and update()
methodsdestroy()
methodWe've added a special updateStatus
method to handle AJAX requests for changing the completion status of todos. This is triggered by the checkbox in our views and uses jQuery to send an AJAX request.
Let's add some custom CSS to make our app look better. Create resources/css/app.css
:
/* Custom styles for our todo app */
body {
background-color: #f8f9fa;
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.list-group-item {
transition: all 0.3s ease;
}
.list-group-item:hover {
background-color: #f8f9fa;
}
.form-check-label.text-decoration-line-through {
color: #6c757d;
}
.btn-group .btn {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
Update your vite.config.js
to include Bootstrap:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js'
],
refresh: true,
}),
],
});
Create resources/js/app.js
:
// Import Bootstrap JS
import 'bootstrap';
Now compile your assets:
npm run build
Congratulations! You've built a complete todo list application with Laravel 12 and Bootstrap 5. This application includes:
To run your application:
php artisan serve
Then visit http://localhost:8000
in your browser.
This tutorial provides a solid foundation for building Laravel applications with Bootstrap. You can now expand this basic todo list into a more sophisticated productivity application!
No comments yet. Be the first to comment!
Please log in to post a comment:
Continue with Google