ToDo List With Django, DRF, Alpine.JS, and Axios
Tags: python, django, javascript, webdev
Published: 2020-07-23 16:26:31

**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]([https://twitter.com/calebporzio](https://twitter.com/calebporzio)) 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](https://github.com/druidmaciek/blog-tutorials/tree/master/todo_list) 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

```bash
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`

```python
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.

```python
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

```python
from django.contrib import admin

from .models import List, Task

admin.site.register(List)
admin.site.register(Task)
```

Run the `makemigrations` and `migrate` commands.

```bash
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.

```python
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.

```python
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`...

```python
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.

```python
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

```python
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.

```python
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.

```python
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**

```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**

```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**

```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

```html
python manage.py createsuperuser
python manage.py runserver
```

Go to the [http://127.0.0.1:8000/admin/](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/](http://127.0.0.1:8000/admin/) and you should see view like that.

![https://janowskidev.s3-eu-west-1.amazonaws.com/images/todo_screen1.png](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](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.

```jsx
<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 

```jsx
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

```html
<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.

```jsx
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 

```html
<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.

```html
{% 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`

```jsx
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

```html
<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](https://twitter.com/MaciejJanowski), to see when I post new stuff.