Installation
Building SPA with Django and HTMX, tailwind
What I will do:
Install packages for the project
Create a Django project app and set up templates and static at the project level
Create an app tracker with Tailwind
Set up basic templates for Htmx
Install packages
To manage Python packages and dependencies, we need to set up a virtual environment for the project.
Usually, venv
is used for this purpose, but this project will use a modern approach, Poetry. Like venv
, Poetry creates an isolated virtual environment for the project but it generates a lock file - poetry.lock
. This ensures every user uses the same versions and helps to avoid version conflicts.
poetry init
poetry add django django-htmx 'django-tailwind[reload]'
poetry shell
The setup process is straightforward - initiate it, add packages, and activate the virtual environment.
Create a Django project and app
After installing packages, we create a Django project called app.
For UI, templates
and static
folders are located at the project level so these folders are created in a root folder.
django-admin startproject app
cd app
mkdir templates static
This creates a Django project directory structure. The new directory contains manage.py
and a project package containing settings.py
with other files.
For Django to use the packages we installed and access folders for UI, settings.py
should be edited like below.
app/settings.py
INSTALLED_APPS = [
...
'django_htmx',
'tailwind'
]
MIDDLEWARE = [
...,
'django_htmx.middleware.HtmxMiddleware'
]
STATICFILES_DIRS = [BASE_DIR / 'static']
TEMPLATES = [
{
...
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
...
},
]
Create an app with Tailwind
django-tailwind
creates a Tailwind application, which means you shouldn’t create an app with startapp
but with tailwind init
python manage.py tailwind init
[1/1] app_name (theme): tracker
Tailwind application 'tracker' has been successfully created. Please add 'tracker' to INSTALLED_APPS in settings.py, then run the following command to install Tailwind CSS dependencies: `python manage.py tailwind install`
The created Tailwind app is added on settings.py
so Django can recognise it.
app/settings.py
INSTALLED_APPS = [
...
'tracker'
]
TAILWIND_APP_NAME = 'tracker'
INTERNAL_IPS = ["127.0.0.1"]
Install Tailwind CSS dependencies, by running the following command:
python manage.py tailwind install
After installing Tailwind, folder structure will be like this.
.
├── README.md
├── app
│ ├── app
│ ├── manage.py
│ ├── static
│ ├── templates
│ └── tracker
│ ├── __init__.py
│ ├── apps.py
│ ├── static_src
│ │ ├── node_modules
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── postcss.config.js
│ │ ├── src
│ │ └── tailwind.config.js
│ └── templates
│ └── base.html
├── poetry.lock
└── pyproject.toml
There are a couple of things to amend in this structure.
First, django-tailwind
comes with a simple base.html
template located at the app level. This can be moved to templates at the project level.
Another point is that django-tailwind
builds a CSS file and copies it into static
folder at the app level. We have static
folder at the project level, so we edit the destination folder as below.
tracker/static_src/package.json
"scripts": {
"build:clean": "rimraf ../../static/css/dist",
"build:tailwind": "cross-env NODE_ENV=production tailwindcss --postcss -i ./src/styles.css -o ../../static/css/dist/styles.css --minify",
"dev": "cross-env NODE_ENV=development tailwindcss --postcss -i ./src/styles.css -o ../../static/css/dist/styles.css -w",
...
},
The structure after amend is like this.
.
├── README.md
├── app
│ ├── app
│ ├── manage.py
│ ├── static
│ ├── templates
│ │ └── base.html
│ └── tracker
│ ├── __init__.py
│ ├── apps.py
│ └── static_src
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ └── tailwind.config.js
├── poetry.lock
└── pyproject.toml
At this point, we can run the server and check if all packages and a structure are set up correctly. For this, we need to set up some views.py
and urls.py
.
#app/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('tracker.urls'))
]
#tracker/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.base_view, name="base")
]
# tracker/views.py
from django.shortcuts import render
def base_view(request):
return render(request, 'base.html')
After finishing the simplest setup, you can run the server. If you see the text with styling like below, you can confirm it is all set up correctly.
python manage.py runserver
Set up basic templates for SPA with HTMX
Finally, it is time to set up Htmx. First, download htmx.min.js
from its latest release and copy it into static
folder.
We are going to amend base.html
so it has navigation and content. With this code, when the user clicks Nav
text, views for test
URL will be displayed on <div> with id content-div
below.
{% load static tailwind_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tracker App</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
{% tailwind_css %}
</head>
<body class="bg-gray-50 font-serif leading-normal tracking-normal">
<nav class="container mx-auto">
<p class="bg-slate-200" hx-get="{% url 'test' %}" hx-target="#content-div">Nav</p>
</nav>
<div class="container mx-auto" id="content-div">
{% block content %}{% endblock content %}
</div>
<script src="{% static 'htmx.min.js' %}" defer></script>
</body>
</html>
We create templates/test.html
and put a simple text and edit urls.py
and views.py
for adding test
endpoint.
<h1>Test</h1>
# tracker/urls.py
urlpatterns = [
...
path('test', views.test_view, name="test"),
]
# tracker/views.py
def test_view(request):
return render(request, 'test.html')
Now you can see that the page works as expected. When I clicked Nav, Test
text showed up below. But when you refresh your page, you will find out that it will show only a partial page. It is because the request after refreshing the page was not made with HTMX, so it is not working with AJAX, and ends up with rendering the partial page. To fix this, as suggested in the official doc, we need to have separate HTML file depending on the request type.
# tracker/views.py
def test_view(request):
if request.htmx:
return render(request, 'test.html')
return render(request, 'test_full.html')
templates/test_full.html
file can have base.html
file and partial page test.html
{% extends 'base.html' %}
{% block content %}
{% include 'test.html' %}
{% endblock content %}
When you run the server again and test, it will work correctly.
Subscribe to my newsletter
Read articles from Yoojin Ko directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by