DEV Community

Cover image for ToDo List With Django, DRF, Alpine.JS, and Axios
Maciej
Maciej

Posted on • Edited on • Originally published at janowski.dev

ToDo List With Django, DRF, Alpine.JS, and Axios

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
Enter fullscreen mode Exit fullscreen mode

Then go to [settings.py](http://settings.py) and add lists and django rest framework app to INSTALLED_APPS

INSTALLED_APPS = [
    ...
    rest_framework,
    lists,
]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Run the makemigrations and migrate commands.

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)),
]
Enter fullscreen mode Exit fullscreen mode

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)),
]
Enter fullscreen mode Exit fullscreen mode

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
    })
Enter fullscreen mode Exit fullscreen mode

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),
]
Enter fullscreen mode Exit fullscreen mode

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)),
]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

https://janowskidev.s3-eu-west-1.amazonaws.com/images/todo_screen1.png

By clicking The Title of the list we will be moved to the list detail page

https://janowskidev.s3-eu-west-1.amazonaws.com/images/todo_screen2.png

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 %}
Enter fullscreen mode Exit fullscreen mode

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);
      }
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
      }
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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);
      }
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
brunowmoreno profile image
Bruno Queiroz

Nice tutorial, thanks a lot!

Collapse
 
druidmaciek profile image
Maciej

I am Glad you liked it!

Collapse
 
freddyxd5 profile image
Jesus More

Thanks Maciej, i 'm practicing django and this is gold for me :)

Collapse
 
druidmaciek profile image
Maciej

Thanks, Great to hear that!

Collapse
 
omarkhatib profile image
Omar

Thanks Maciej , this is a good tutorial <3

Collapse
 
druidmaciek profile image
Maciej

Thank you, I am happy you liked it!

Collapse
 
m4r4v profile image
m4r4v

I really enjoyed reading this post.

I do have a question, why showing exceptions on console?

Collapse
 
druidmaciek profile image
Maciej

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.