Installation

Yoojin KoYoojin Ko
5 min read

Building SPA with Django and HTMX, tailwind

What I will do:

  1. Install packages for the project

  2. Create a Django project app and set up templates and static at the project level

  3. Create an app tracker with Tailwind

  4. 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.

0
Subscribe to my newsletter

Read articles from Yoojin Ko directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yoojin Ko
Yoojin Ko