To Do List - Part 5: Improve the UI
Welcome to Part 5 of my tutorial series on creating a To Do list app with Django.
In Parts 1 to 4, we focused on adding functionality to our app. In this part, we will focus on building out the UI. We will improve the layout. In Parts 6, 7 and 8, we will add interactivity to the UI, letting users edit, delete and filter tasks.
What our UI looks like at the moment
What the to-do list will look like
Code
As usual, the code for this tutorial is available to view on GitHub. The initial code for this tutorial can be found here, and the finished code here.
Step 1: Partials
Partials are snippets of HTML that you can import into templates. These are good for snippets that will appear in multiple places.
To create a partial, start by creating a directory inside the templates
directory called partials
.
Todo row
Create a file called todo_row.html
and enter the following code:
<!-- todo/templates/partials/todo_row.html -->
<tr id="task-row-{{task.id}}">
{% if task.status == Status.DONE %}
<td>
<del>{{task.name}}</del>
</td>
{% else %}
<td>{{task.name}}</td>
{% endif %}
<td>
{% if task.status == Status.TODO %}
<div class="btn btn-danger border">To Do</div>
{% else %}
<a class="btn btn-light border">To Do</a>
{% endif %}
{% if task.status == Status.DOING %}
<div class="btn btn-primary border">Doing</div>
{% else %}
<a class="btn btn-light border">Doing</a>
{% endif %}
{% if task.status == Status.DONE %}
<div class="btn btn-success border">Done</div>
{% else %}
<a class="btn btn-light border">Done</a>
{% endif %}
</td>
<td>
<a class="m-2">
<i class="link-dark bi-x-lg"></i>
</a>
</td>
</tr>
This is the HTML for a single row of the table.
Todo list
Create another file in the partials
folder called todo_list.html
.
<!-- todo/templates/partials/todo_list.html -->
<table class="table">
<thead>
<tr>
<th scope="col">Task</th>
<th scope="col">Status</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="todo-rows">
{% for task in tasks %}
{% include 'partials/todo_row.html' %}
{% endfor %}
</tbody>
</table>
This snippet creates the todo list table.
Inside the for-loop, we include the partial for the row.
Note: If you plan on using htmx, partials are very helpful, because htmx works by swapping HTML inside tags. You can get your view to render a partial instead of a full template to selectively rerender parts of a page. If you don’t plan on using htmx, then the decision to use partials depends on your personal code style.
Step 2: Create a base template
A base template is useful to provide a common layout to all of your pages.
Even though this project just has one page, a base template is a good place to add our <head>
tag and install Bootstrap without cluttering the index.html
template.
In todo/templates
, create a file called base.html
.
Add the following code:
<!-- todo/templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To Do</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
</head>
<body class="bg-light">
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
{% block javascript %}
<scr?pt src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2"
crossorigin="anonymous"></scr?pt>
{% endblock %}
</html>
If you would like to understand base templates in more detail, I have a separate tutorial here.
Step 3: Add a new index.html
In todo/templates
, create a file called index.html
and add the following code:
<!-- todo/templates/index.html -->
{% extends 'base.html' %}
{% block content %}
<h1 class="text-center my-5">To Do List</h1>
<form class="w-50 mx-auto mb-5 d-flex" id="todo-form" method="post">
{% csrf_token %}
{{ form }}
<input class="btn btn-primary mx-1" type="submit" value="Add">
</form>
<section class="card p-4">
<ul id="tabs" class="nav nav-pills mb-4 pb-2">
<li class="nav-item">
<a class="nav-link active" href="#">All</a>
</li>
<li class="nav-item">
<a class="nav-link">To Do</a>
</li>
<li class="nav-item">
<a class="nav-link">Doing</a>
</li>
<li class="nav-item">
<a class="nav-link">Done</a>
</li>
</ul>
<section id="todo-list">
{% include 'partials/todo_list.html' %}
</section>
</section>
{% endblock %}
The template extends the base.html
template.
Step 4: Update the view
We now need to update our index view to use the our new template, index.html
, instead of the old one, temp_index.html
.
return render(request, "index.html", context)
Start the server and go to localhost:8000
in the browser.
You should now see this:
The list now has buttons to filter by status.
There is also an actions column. Users can press the green check icon to mark a task as complete, or the red trash icon to delete a task.
None of these additional features work yet. We will handle these in the next tutorial.
Add status to context (fix formatting)
There is some extra formatting in the templates that hasn’t been applied.
For example:
{% if task.status == Status.TODO %}
<div class="btn btn-danger border">To Do</div>
{% else %}
<a class="btn btn-light border">To Do</a>
{% endif %}
We need to pass the status choices into the template.
Go to the view and set a new key called Status
:
context = {
"tasks": tasks,
"form": form,
"Status": Task.StatusChoice
}
Save the file and refresh the page, you should now see this: