Hi there!
I’m using a SessionWizardView to iterate over forms and templates during user-creation of an entity and its relations. One such template uses htmx to dynamically render the dropdown selections.
The template itself works - that is, when a user selects a country, the appropriate states appear in the ‘states’ dropdown; likewise, when a user selects a state, the cities selection list is correctly populated using City.objects.filter(state_id=state). The rendering here works.
However, when the form is introduced into a special htmx template in the formtools SessionWizardView, the ‘next’ button does not move to the next form in the list. This is the issue. I have inspected the Network and there is nothing being sent when I click on ‘Next’, ‘First’, or ‘Previous’. It seems like the htmx templates I’ve made have broken the SessionWizardView from progressing.
Relevant code:
address_form.html
{% extends "base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% block head %}
{{ wizard.form.media }}
{% endblock %}
{% block content %}
<script src="//unpkg.com/htmx.org"></script>
<p>Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}</p>
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<table>
{{ wizard.management_form }}
<label class="" for="id_address_line_one">Address line one*</label>
{{ wizard.form.address_line_one }}
<p></p>
<label for="id_address_line_two">Address line two</label>
{{ wizard.form.address_line_two }}
<label for="id_postal_code">Postal code</label>
{{ wizard.form.postal_code }}
<div id="countries">
<label for="id_country">Country*</label>
<select class="custom-select mb-4"
name="country"
hx-get="{% url 'states' %}"
hx-trigger="change"
hx-target="#states">
<option selected>Country</option>
{% for country in wizard.form.country %}
<option value="{{country.pk}}">{{ country }}</option>
{% endfor %}
</select>
</div>
<div id="states" class="mb-4">
{% include 'addresses/partials/states.html' %}
</div>
<div id="cities" class="mb-4" hx-swap-oob="true">
{% include 'addresses/partials/cities.html' %}
</div>
<!-- Supplier field is hidden using inline CSS style -->
<div style="display:none;">
{{ wizard.form.supplier }}
</div>
</table>
<div class="form-group">
{% if not wizard.steps.next %}
<input class="btn btn-outline-info" type="submit" value="{% translate "Submit" %}"/>
{% else %}
<input class="btn btn-outline-info" type="submit" value="{% translate 'Next' %}"/>
{% endif %}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" class="btn btn-outline-info" type="submit"
value="{{ wizard.steps.first }}">{% translate "First" %}</button>
<button name="wizard_goto_step" class="btn btn-outline-info" type="submit"
value="{{ wizard.steps.prev }}">{% translate "Previous" %}</button>
{% endif %}
</div>
</form>
{% endblock %}
states.html
{% load i18n %}
{% load widget_tweaks %}
<label for="id_state">State*</label>
<select class="custom-select mb-4"
name="state"
id="id_state"
hx-get="{% url 'cities' %}"
hx-trigger="change"
hx-target="#cities"
>
{% if states %}
{% for state in states %}
<option value="{{ state.pk }}">{{ state }}</option>
{% endfor %}
{% else %}
<option>Select a Country first</option>
{% endif %}
</select>
cities.html
{% load i18n %}
{% load widget_tweaks %}
<label for="id_city">City*</label>
<select class="custom-select mb-4"
name="city"
id="id_city">
{% if cities %}
{% for city in cities %}
<option value="{{ city.pk }}">{{ city }}</option>
{% endfor %}
{% else %}
<option>Select a Country first</option>
{% endif %}
</select>
addresses/forms.py
from django import forms
from .models import Address, Country, State, City
from suppliers.models import Supplier
from dynamic_forms import DynamicField, DynamicFormMixin
class AddressForm(DynamicFormMixin, forms.ModelForm):
def state_choices(form):
country = form['country'].value()
return State.objects.filter(country=country)
def initial_state(form):
country = form['country'].value()
return State.objects.filter(country=country).first()
def city_choices(form):
state = form['state'].value()
return City.objects.filter(state=state)
def initial_city(form):
state = form['state'].value()
return City.objects.filter(state=state).first()
supplier = forms.ModelChoiceField(queryset=Supplier.objects.all())
address_line_one = forms.CharField(max_length=132)
address_line_two = forms.CharField(max_length=132, required=False)
postal_code = forms.CharField(max_length=10, required=False)
# State should be dynamic based on country; similarly,
# city should be dynamic based on state
country = forms.ModelChoiceField(
queryset=Country.objects.all(),
required=True,
initial=Country.objects.first()
)
state = DynamicField(
forms.ModelChoiceField,
queryset=lambda form: State.objects.filter(country=form['country'].value()),
initial=initial_state,
required=True,
)
city = DynamicField(
forms.ModelChoiceField,
queryset=lambda form: City.objects.filter(state=form['state'].value()),
initial=initial_city,
required=True,
)
class Meta:
model = Address
fields = ['address_line_one', 'address_line_two', 'postal_code', 'country', 'state', 'city']
addresses/urls.py
from django.urls import path
from .views import *
urlpatterns = [
path("cities/", cities, name='cities'),
path("states/", states, name='states'),
]
addresses/views.py
from django.shortcuts import render
from .forms import *
def cities(request):
state = request.GET.get('state')
cities = City.objects.filter(state_id=state)
context = {'cities': cities} # i have also tried 'wizard.form.states' here and in the templates
return render(request, 'addresses/partials/cities.html', context)
def states(request):
country = request.GET.get('country')
states = State.objects.filter(country_id=country)
context = {'states': states} # i have also tried 'wizard.form.states' here and in the templates
return render(request, 'addresses/partials/states.html', context)
relevant WizardView:
class SupplierWizardView(LoginRequiredMixin, SessionWizardView):
form_list = [('supplier', SupplierForm),
('address', AddressForm),
('contact', ContactPersonForm),
('risks', RiskForm),
('documents', DocumentsForm),
('history', HistoryForm), ]
templates = {'supplier': "suppliers/supplier_create_wizard.html",
'address': "addresses/address_form.html",
'contact': "suppliers/supplier_create_wizard.html",
'risks': "suppliers/supplier_create_wizard.html",
'documents': "suppliers/supplier_create_wizard.html",
'history':"suppliers/supplier_create_wizard.html",
}
file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, "tmp"))
def get_template_names(self):
return self.templates[self.steps.current]
def done(self, form_list, **kwargs):
supplier = form_list[0].save() # Save supplier to DB
# Iterate through other forms; add supplier as FK to that form, then save it.
for i in range(1, len(form_list)):
instance = form_list[i].save(commit=False)
instance.supplier = supplier
instance.save()
messages.success(self.request, f'Supplier "{supplier.name}" created successfully!')
return redirect('home')
I’m really not sure where to begin. I’ve tried a lot of options but I think that maybe htmx and WizardViews just don’t work. Advice? Guidance?
Thanks!