Introduction
To-do List is a pretty basic example app that is often done as one of the first projects, today we will make it a little more interesting by using few interesting technologies.
As the backend we will use Django and Django Rest Framework, and Alpine.js + Axios to glue it all together easily on the frontend.
What is Alpine.js
A pretty new lightwave framework inspired by Vue.js created last year by Caleb Porzio it gives us reactivity and declarativity of React and Vue, while keeping it light, and in the DOM. It’s described as TailwindCSS for JS. And I pretty much agree with that, using it together with Tailwind is a great boost to productivity when doing front-end because you can stay in one HTML file, and keep writing HTML, CSS, and js.
Axios
It’s an asynchronous HTTP client for JS.
Here is a link to finished project GitHub repo
Starting the app
Let’s start by creating a new virtual environment for our project, and installing required packages, then create a new Django project, and a lists app
pip install Django
pip install djangorestframework
django-admin startproject todo_list
cd todo_list
django-admin startapp lists
Then go to [settings.py](http://settings.py)
and add lists
and django rest framework app to INSTALLED_APPS
INSTALLED_APPS = [
...
‘rest_framework’,
‘lists’,
]
Creating App Models
Let’s create db models for our To-Do App. We are going to define 2 models, a List Model, and a Task model, Each user can create as many lists as he/she wants, and then add multiple tasks to each list.
from django.contrib.auth.models import User
from django.db import models
class List(models.Model):
title = models.CharField(max_length=75)
user = models.ForeignKey(User,
on_delete=models.CASCADE,
related_name=‘lists’)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Task(models.Model):
parent_list = models.ForeignKey(List,
on_delete=models.CASCADE,
related_name=‘tasks’)
title = models.CharField(max_length=75)
completed = models.BooleanField(default=False,
blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
We created a List
model with the title field and relation to the user who created the list.
The Task
model, has a relation to List
object, title, and boolean for the complete status of the task.
Also for both models also 2 DateTime fields for cerated and updated times.
Go to the [admin.py](http://admin.py)
file and register the models in the admin panel
from django.contrib import admin
from .models import List, Task
admin.site.register(List)
admin.site.register(Task)
Run the makemigrations
and migrate
commands.
python manage.py makemigrations
python manage.py migrate
Create API
Create Serializers
Inside the lists app create a new python package (new directory with an empty __init__.py
file), and call it api
. There create a file [serializers.py](http://serializers.py)
, views.py
, [urls.py](http://urls.py)
files inside. Go to [serialziers.py](http://serialziers.py)
and create serializers for the models.
from rest_framework import serializers
from ..models import List, Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = (‘title’, ‘completed’, ‘parent_list’)
class ListSerializer(serializers.ModelSerializer):
tasks = TaskSerializer(many=True, read_only=True)
class Meta:
model = List
fields = (‘title’, ‘tasks’)
Create Viewsets
Now we will create viewsets, that will automatically provide us with Create, Read, Update, and Delete endpoints (CRUD), so we can avoid repetition and writing them for each model. In [views.py](http://views.py)
file create viewsets.
from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from .serializers import TaskSerializer, ListSerializer
from ..models import Task, List
class ListViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = ListSerializer
authentication_classes = [SessionAuthentication]
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
return List.objects.filter(user=user)
def create(self, request, *args, **kwargs):
serializer = ListSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user)
return Response(serializer.validated_data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
authentication_classes = [SessionAuthentication]
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
return Task.objects.filter(parent_list__user=user)
Register Routes
Now we will create a router to automatically register the url routes for our models. Open urls.py
...
from django.urls import path, include
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(“lists”, views.ListViewSet)
router.register(“tasks”, views.TaskViewSet)
app_name = “lists”
urlpatterns = [
path(“”, include(router.urls)),
]
And finally, include them in the main [urls.py](http://urls.py)
of the project.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path(‘admin/’, admin.site.urls),
path(“api/“, include(“lists.api.urls”, namespace=“api”)),
]
Website Backend
Now go to lists/views.py
and create routes for home, and the list detali viewurls
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404
from .models import List
@login_required
def home(request):
return render(request, ‘index.html’, {
‘lists’: request.user.lists.all()
})
@login_required
def list_detail(request, list_id):
user_list = get_object_or_404(List, id=list_id)
return render(request, ‘detail.html’, {
‘list’: user_list
})
Now create a [urls.py](http://urls.py)
file inside the lists
app, and register the home route.
from django.urls import path
from . import views
app_name = ‘lists’
urlpatterns = [
path(″, views.home, name=‘home’),
path(‘list/<list_id>/’, views.list_detail,
name=‘detail’),
]
Now go to main [urls.py](http://urls.py)
file inside the todo_list project directory, and include the lists
app urls.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path(‘admin/’, admin.site.urls),
path(“api/“, include(“dashboard.api.urls”, namespace=“api”)),
path(″, include(‘lists.urls’, namespace=‘lists’)),
]
Front-end
We are done with the backend, now let’s create our front-end. In the lists
app create a directory called templates, and inside create 3 files base.html
, index.html
, and detail.html
.
base.html
<!DOCTYPE html>
<html lang=“en”>
<head>
<meta charset=“UTF-8”>
<meta name=“viewport” content=“width=device-width, initial-scale=1”/>
<title>{% block title %} {% endblock %}</title>
<link href=“https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css” rel=“stylesheet”>
<script src=“https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js” defer></script>
<script src=“https://unpkg.com/axios/dist/axios.min.js”></script>
</head>
<body class=“bg-gray-100”>
<div class=“bg-green-500 py-8 text-center”>
<h1 class=“text-gray-100 font-black text-4xl tracking-5”>To-Do List App</h1>
<p class=“text-green-800 font-medium font-mono text-sm”>Written with Django, Django Rest Framework, TailwindCSS, Alpine.js</p>
</div>
{% block content %}
{% endblock %}
</body>
</html>
This is the base.html file for our app, in the head, we created a django template block for title, and included TailwindCss
for styling our app, Alpine.js
and Axios
for making async HTTP requests.
In the body tag we created a content block
index.html
{% extends “base.html” %}
{% block title %}
To-Do List App
{% endblock %}
{% block content %}
<div class=“mx-4 md:mx-32 my-16 bg-white shadow p-8 px-8 rounded-sm”>
<h2 class=“text-green-500 font-black text-2xl uppercase text-center”>{{ request.user.username }}’s Lists</h2>
<form id=“list-form”>
{% csrf_token %}
</form>
<div class=“flex justify-end mt-4”>
<div class=“rounded-md border shadow p-2 flex-1 inline-flex”>
<input class=“mr-2 w-5/6” type=“text” placeholder=“new list”>
<button class=“w-1/6 border px-2 py-1 font-mono text-sm bg-green-400 hover:bg-green-500 active:bg-green-700 text-gray-100”>Add List</button>
</div>
</div>
<ul class=“mt-4”>
{% for list in lists %}
<li class=“bg-gray-100 border border-gray-300 rounded-md shadow-sm p-2 px-4 my-2 flex justify-between”>
<a href=“{% url ‘lists:detail’ list.id %}” class=“border border-gray-100 text-green-800 font-mono px-2 py-1 hover:text-green-500”>{{ list.title }}</a>
<button class=“border px-2 py-1 font-mono text-sm bg-red-400 hover:bg-red-500 text-gray-100”>Delete List</button>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
In the index.html we fill the content block, by creating, input with a button for adding new lists,
and display the user lists, with anchor tag linking to the details page. there is also delete button.
We will implement the create and delete funcionality with alpine.js and axios.
detail.html
{% extends “base.html” %}
{% block title %}
{{ list.title }} - ToDos
{% endblock %}
{% block content %}
<div class=“mx-4 md:mx-32 my-16 bg-white shadow p-8 px-8 rounded-sm”>
<h2 class=“text-green-500 font-black text-2xl uppercase text-center”>{{ list.title }}’s ToDos</h2>
<form id=“list-form”>
{% csrf_token %}
</form>
<div class=“flex justify-end mt-4”>
<div class=“rounded-md border shadow p-2 flex-1 inline-flex”>
<input class=“mr-2 w-5/6” type=“text” placeholder=“new task”>
<button class=“w-1/6 border px-2 py-1 font-mono text-sm bg-green-400 hover:bg-green-500 active:bg-green-700 text-gray-100”>Add Task</button>
</div>
</div>
<ul class=“mt-4”>
{% for task in list.tasks.all %}
<li class=“bg-gray-100 border border-gray-300 rounded-md shadow-sm p-2 px-4 my-2 flex justify-between font-mono”>
<div class=“flex justify-start”>
<button class=“mr-2”>
<svg class=“h-5 text-gray-500 hover:text-red-500” fill=“none” stroke-linecap=“round” stroke-linejoin=“round” stroke-width=“2” viewBox=“0 0 24 24” stroke=“currentColor”><path d=“M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z”></path></svg>
</button>
<p class=“text-gray-800”>{{ task.title }}</p>
</div>
{% if task.completed %}
<button class=“text-green-500 hover:text-gray-500 cursor-pointer”>Completed</button>
{% else %}
<button class=“text-gray-500 hover:text-green-500 cursor-pointer”>Not Completed</button>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
in detail.html
we create a similar view, with input for adding tasks, button to remove tasks, and button to switch between task status.
Now create a superuser, and run a server
python manage.py createsuperuser
python manage.py runserver
Go to the http://127.0.0.1:8000/admin/and create couple lists, and tasks with different statuses, then go to the http://127.0.0.1:8000/ and you should see view like that.
By clicking The Title of the list we will be moved to the list detail page
Now let’s put it all together using Alpine.js and Axios
Introducing Alpine.js
let’s go to index.html
and let’s switch the {% for list in lists %}
to be an alpine.js template. To do so edit the code like below.
<div x-data=“getLists()” class=“mx-4 md:mx-32 my-16 bg-white shadow p-8 px-8 rounded-sm”>
<h2 class=“text-green-500 font-black text-2xl uppercase text-center”>{{ request.user.username }}‘s Lists</h2>
<form id=“list-form”>
{% csrf_token %}
</form>
<div class=“flex justify-end mt-4”>
<div class=“rounded-md border shadow p-2 flex-1 inline-flex”>
<input class=“mr-2 w-5/6” type=“text” placeholder=“new list”>
<button class=“w-1/6 border px-2 py-1 font-mono text-sm bg-green-400 hover:bg-green-500 active:bg-green-700 text-gray-100”>Add List</button>
</div>
</div>
<ul class=“mt-4”>
<template x-for=“list in lists”>
<li class=“bg-gray-100 border border-gray-300 rounded-md shadow-sm p-2 px-4 my-2 flex justify-between”>
<a
x-text=“list.title”
x-bind:href=“‘/list/‘+list.id”
class=“border border-gray-100 text-green-800 font-mono px-2 py-1 hover:text-green-500”></a>
<button class=“border px-2 py-1 font-mono text-sm bg-red-400 hover:bg-red-500 text-gray-100”>Delete List</button>
</li>
</template>
</ul>
</div>
<script>
const csrftoken = document.querySelector(‘#list-form > input’).value;
const getLists = () => {
return {
lists: [
{% for l in lists %}
{ ‘title’: ‘{{ l.title }}’, ‘id’: ‘{{ l.id }}’ },
{% endfor %}
]
}
};
</script>
{% endblock %}
So what we did here? First, we added an x-data
attribute to the div holding our list, from the getTask method, which we defined at the bottom in the script
tag. As you can see we moved the Django template for loop there to create a json object.
Finally, we removed the Django for loop from ul
element, and we wrapped the li
element in the template tag. which has x-for
attribute that loops over the json array of list items.
We used x-bind:href
and x-text
to fill the tag with values from json.
Adding Lists
First, add a variable to json returned by getTasks(), and function to make a post request
const getLists = () => {
return {
newList: ″,
lists: [
{% for l in lists %}
{ ‘title’: ‘{{ l.title }}’, ‘id’: ‘{{ l.id }}’ },
{% endfor %}
]
}
};
const csrftoken = document.querySelector(‘#list-form > input’).value;
const addList = async title => {
try {
const res = await axios.post(‘/api/lists/’,
{ title },
{ headers: { ‘X-CSRFToken’: csrftoken }}
);
location.reload();
} catch (e) {
console.error(e);
}
};
Then find the input element and edit it
<div class=“rounded-md border shadow p-2 flex-1 inline-flex”>
<input x-model=“newList” class=“mr-2 w-5/6” type=“text” placeholder=“new list”>
<button @click=“addList(newList)” type=“button” class=“w-1/6 border px-2 py-1 font-mono text-sm bg-green-400 hover:bg-green-500 active:bg-green-700 text-gray-100”>Add List</button>
</div>
We gave the input x-model
attribute with the value set to newList
On the button we add @click
listener, which will trigger addList
function and pass the value of newList, if the request is successful it will reload the page to show the new item. Give it a try and try adding a few lists.
Removing Lists.
Removing lists will be even easier. First, add new axios function in our script tag.
const removeList = async listId => {
try {
const res = await axios.delete(‘/api/lists/’ + listId + ‘/’,
{ headers: { ‘X-CSRFToken’: csrftoken }}
);
location.reload();
} catch (e) {
console.error(e);
}
};
Then edit the delete button by adding @click
attribute and
<template x-for=“list in lists”>
<li class=“bg-gray-100 border border-gray-300 rounded-md shadow-sm p-2 px-4 my-2 flex justify-between”>
<a
x-text=“list.title”
x-bind:href=“‘/list/’+list.id”
class=“border border-gray-100 text-green-800 font-mono px-2 py-1 hover:text-green-500”></a>
<button @click=“removeList(list.id)”
class=“border px-2 py-1 font-mono text-sm bg-red-400 hover:bg-red-500 text-gray-100”>
Delete List</button>
</li>
</template>
Adding and removing tasks
Now we have to do the same for the tasks. open the detail.html
and edit it like so.
{% extends “base.html” %}
{% block title %}
{{ list.title }} - ToDos
{% endblock %}
{% block content %}
<div x-data=“getTasks()” class=“mx-4 md:mx-32 my-16 bg-white shadow p-8 px-8 rounded-sm”>
<h2 class=“text-green-500 font-black text-2xl uppercase text-center”>{{ list.title }}’s ToDos</h2>
<form id=“list-form”>
{% csrf_token %}
</form>
<div class=“flex justify-end mt-4”>
<div class=“rounded-md border shadow p-2 flex-1 inline-flex”>
<input x-model=“newTask” class=“mr-2 w-5/6” type=“text” placeholder=“new task”>
<button @click=“addTask(newTask, {{ list.id }} )” class=“w-1/6 border px-2 py-1 font-mono text-sm bg-green-400 hover:bg-green-500 active:bg-green-700 text-gray-100”>Add Task</button>
</div>
</div>
<ul class=“mt-4”>
<template x-for=“task in tasks”>
<li class=“bg-gray-100 border border-gray-300 rounded-md shadow-sm p-2 px-4 my-2 flex justify-between font-mono”>
<div class=“flex justify-start”>
<button @click=“removeTask(task.id)” class=“mr-2”>
<svg class=“h-5 text-gray-500 hover:text-red-500” fill=“none” stroke-linecap=“round” stroke-linejoin=“round” stroke-width=“2” viewBox=“0 0 24 24” stroke=“currentColor”><path d=“M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z”></path></svg>
</button>
<p class=“text-gray-800” x-text=“task.title”></p>
</div>
<button x-show=“task.status == ‘True’” class=“text-green-500 hover:text-gray-500 cursor-pointer”>Completed</button>
<button x-show=“task.status == ‘False’” class=“text-gray-500 hover:text-green-500 cursor-pointer”>Not Completed</button>
</li>
</template>
</ul>
</div>
<script>
const csrftoken = document.querySelector(‘#list-form > input’).value;
const getTasks = () => {
return {
newTask: ″,
tasks: [
{% for t in list.tasks.all %}
{ ‘title’: ‘{{ t.title }}’, ‘id’: ‘{{ t.id }}’, ‘status’: ‘{{ t.completed }}’ },
{% endfor %}
]
}
};
const addTask = async (title, listId) => {
try {
const res = await axios.post(‘/api/tasks/’,
{ title, parent_list: listId },
{ headers: { ‘X-CSRFToken’: csrftoken }}
);
location.reload();
} catch (e) {
console.error(e);
}
};
const removeTask = async taskId => {
try {
const res = await axios.delete(‘/api/tasks/’ + taskId + ‘/’,
{ headers: { ‘X-CSRFToken’: csrftoken }}
);
location.reload();
} catch (e) {
console.error(e);
}
};
</script>
{% endblock %}
Here we implemented the same way adding and removing of list tasks.
Now let’s add task status updating. Create function updateTask
const updateTask = async (taskId, taskStatus) => {
try {
const res = await axios.patch(‘/api/tasks/’ + taskId + ‘/’,
{ id: taskId, completed: taskStatus},
{ headers: { ‘X-CSRFToken’: csrftoken }}
);
location.reload();
} catch (e) {
console.error(e);
}
};
Then Add the function call on the status buttons
<template x-for=“task in tasks”>
<li class=“bg-gray-100 border border-gray-300 rounded-md shadow-sm p-2 px-4 my-2 flex justify-between font-mono”>
<div class=“flex justify-start”>
<button @click=“removeTask(task.id)” class=“mr-2”>
<svg class=“h-5 text-gray-500 hover:text-red-500” fill=“none” stroke-linecap=“round” stroke-linejoin=“round” stroke-width=“2” viewBox=“0 0 24 24” stroke=“currentColor”><path d=“M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z”></path></svg>
</button>
<p class=“text-gray-800” x-text=“task.title”></p>
</div>
<button @click=“updateTask(task.id, false)”
x-show=“task.status == ‘True’” class=“text-green-500 hover:text-gray-500 cursor-pointer”>Completed</button>
<button @click=“updateTask(task.id, true)”
x-show=“task.status == ‘False’” class=“text-gray-500 hover:text-green-500 cursor-pointer”>Not Completed</button>
</li>
</template>
And this is the basic To-Do List implemented with Django, DRF, Alpine.js, and Axios.
Next Steps
- Create users registration and pages
- Update the Dom instead of page reloads after successful Axios requests.
I hope you learned something new, give me a follow on Twitter, to see when I post new stuff.
Top comments (8)
Nice tutorial, thanks a lot!
I am Glad you liked it!
Thanks Maciej, i 'm practicing django and this is gold for me :)
Thanks, Great to hear that!
Thanks Maciej , this is a good tutorial <3
Thank you, I am happy you liked it!
I really enjoyed reading this post.
I do have a question, why showing exceptions on console?
Thank you, I appreciate it.
Do you mean the exceptions in the Axios requests? I just put them for debugging purposes, normally I would add there the code to show up an error message to the user.