Create dependent dropdown with Django and HTMX

Recently, I faced the challenge of creating dynamic, interdependent form fields in my Django application. After some trial and error, I found a solution using Django and HTMX that I’d like to share. This combination allowed for seamless, server-side updates without full page reloads, significantly enhancing the user experience and performance of my application.

The Challenge: Implementing Interconnected Form Fields

Imagine you’re developing a web page with three interconnected fields:

  • Date_1
  • Category
  • Date_2

Each field was a dropdown (HTML select element), and their values depended on the selections made in the previous fields. Additionally, a chart on the page had to update based on these selections. It seemed complex at first, but I managed to find a workable solution.

The Solution: Leveraging Django Forms and HTMX

Here’s the approach I took, broken down into steps:

  1. Define the field order
  2. Create a custom Django Form
  3. Create a view to load initial values into the the first field and handle form submission
  4. Create a template to show the form
  5. Create views to load options into dependent fields
  6. Create templates to replace dependent field options of the form in your main view

Step 1: Define the field order

In my solution, I established a logical sequence for filling out the form fields. The process begins with the Date_1 field. Once a value is selected for Date_1, it triggers the loading of relevant options for the Category field. This dependency ensures that only applicable categories are presented based on the chosen date. After both Date_1 and Category are completed, the system then populates the Date_2 field with appropriate options.

Step 2: Creating the Custom Form

I created a custom Django Form with only the necessary fields. Here’s the detailed custom form I developed:

from django import forms
from .models import MyModel

class MyModelCustomForm(forms.Form):
    class Meta:
        model = MyModel
        fields = ['date_1', 'category', 'date_2']
    
    date_1 = forms.DateField(
        label="Date 1",
        widget=forms.Select(
            choices=[("", "Please Select a Value")] + 
                    [(x.strftime('%Y-%m-%d'), x.strftime('%Y-%m-%d')) 
                     for x in MyModel.objects.dates('date_1', 'day')],
            attrs={'hx-get': 'load-associated-category', 
                   'hx-target': '#id_category', 
                   'hx-trigger': 'change'}
        )
    )
    category = forms.CharField(
        label="Category",
        widget=forms.Select(
            attrs={'hx-get': 'load-date-2', 
                   'hx-target': '#id_date_2', 
                   'hx-trigger': 'change',
                   'hx-include': '#id_date_1'}
        )
    )
    date_2 = forms.DateField(
        label='Date 2',
        widget=forms.Select()
    )

The HTMX attributes needs to be in the field tags. One way to do this is through the “attrs” attribute of “forms.Select” method. In this form, we’re using HTMX attributes to enable dynamic updates:

  • hx-get: Specifies the URL to fetch updated data
  • hx-target: Identifies the HTML element to update
  • hx-trigger: Defines the event that triggers the HTMX request
  • hx-include: Allows sending additional data with the request

Step 3: Implementing the Main View

I created a view to handle both the initial load of the form and the form submission:

from django.shortcuts import render
from .models import MyModel
from .forms import MyModelCustomForm

def home(request):
    if request.method == 'POST':
        form = MyModelCustomForm(request.POST)
        if form.is_valid():
            # Process form data and render chart
            return render(request, 'chart.html', context)
    else:
        form = MyModelCustomForm()
    
    context = {'form': form}
    return render(request, 'main.html', context)

This view handles both GET and POST requests, initializing the form or processing submitted data accordingly.

Step 4: Creating the Main Template

I created a template to display the form and handle form submission:

<h1>Dynamic Dropdown Demo</h1>
<form method="post" hx-post="{% url 'home' %}" hx-target="#chart" hx-swap="innerHTML">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Submit</button>
</form>
<div id='chart'>Chart will appear here</div>

This template uses HTMX to update the chart div when the form is submitted.

Step 5: Implementing Dependent Field Updates

To load new options for the dependent fields, I created these views:

def load_associated_category(request):
    selected_date_1 = request.GET.get('date_1')
    category_list = MyModel.objects.filter(date_1=selected_date_1).values_list('category', flat=True).distinct()
    context = {'category': category_list}
    return render(request, 'category_select.html', context)

def load_date_2(request):
    selected_date_1 = request.GET.get('date_1')
    selected_category = request.GET.get('category')
    date_2_list = MyModel.objects.filter(
        date_1=selected_date_1,
        category=selected_category
    ).values_list('date_2', flat=True).order_by('date_2').distinct()
    date_2_list = [x.strftime('%Y-%m-%d') for x in date_2_list]
    context = {'date_2': date_2_list}
    return render(request, 'date_2_select.html', context)

These views filter data based on previous selections and return updated options for the dependent fields.

Step 6: Creating Templates for Updated Fields

Finally, I created simple templates to render the updated options for the dependent dropdowns:

<option value="">Please select</option>
{% for x in category %}
    <option value="{{ x }}">{{ x }}</option>
{% endfor %}
<option value="">Please select</option>
{% for x in date_2 %}
    <option value="{{ x }}">{{ x }}</option>
{% endfor %}

These templates render the updated options for our dependent dropdowns.

Conclusion

By following these steps, I created a dynamic, interdependent form that updates in real-time without full page reloads, providing a smooth and responsive user experience.

Remember to adjust the code to fit your specific model and requirements. Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *