Creating Cascading Selects in Django with HTMX

Adi Ramadhan
3 min readNov 10, 2024

--

Reference https://www.djangoproject.com/ and https://htmx.org/

Note: Django Admin used for add category and subcategory data.

Step 1: Preparation, Create Django Project, Inital Migration
create virtualenv: virtualenv venv
start virtualenv: venv/Scripts/activate
install Django in virtualenv: pip install django==4.2
Create Django: django-admin startproject myproject
Go to myproject folder: cd myproject
Initial Migration: python manage.py migrate

Step 2: Create Django Apps
Create apps: python manage.py startapp myapp

Step 3: Project Setting: Register Apps, Set Templates Folder (myproject/settings.py)

...

INSTALLED_APPS = [
...
'myapp', #updated
]

...

TEMPLATES = [
...
'DIRS': [Path(BASE_DIR, 'templates')], #updated
...
]


...

Step 4: Add Model in myapp/models.py

from django.db import models

# Create your models here.
class Category(models.Model):
name = models.CharField(db_column='name', max_length=100)

class Meta:
verbose_name = 'Category'
verbose_name_plural = 'Categories'
def __unicode__(self):
return self.name
def __str__(self):
return self.name

class Subcategory(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE)
name = models.CharField(db_column='name', max_length=100)

class Meta:
verbose_name = 'Subcategory'
verbose_name_plural = 'Subcategories'
def __unicode__(self):
return self.name
def __str__(self):
return self.name

Step 5: Register Model in Django Admin

from django.contrib import admin

# Register your models here.
from django.contrib import admin
from .models import Category, Subcategory

class CategoryAdmin(admin.ModelAdmin):
list_display = ('name',)

admin.site.register(Category, CategoryAdmin)


class SubcategoryAdmin(admin.ModelAdmin):
list_display = ('name','category')

admin.site.register(Subcategory, SubcategoryAdmin)

Step 6: Makemigrations and Migrate
Make migrations: python manage.py makemigrations
Migrate: python manage.py migrate

Step 7: Create HTML Files in Templates Folder

Create templates folder in main app

Create templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Cascading Select</h1>
<form method="POST">
{% csrf_token %}
<label>Category</label>
<select id="category" name="category" hx-get="{% url 'subcategory_choice' %}" hx-target="#subcategory" hx-indicator=".htmx-indicator">
<option value="" selected>Select category</option>
{% for category in categories %}
<option value={{category.id}}>{{category.name}}</option>
{% endfor %}
</select>
<label>Subcategory</label>
<select id="subcategory" name="subcategory">
</select>
<span class="htmx-indicator">
Loading...
</span>
<br>
<br>
<button type="submit">Submit</button>
</form>

<script src="https://unpkg.com/htmx.org@2.0.3"></script>

</body>
</html>

Create templates/subcategory_choice.html

{% if subcategories %}
{% for subcategory in subcategories %}
<option value={{subcategory.id}}>{{subcategory.name}}</option>
{% endfor %}
{% endif %}

Step 8: Create Function Views myapp/views.py

from django.shortcuts import render, redirect
from .models import Category, Subcategory

def index(request):
categories = Category.objects.all()
if request.method == "GET" :
return render(request, 'index.html', {'categories': categories})
elif request.method == "POST":
selected_category_id = request.POST.get("category")
selected_subcategory_id = request.POST.get("subcategory")
print("Category ID", selected_category_id)
print("Subcategory ID", selected_subcategory_id)
return redirect("index")

def subcategory_choice(request):
category = request.GET.get("category")
if category:
subcategories = Subcategory.objects.filter(category__id = category)
return render(request, 'subcategory_choice.html', {'subcategories': subcategories})
else:
return render(request, 'subcategory_choice.html', {})

Step 9: Setup URLS

Create myapp/urls.py


from django.urls import path
from . import views

urlpatterns = [
path('', views.index, name="index"),
path('subcategory_choice', views.subcategory_choice, name="subcategory_choice"),
]

Update myproject/urls.py

from django.contrib import admin
from django.urls import path, include #updated

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('myapp.urls')), #updated
]

Step 10: Create Superuser
Create superuser: python manage.py createsuperuser
Type username, email password and retype password

Step 11: Run Server and Testing
Run Server: python manage.py runserver
Testing:

a. Login in Django Admin, add categories and subcategories http://127.0.0.1:8000/admin

Category Data
Subcategory Data

b. Test Cascading Select
http://127.0.0.1:8000/

--

--