Django and HTMX Chained Dropdown

In this video, we build a simple chained dropdown using Django and HTMX. We will look at a simple scenario where the first dropdown contains a set of university courses, and the second dropdown contains a set of modules for that course. The contents of the second dropdown depend on the selection of the first dropdown, and we will use HTMX to perform actions when an option in the first dropdown is selected.

The associated video for this post can be found below:


The final project should allow a no-refresh updating of the list of modules, as seen below:

Objectives

In this post, we will:

  • Learn how to build a chained/dependent dropdown using Django and HTMX

Project Setup

The starter code for this tutorial can be found here on Github.

After cloning the repository, you can then install the requirements from the requirements.txt file using the following command:

pip install -r requirements.txt

After running the install command, you should be ready to go. We are going to start by building our models!

Models

There are a few simple models that we will work with. We want to build a system for a university, which has a Course model, and a Module model. The courses and modules in our system should have a name, and the module should belong to a course.

Let's build these models now!

from django.db import models

class Course(models.Model):
    name = models.CharField(max_length=128)

class Module(models.Model):
    name = models.CharField(max_length=128)
    course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='modules')

Both of these modes have a simple CharField for the name of the entity. The Module also has a Foreign Key to the course model, linking them together relationally in our database.

Now, we need to use Django's migrations system to reflect these changes in the database.

python manage.py makemigrations
python manage.py migrate

This should create the two tables in our database, for each of these models. Now, we're going to populate these tables with some dummy data for use in the tutorial, and we'll use a custom management command to do this!

Populating the Tables with Dummy Data

In the starter code that was cloned from Github, there is a custom management command called load_data. We're going to fill this command with some code that will create some Course objects, and also create some Module objects that are correctly associated with their parent course.

Within the core\management\commands\load_data.py file, add the following code:

from django.core.management.base import BaseCommand
from core.models import Course, Module

class Command(BaseCommand):
    help = 'Load Courses and Modules'

    def handle(self, *args, **kwargs):
        Module.objects.all().delete()
        course_names = [
            'Computer Science', 'Mathematics', 'Physics', 'Film Studies'
        ]

        if not Course.objects.count():
            for course_name in course_names:
                Course.objects.create(name=course_name)

        # Computer Science
        cs = Course.objects.get(name='Computer Science')

        compsci_modules = [
            'AI',
            'Machine Learning',
            'Web Development',
            'Software Engineering', 
            'NoSQL Databases'
        ]

        for module in compsci_modules:
            Module.objects.create(name=module, course=cs)

        # Maths
        math = Course.objects.get(name='Mathematics')
        math_modules = [
            'Linear Algebra',
            'Differential Equations',
            'Graph Theory',
            'Topology',
            'Number Theory'
        ]

        for module in math_modules:
            Module.objects.create(name=module, course=math)

        # PHYSICS
        physics = Course.objects.get(name='Physics')
        physics_modules = [
            'Quantum Mechanics',
            'Optics',
            'Astronomy',
            'Solid State Physics',
            'Electromagnetic Theory'
        ] 
        for module in physics_modules:
            Module.objects.create(name=module, course=physics)

        # Film
        film = Course.objects.get(name='Film Studies')

        film_modules = [
            'Film Noir',
            'Silent Cinema',
            'American Independent Cinema',
            'Avant-Garde Cinema',
            'Scriptwriting'
        ]

        for module in film_modules:
            Module.objects.create(name=module, course=film)

This command can be run on the terminal, as below:

python manage.py load_data

This should populate the database with the courses and modules specified in the script above. You can verify this in the Django shell, accessible with python manage.py shell_plus, by running the following code.

Course.objects.count()  # should be 4
Module.objects.count() # should be 20

Now that our database is populated, we will create a URL, view and template for rendering a simple dropdown page with our lists of courses and modules.

Within the core\urls.py file, add another path with the code below:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.courses, name='courses')
]

We now need to create a new view called courses within our views.py file. Add the following code:

from django.shortcuts import render
from .models import Course, Module

# Create your views here.
def courses(request):
    courses = Course.objects.all()
    context = {'courses': courses}
    return render(request, 'university.html', context)

This view fetches all of the courses from the database, and renders a template called university.html, attaching all courses to that template's context. Let's now create this template, and add to it the following code:

{% extends 'base.html' %} 

{% block content %}
<select class="custom-select mb-4" name="course">

    <option selected>Open this select menu</option>
    {% for course in courses %}
        <option value="{{course.pk}}">{{ course.name }}</option>
    {% endfor %}
</select>

<div id="modules">
    {% include 'partials/modules.html' %} 
</div>
{% endblock %}

We extend the base.html template, and within the content block we set up a <select> element for our dropdown box - this is the dropdown containing our list of courses. The options are defined by looping over the courses in our context, and adding one option for each course, with the value attribute (i.e. the value sent to the Django server if selected) being set to the primary key of that course object.

Below the select dropdown, we have a <div> element which encloses an {% include 'partials/modules.html' %} template tag. This partial will contain our second <select> element, for our modules.

We need to create that template now, so add a templates/partials directory and add the modules.html file. To this file, we'll add the code for another select box.

<select class="custom-select mb-4" name="course">

    <option selected>Open this select menu</option>
    {% for module in modules %}
        <option value="{{module.pk}}">{{ module.name }}</option>
    {% endfor %}
</select>

This time, we're looping over the modules, which we don't currently have in our context, therefore no options will be shown in this dropdown at the moment! We are going to get HTMX to handle fetching the correct set of modules, depending on the course that is selected in the original dropdown.

To do this, we need to add some HTMX attributes to our course dropdown.

Adding HTMX Attributes

Within the core\templates\university.html file, let's amend our <select> element and add to it some HTMX attributes, as below:

<select class="custom-select mb-4" 
    name="course" 
    hx-get="{% url 'modules' %}"
    hx-trigger="change"
    hx-target="#modules">

We are wiring up a GET request to a modules URL, which we'll create soon. The view associated with this URL will return the modules.html partial with the correct set of modules, depending on which course has been picked in this dropdown.

The trigger for the request will be whenever the selected option is changed, and the hx-target specifies that the response will be swapped into the <div id="modules"> element, which is the parent element for the modules.html fragment.

We now need to create the URL that is referenced by the hx-get attribute. Within our urls.py file, add the following route to the urlpatterns:

path('modules/', views.modules, name='modules')

Now, let's create the associated view in the views.py file.

def modules(request):
    course = request.GET.get('course')
    modules = Module.objects.filter(course=course)
    context = {'modules': modules}
    return render(request, 'partials/modules.html', context)

This view will receive the GET request sent by HTMX, and the value of the option selected within the dropdown will be available on the request.GET dictionary (line 2). Since the value is equal to the primary key of the parent course, we can then filter down the modules to only those that belong to the selected course (line 3). Finally, we render the partials/modules.html template with the queryset of the correct modules added to the context.

This setup should now allow the list of modules to change depending on the selected course! You should see something similar to the following:

Notice how changing the course selected (the top dropdown) modifies the list of modules available in the second dropdown. We have successfully implemented a chained dropdown!

Summary

In this post, we've implemented a chained dropdown, where the modules loaded into our second dropdown are dependent on the course selection in the first dropdown. This is very simple to do with HTMX, and all we are doing is sending a GET request whenever the parent dropdown changes to fetch the correct set of dependent modules.

If you enjoyed this post, please subscribe to our YouTube channel and follow us on Twitter to keep up with our new content!

Please also consider buying us a coffee, to encourage us to create more posts and videos!

;