Simple To-Do List With Django, Alpine.Js, Tailwidcss, And Axios
While back I wrote a tutorial on how to make a todo app with Django, Alpine.js, and Axios, however, there was some room for improvement as I was just beginning with Alpine, so I decided to write a new version.
In this tutorial, I will show you how to create a simple and beautiful ToDo app with
- Django
- TailwindCSS
- Alpine.js
- Axios
We will divide this into 2 parts
- Building The Backend with Django
- Building the Front-end and connecting it with the Backend using TailwindCSS, Alpine.js, and Axios
Check the finished project on GitHub
Back-end
Let's start by creating a new project and its virtualenvironment, and installing Django
pip install Django
Then create a Django project, an app called tasks
django-admin startproject todo_list .
django-admin startapp tasks
Add tasks applications to INSTALLED_APPS
in the settings.py
file.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'tasks' # Add newly created app
]
Now to go the tasks
app, and models.py
file, and create a model for our task
from django.db import models
class Task(models.Model):
title = models.CharField(max_length=255)
completed = models.BooleanField(default=False)
To keep things simple we just use two fields title
and completed
Next let's create views for reading, creating, deleting, and updating the status of the task.
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from tasks.models import Task
def task_list(request):
tasks = [{"id": task.id,
"title": task.title,
"completed": task.completed} for task in Task.objects.all()]
return JsonResponse(status=200, data=tasks, safe=False)
def create_task(request):
title = request.POST.get('title')
if not title:
return JsonResponse(status=400, data={'error': 'title is required'})
task = Task.objects.create(title=title)
return JsonResponse(status=201, data={'title': task.title,
'completed': task.completed,
'id': task.id}, safe=False)
def delete_task(request, task_id):
task = get_object_or_404(Task, pk=task_id)
task.delete()
return JsonResponse(status=204, data={'message': 'task deleted'})
def update_task_status(request, task_id):
task = get_object_or_404(Task, pk=task_id)
status = request.POST.get('status')
if not status:
return JsonResponse(status=400, data={'error': 'status is required'})
task.completed = int(status)
task.save()
return JsonResponse(status=204, data={'message': 'task status updated'})
Now let's create urls.py
file inside our tasks
app, and create url routes for our views.
from django.urls import path
from . import views
urlpatterns = [
path('tasks/', views.task_list, name='task_list'),
path('tasks/create/', views.create_task, name='create_task'),
path('tasks/<int:task_id>/delete/', views.delete_task, name='delete_task'),
path('task/<int:task_id>/update/', views.update_task_status, name='update_task')
]
Now go to the main urls.py
file of our application and include the tasks app urls.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path(″, include('tasks.urls')),
]
Now run the migrations, and start our app
python manage.py makemigratinos
python manage.py migrate
python manage.py runserver
Now using HTTP client ( i am using httpie, but you can use any other tool like postman or insomia)
test our endpoints. Here are the httpie commands I am using.
# Making Get request to retrive task list
http http://127.0.0.1:8000/tasks
# To test the POST/DELETE request you will need to add @csrf_exempt
# decorator to the views, to bypass the csrf token check,
# so you don't need to pass csrf token in http client.
# Once we will be building front-end part you can delete the decorator
# Making POST request to create a task
http -f POST http://127.0.0.1:8000/tasks/create/ title="Clean the dishes"
# Making task Delete request
http -f DELETE http://127.0.0.1:8000/tasks/1/delete/
# Making POST request to update a task status
http -f POST http://127.0.0.1:8000/tasks/2/update/ status=1
Create a few tasks to populate our database.
Before moving to the front-end let's create last view for the home page. Go to views.py
file and create a simple view.
def index(request):
return render(request, 'index.html')
Finally add it to the urls.py
of the tasks
app.
urlpatterns = [
path(″, views.index, name='index'),
path('tasks/', views.task_list, name='task_list'),
path('tasks/create/', views.create_task, name='create_task'),
path('tasks/<int:task_id>/delete/', views.delete_task, name='delete_task'),
path('tasks/<int:task_id>/update/', views.update_task_status, name='update_task')
]
Front-end
In the tasks
application create a directory called templates
and inside a file called index.html
Include CDNs for TailwindCSS, Alpine.js and Axios in the <head>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Simple ToDo List</title>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/[email protected]/dist/alpine.min.js" defer></script>
</head>
<body>
</body>
</html>
We need to create a simple form for adding tasks, and task list component.
Let's start with the form. Add the input and button for adding tasks inside the body
<div class="max-w-4xl mx-auto mt-6">
<div class="text-5xl font-extrabold leading-none tracking-tight text-center">
<h1 class="bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 via-pink-600 to-purple-900">Simple To-Do List</h1>
</div>
<!-- Task Input -->
<div id="task-input" class="mt-4 flex justify-center">
<div class="m-4 flex">
<input class="rounded-l-lg p-4 border-t mr-0 border-b border-l text-gray-800 border-gray-200" placeholder="Task Title"/>
<button class="px-8 rounded-r-lg bg-purple-800 text-gray-100 font-bold p-4 uppercase">Add Task</button>
</div>
</div>
</div>
Now let's create a task component, we need one for a completed task and one for an uncompleted one.
<!-- Task Input -->
<div id="task-input" class="mt-4 flex justify-center">
<div class="m-4 flex">
<input class="rounded-l-lg p-4 border-t mr-0 border-b border-l text-gray-800 border-gray-200" placeholder="Task Title"/>
<button class="px-8 rounded-r-lg bg-purple-800 text-gray-100 font-bold p-4 uppercase">Add Task</button>
</div>
</div>
<!-- Task List -->
<div id="task-list" class="max-w-md mx-auto grid grid-cols-1 gap-2 mt-6">
<!-- Task in progress -->
<div class="p-4 bg-white hover:bg-gray-100 cursor-pointer flex justify-between items-center border rounded-md">
<p>Uncompleted Task</p>
<button type="button">
<svg class="h-6 w-6 text-gray-500 hover:text-green-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
<!-- Completed Task -->
<div class="p-4 bg-white hover:bg-gray-100 cursor-pointer flex justify-between items-center border rounded-md">
<p class="line-through">Completed Task</p>
<svg class="h-6 w-6 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
</div>
Ok now we have our HTML, ready let's connect it all together using Alpine.js and Axios
Let's start by populating the list with the initial tasks, we created earlier.
Open a <script>
tag before closing the body tag and create a function for Alpine components.
We will create a tasks
array to hold our tasks, and couple functions equavilent to the endpoints we created earlier in views.py
<script>
function todos() {
return {
tasks: [],
loadTasks() {},
addTask() {},
deleteTask(taskId) {},
updateTask() {},
}
}
</script>
</body>
Let's start with loadTasks()
loadTasks() {
let self = this;
axios.get('http://127.0.0.1:8000/tasks/')
.then(function (response) {
// handle success
self.tasks = response.data;
})
.catch(function (error) {
// handle error
console.log(error);
});
}
Now let's go back to HTML, and add x-data
properties and x-init
to the div enclosing the task input and task list.
<div x-data="todos()" x-init="loadTasks()" class="max-w-4xl mx-auto mt-6">
x-init is used to run a function once a component is initialized, so we will get the list of the tasks we have. Now go to the task list div and inside create a <template>
tag enclosing the task component, and add x-for
property to iterate through all tasks on the list
<!-- Task List -->
<div id="task-list" class="max-w-md mx-auto grid grid-cols-1 gap-2 mt-6">
<template x-for="task in tasks">
<div class="p-4 bg-white hover:bg-gray-100 cursor-pointer flex justify-between items-center border rounded-md">