If you have worked with Django forms, you have come across the need to create dependent and chained dropdown lists. This is usually required when you have multiple select fields, whose content depends on the selection of a previous select.
For example — if you want to enter simple addresses into a form, you could have people type out their country, state, and city manually. The manual option could lead to people mis-spelling the same state/province or calling it different names, which will become a problem later on if you want to analyze this data and create reports on your database. Therefore, what makes sense is to let your uses select from a dropdown list, since you know all the countries in the world — you can populate that list. However once they have selected a country, how do you prepare the province/state selection?
Ideally, you would want to know what country they have selected, so you can show them options of states/provinces only in that country. If they select a different country, you want to adjust your state selection accordingly.
Python Django Creating Dependent Chained Dropdown Select List
The reason why this is so tricky for Django applications is, the solution is a frond-end JQuery one. Django is pre-built to manage the front-end form rendering, and we will need to unpack that process and get in there to manually render the form and dynamically enter select options.
In addition, the second province/state select options can only be added once a user has made an initial country selection, before they submit the form.
What we are going to cover
-
Get data for country and province/state to use in our application
-
Create our Django Models
-
Create our Django Forms
-
Front-end implementation with JQuery
-
Server view processing
Country, Province/State, and City JSON data
I came across this amazing country, state, and city database on Github that is open source and free to use on personal and commercial projects. They also have an API you can access and build into your app. I downloaded the data set and refer to it directly from my Django application.
I created a new folder called data inside the static folder and added the JSON file there. The path to where the JSON file is saved is:
filepath = './static/data/countries_states_cities.json'
Ensure you can access data from the app
Before you proceed, at this point — you might want to check that you can:
-
Read the JSON file
-
Given a country, you can parse the JSON to return the state/province
Create a python file in the root of the directory and paste this:
NOTE: Check the indents in the file if you copy and paste
import json
def readJson(filename):
with open(filename, 'r') as fp:
return json.load(fp)
def get_country():
""" GET COUNTRY SELECTION """
filepath = './static/data/countries_states_cities.json'
all_data = readJson(filepath)
all_countries = [('-----', '---Select a Country---')]
for x in all_data:
y = (x['name'], x['name'])
all_countries.append(y)
return all_countries
def return_state_by_country(country):
""" GET STATE SELECTION BY COUNTRY INPUT """
filepath = './static/data/countries_states_cities.json'
all_data = readJson(filepath)
all_states = []
for x in all_data:
if x['name'] == country:
if 'states' in x:
for state in x['states']:
y = (state['name'], state['name'])
all_states.append(state['name'])
else:
all_states.append(country)
return all_states
These two functions are the engine of what we need to do.
The first function:
get_country()
Should return a list of all countries in the database, you should call this function inside of the Django form, when you are specifying the select options. You do not have to build this list dynamically, so you can call it once inside the form (See form section below).
The second function:
def return_state_by_country(country):
Needs to be called dynamically on the run, once a user selects a country, that country is added as the argument to this function to return states/provinces for that selected country.
You can use manual selections to test these functions at this point, make sure that you parsed the JSON correctly and that, entering a Country — will return all the states/provinces in that country.
Django Models
We need a model for this form for the address, you can use your own code — or this one below for testing:
from django.db import models
class Address(models.Model):
country = models.CharField(null=True, blank=True, max_length=100)
state = models.CharField(null=True, blank=True, max_length=100)
def __str__(self):
return '{} {}'.format(self.country, self.state)
Note it is not necessary to state the fields as select fields in the model itself, we can handle this in the form.
Django Forms
from django import forms
from .models import Address
import json
def readJson(filename):
with open(filename, 'r') as fp:
return json.load(fp)
def get_country():
""" GET COUNTRY SELECTION """
filepath = './static/data/countries_states_cities.json'
all_data = readJson(filepath)
all_countries = [('-----', '---Select a Country---')]
for x in all_data:
y = (x['name'], x['name'])
all_countries.append(y)
return all_countries
def return_state_by_country(country):
""" GET STATE SELECTION BY COUNTRY INPUT """
filepath = './static/data/countries_states_cities.json'
all_data = readJson(filepath)
all_states = []
for x in all_data:
if x['name'] == country:
if 'states' in x:
for state in x['states']:
y = (state['name'], state['name'])
all_states.append(state['name'])
else:
all_states.append(country)
return all_states
class AddressForm(forms.ModelForm):
country = forms.ChoiceField(
choices = get_country(),
required = False,
label='Company Country Location',
widget=forms.Select(attrs={'class': 'form-control', 'id': 'id_country'}),
)
class Meta:
model = Address
fields = ['country']
Note we did not include the field for the state in the form, we are going to add it manually in the HTML.
Front End Implementation in HTML and JavaScript
In the front end, we will be using AJAX to send data to the back-end while the form is being completed when the user selects and country, and return state/province data to populate the state dropdown
<form class="" action="" method="post">
{% csrf_token %}
{% for error in errors %}
<div class="alert alert-danger mb-4" role="alert">
<strong>{{ error }}</strong>
</div>
{% endfor %}
<div class="row">
<div class="col-lg-6">
<div class="mb-4">
{{ form.country}}
</div>
</div>
<div class="col-lg-6">
<div class="mb-4">
<div class="form-group">
<label >Select a Province/State</label>
<select id="id_province" class="form-control" name="state">
<option value="-----">Select Province/State</option>
</select>
</div>
</div>
</div>
</div>
</form>
Add the following JS Code, make sure you import JQuery before this code, AJAX needs jQuery.
<script>
$("#id_country").change(function () {
var countryId = $(this).val();
$.ajax({
type: "POST",
url: "{% url 'get-province' %}",
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}',
'country': country
},
success: function (data) {
console.log(data.provinces);
let html_data = '<option value="-----">Select Province/State</option>';
data.provinces.forEach(function (data) {
html_data += `<option value="${data}">${data}</option>`
});
$("#id_province").html(html_data);
}
});
});
</script>
The AJAX call requires two things to run successfully: (1) URLs set up for the Ajax call and a view function in the server to return provinces/states from the selected country.
In your URLS File
Add these URLs
path('get-province',views.getProvince, name='get-province'),
path('process-form',views.processForm, name='process-form'),
The first URL will handle the AJAX call and should look like this:
def getProvince(request):
country = request.POST.get('country')
provinces = return_state_by_country(country)
return JsonResponse({'provinces': provinces})
That function: return_state_by_country is the same function I have already shared with you, you can paste it into this file. Make sure the correct libraries are imported at the top of the file.
The second file will process the form. Since we did not include the state/province variable in the form, we have to fish it out manually from the request.POST and add it manually to the model before saving.
The view route should look something like this.
def processForm(request):
context = {}
if request.method == 'GET':
form = AddressForm()
context['form'] = form
return render(request, 'address.html', context)
if request.method == 'POST':
form = AddressForm(request.POST)
if form.is_valid():
selected_province = request.POST['state']
obj = form.save(commit=False)
obj.state = selected_province
obj.save()
#Complete the rest of the view function
That's it for this topic. Thank you for reading.