How to Create a Dependent Drop Down List in Django.
Let's go throgh a scenario. You are working on a Project whereby you have three models that are related.
- model 1 - State
- model 2 - Town
model 3 - Person
A state can have many towns
- A person can only be from a single State and Town.
You are to develop a form for creating a Person. You could have created a Form, and then inherit from the django forms.ModelForm, so you could create the form from your model field.
Building the form this way is going to work, but not so efficient as everytime you want to create a Person, you will have to select a State, and then go through all the Towns including Towns that are not in the State you selected.
Let's say you have over 100 States, and each states has 200 Towns, you will have to go through the list of Towns to select a Town.
What if you could select a State, and then the Town fields shows only the Towns associated with that State.
That is exactly what we shall be doing in this Article. Creating a Dependent Field
Step 1:
Create the Model
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=100)
state = models.ForeignKey("State", on_delete=models.CASCADE)
town = models.ForeignKey("Town", on_delete=models.CASCADE)
def __str__(self):
return self.name
class State(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Town(models.Model):
name = models.CharField(max_length=100)
state = models.ForeignKey("State", on_delete=models.CASCADE, null=True, blank=True)
def __str__(self):
return self.name
Step 2:
Create the Form for creating the Person.
from django import forms
from .models import Person, Town
class PersonCreationForm(forms.ModelForm):
class Meta:
model = Person
fields = ["name", "state", "town"]
Step 3:
Create a view for creating the Person.
from django.shortcuts import render
from django.views.generic import CreateView
from .models import Person
from .forms import PersonCreationForm
class PersonCreateView(CreateView):
model = Person
form_class = PersonCreationForm
template_name = "person_create.html"
Step 4:
Edit the settings file so as to change the template file structure.
[BASE_DIR, "templates"]
Step 5:
Create a urls.py file and fill with the following.
from django.urls import path
from . import views
urlpatterns = [
path('create/', views.PersonCreateView.as_view(), name='person_create'),
]
Step 6:
Create a template folder, and create a file person_create.html
inside the folder. fill the folder with
<form method="post">
{% csrf_token %}
{{form.as_p}}
<input type="submit" value="Create">
</form>
Step 7:
Run python manage.py makemigrations
followed by python manage.py migrate
to make migrations of your model, then create a SuperUser using python manage.py createsuperuser
Step 8:
visit 127:0.0.1:8000/create
or the port you are running on to checkout the Person creation form and make sure everything is working correctly. Create some items using your admin panel, and you should have something like this.
Step 9:
Since what we want to achive is that whenever a State is selected, we want the Town dropdown to only show the Towns under that State, we have to over ride our Town field in the forms.py
to not display any item.
Add this init function to your forms.py which is setting the Town field to be none.
def __init__(self, **kwargs):
super(PersonCreationForm, self).__init__(**kwargs)
self.fields["town"].queryset = Town.objects.none()
Step 10:
get the id associated with the State fields: since we are using the ModelForm, Django automatically generate an ID for each of our fields. you can inspect the field to get the id. in this case, the id is id_state
for the state field.
Step 11:
In your template file, add the javascript code below.
<script type="text/javascript">
let state_field = document.getElementById("id_state"); // get the state_field
state_field.addEventListener("change", getStateId);
/* the code above add an event listener
that wait for change on the field, and on any change
it runs the function getStateId which get the Id of the selected state.
*/
function getStateId(e) {
let state_id = e.target.value; // assign the id of the state to state_id
console.log(e.target.value); // console the id of the selected state
}
</script>
you can play around with this by opening your browser console, and changing fields, you should see the console displaying the ID of the selected state, and the corresponding towns.
Step 12:
We are going to create a new view and url which will be used for sending the ID of the selected state to the backend, so we can get the Towns in that state from the backend and use on the front end.
import json
from django.http import JsonResponse
def get_state_id(request):
return JsonResponse({"success": "It worked"})
Step 13
In our template file, we are going to be using Javascript Fetch API for posting the data to the backend. you can read more about it here Fetch Api.
Copy the code below and paste inside the getStateId function, after console.log(e.target.value);
const data = { 'id': state_id }; // the data we are passing to the backend
let url = "{% url 'get_state_id' %}"; // the url we are posting the state id to on the backend
fetch(url, {
method: 'POST', // or 'PUT'
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then((response) => response.json())
.then((data) => {
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
});
Step 14:
If you refresh the page and try changing the State fields once again, you should get an error 403 Forbidden
. Remember, whenever we want to post data in Django, we have to use the {% csrf_token %}
tag as a security measure. Even though we are no posting the data like we normally do, we still have to pass the CSRF TOKEN.
In this case, copy the code below, and paste at the top of your script tag.
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
let csrf_token = '{{ csrf_token }}';
So alongside the header, we will be passing the csrf_token variable. Add the code below to the header inside the Fetch API function. 'X-CSRFToken': csrf_token
. The headers should now be;
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrf_token
},
Step 15:
Replace you get_state_id view function with
def get_state_id(request):
data = json.loads(request.body) # loads the posted data from the request body
state_id = data["id"] # slice for the posted state ID
towns = Town.objects.filter(state__id=state_id) # filter the towns by the state ID and return all towns for the selected state.
return JsonResponse(list(towns.values()), safe=False) # since the data is not a dictionary, use safe=False
Step 16:
We can now post data to the backend, and the backend is return the right data to the frontend. Let us now pass the town gotten from the backend into the town field. update your Javascript code with the below.
<script type="text/javascript">
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
let csrf_token = '{{ csrf_token }}';
let state_field = document.getElementById("id_state"); // get the state_field
state_field.addEventListener("change", getStateId);
/* the code above add an event listener
that wait for change on the field, and on any change
it runs the function getStateId which get the Id of the selected state.
*/
function getStateId(e) {
let state_id = e.target.value; // assign the id of the state to state_id
console.log(e.target.value); // console the id of the selected state
const data = { 'id': state_id }; // the data we are passing to the backend
let url = "{% url 'get_state_id' %}"; // the url we created for getting the state id
fetch(url, {
method: 'POST', // or 'PUT'
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrf_token
},
body: JSON.stringify(data),
})
.then((response) => response.json())
.then((data) => { // if post request is successful
console.log('Success:', data);
let townField = document.getElementById("id_town"); // get the ID of town field
townField.innerHTML = '<option value="" selected}>Select Town</option>' // add to its innerhtml
for (let i=0; i < data.length; i++ ) {
/* loop across the data gotten from the backend */
townField.innerHTML += `<option value="${data[i]['id']}">${data[i]['name']}</option>`
// console.log(${data[i]['name']});
}
})
.catch((error) => { // catches the error if post request fails.
console.error('Error:', error);
alert("Error");
});
}
</script>
Step 17:
We also need to pass the town id and name to the data we are sending to the frontend, let's update our get_state_id in the views.py with the following;
def get_state_id(request):
data = json.loads(request.body) # loads the posted data from the request body
state_id = data["id"] # slice for the posted state ID
towns = Town.objects.filter(state__id=state_id) # filter the towns by the state ID
return JsonResponse(list(towns.values("id", "name")),
safe=False) # since the data is not a dictionary, use safe=False
Step 18:
At this moment, if you try creating a person, you are going to get an error which says that the Selected town is not valid. Don't forget that towards the beginning, we set the queryset for the Town field to be none, and now we are submitting data for that same field, that is the cause of the error. In order to solve that, replace your init function in the forms.py with the code below.
def __init__(self, **kwargs):
super(PersonCreationForm, self).__init__(**kwargs)
self.fields["town"].queryset = Town.objects.none()
if 'state' in self.data: # checking if we have state in the posted data
try:
state_id = int(self.data.get('state')) # get the state ID
self.fields['town'].queryset = Town.objects.filter(state__id=state_id).order_by('name')
# the above is setting the queryset for the town field by filtering
# the Town using the posted state_id.
except (ValueError, TypeError):
pass
elif self.instance.pk:
self.fields['town'].queryset = self.instance.state_town_set.order_by('name')
Step 19:
Our Person creation form is now ready for use. Next, we will create a view to redirect to after creating a person and also a template for it.
class PersonListView(ListView):
model = Person
template_name = "person_list.html"
queryset = Person.objects.all()
context_object_name = "persons"
Create a new template file in the template directory person_list.html
{% for person in persons %}
ID: {{ person.id }} | Name: {{person.name}} <br>
{% endfor %}
Create a URL for the new view, your urls.py should be similar to this
from django.urls import path
from . import views
urlpatterns = [
path('', views.PersonListView.as_view(), name='person_list'),
path('create/', views.PersonCreateView.as_view(), name='person_create'),
path('get-data/', views.get_state_id, name='get_state_id')
]
Step 20:
Add a get_success_url function to the PersonCreateView
class PersonCreateView(CreateView):
model = Person
form_class = PersonCreationForm
template_name = "person_create.html"
def get_success_url(self):
return reverse("person_list")
If you follow the steps correctly, you should have something similar to this.
Thank you for reading. If you have any question, Kindly drop them in the comment section below. Don't forget to follow me on LinkedIn