How to Create a Dependent Drop Down List in Django.

Photo by Faisal on Unsplash

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.

firefox_Ih2Hsu18R4.gif

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.

firefox_LpdSPEc9oq.gif

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.

firefox_2duFKM9Y8B.gif 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