This site runs best with JavaScript enabled.

Dynamic Filtered Drop-Down Choice Fields With Django

Dynamic Filtered Drop-Down Choice Fields With Django Dynamic Filtered Drop-Down Choice Fields With Django

Photo by Rene Böhmer on Unsplash

I'm enjoying the Django framework. Sometimes, what was seems rather easy (relatively speaking) gets sort of complicated when working within the boundaries of a framework. Such was the case with my latest endeavor to filter on drop-down based on the selection of another drop-down.

Here is the situation: I have a database with car makes and models. When a user selects a make, I want to update the models drop-down with only the models associated with that make. Sounds pretty common, right? The database has a lot of entries, so I don't want to do it with pure javascript because that could make for a big script, and maybe I don't want to give away all my data so easily so some hacker can just parse a simple javascript file to get all the make/model data out of my database (not that I care, but some people might). Therefore I want to use Ajax to populate the data.

First, the proof of concept. I created this simple html file to test how it might work:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "[http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"](http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd)>

<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <title>Update Drop-Down Without Refresh</title>
    <script type="text/javascript" charset="utf-8">
      function FilterModels() {
        var makeslist = document.getElementById('makes')
        var modelslist = document.getElementById('models')
        var make_id = makeslist.options[makeslist.selectedIndex].value
        var modelstxt = new Array()
        modelstxt[1] = '1\tEscort\n2\tTaurus'
        modelstxt[2] = '1\tAltima\n2\tMaxima'
        var models = modelstxt[make_id].split('\n')
        for (var count = modelslist.options.length - 1; count > -1; count--) {
          modelslist.options[count] = null
        }
        for (i = 0; i < models.length; i++) {
          var modelvals = models[i].split('\t')
          var option = new Option(modelvals[1], modelvals[2], false, false)
          modelslist.options[modelslist.length] = option
        }
      }
    </script>
  </head>
  <body>
    <p>
      This is a proof of concept to update a select (drop-down) list of values,
      based on the selection of another select (drop-down) element using ajax.
    </p>
    <form action="" method="get" accept-charset="utf-8">
      <select name="makes" onchange="FilterModels()" id="makes">
        <option>--</option>
        <option value="1">Ford</option>
        <option value="2">Nissan</option>
      </select>
      <select name="models" id="models">
        <option>Choose Make</option>
      </select>
    </form>
  </body>
</html>

That basically helped me organize the code I needed to change the drop-down, now I just need to plug in the Ajax and server-side code to make it happen.

Notice, in the proof of concept, I used a tab-delimited format for my models. I really didn't want to mess with parsing XML or JSON or whatever. So I wrote a model feed based on my automobile make ID.

URL:

    (r'^feeds/models-by-make-id/(\d+)/$', 'autos.views.feed_models'),

View:

def feed_models(request, make_id):
    make = AutoMake.objects.get(pk=make_id)
    models = AutoModel.objects.filter(make=make)
    return render_to_response('feeds/models.txt', {'models':models}, mimetype="text/plain")

Template:

That gave me the data feed that I needed to request via Ajax. Now for my form. Because the model field loads dynamically, I had to override the clean function in the ChoiceField class and use it instead so I didn't receive any invalid choice errors:

class DynamicChoiceField(forms.ChoiceField):
    def clean(self, value):
        return value

class MyForm(forms.Form):
    make = ModelChoiceField(AutoMake.objects, widget=forms.Select(attrs={'onchange':'FilterModels();'}))
    model = DynamicChoiceField(widget=forms.Select(attrs={'disabled':'true'}), choices=(('-1','Select Make'),))

Notice that I am making a call to FilterModels() on my ModelChoiceField's onchange event. So, here is that code as well (Note: I'm using the Prototype library for the Ajax call):

function FilterModels(sel_val) {
  var modelList = $('id_model')
  for (var count = modelList.options.length - 1; count > -1; count--) {
    modelList.options[count] = null
  }
  modelList.options[0] = new Option('Loading...', '-1', false, false)
  modelList.disabled = true

  var makeList = $('id_make')
  var make_id = makeList.options[makeList.selectedIndex].value
  if (make_id > 0) {
    new Ajax.Request('/feeds/models-by-make-id/' + make_id + '/', {
      method: 'get',
      onSuccess: function (transport) {
        var response = transport.responseText || 'no response text'
        var kvpairs = response.split('\n')
        for (i = 0; i < kvpairs.length - 1; i++) {
          m = kvpairs[i].split('\t')
          var option = new Option(m[1], m[0], false, false)
          modelList.options[i] = option
        }
        modelList.disabled = false
        if (sel_val > 0) {
          modelList.value = sel_val
        }
      },
      onFailure: function () {
        alert('An error occured trying to filter the model list.')
        modelList.options[0] = new Option('Other', '0', false, false)
        modelList.disabled = false
      },
    })
  } else {
    modelList.options[0] = new Option('Select Make', '-1', false, false)
    modelList.disabled = true
  }
}

I also make a call to this function in my template after the form loads. This is incase the form is refreshed, or it fails validation and a make is still selected. It will populate the model list and select the value that was selected previously:

<script type="text/javascript" charset="utf-8">
  FilterModels();
</script>

Note that in order to get the model_id, I had to set it in my view:

def add_classified(request):
    if request.method == 'POST':
        form = AdForm(request.POST)
    if request.POST.has_key('model'):
        model_id = request.POST['model']
    else:
        model_id = 0
    return render_to_response('add_classified.html', {'form':form,'model_id':model_id}, context_instance=RequestContext(request))

I think that's about it... Did I miss anything?

Share article
Dustin Davis

Dustin Davis is a software engineer, people manager, hacker, and entreprenuer. He loves to develop systems and automation. He lives with his wife and five kids in Utah.