This site runs best with JavaScript enabled.

Dynamic Filtered Drop-Down Choice Fields With Django


How to filter the options of a <select> component based on the selection of another <select> component.

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:

1<!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)>
2
3<html>
4 <head>
5 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
6 <title>Update Drop-Down Without Refresh</title>
7 <script type="text/javascript" charset="utf-8">
8 function FilterModels() {
9 var makeslist = document.getElementById('makes')
10 var modelslist = document.getElementById('models')
11 var make_id = makeslist.options[makeslist.selectedIndex].value
12 var modelstxt = new Array()
13 modelstxt[1] = '1\tEscort\n2\tTaurus'
14 modelstxt[2] = '1\tAltima\n2\tMaxima'
15 var models = modelstxt[make_id].split('\n')
16 for (var count = modelslist.options.length - 1; count > -1; count--) {
17 modelslist.options[count] = null
18 }
19 for (i = 0; i < models.length; i++) {
20 var modelvals = models[i].split('\t')
21 var option = new Option(modelvals[1], modelvals[2], false, false)
22 modelslist.options[modelslist.length] = option
23 }
24 }
25 </script>
26 </head>
27 <body>
28 <p>
29 This is a proof of concept to update a select (drop-down) list of values,
30 based on the selection of another select (drop-down) element using ajax.
31 </p>
32 <form action="" method="get" accept-charset="utf-8">
33 <select name="makes" onchange="FilterModels()" id="makes">
34 <option>--</option>
35 <option value="1">Ford</option>
36 <option value="2">Nissan</option>
37 </select>
38 <select name="models" id="models">
39 <option>Choose Make</option>
40 </select>
41 </form>
42 </body>
43</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:

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

View:

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

Template:

1{% for model in models %}{{ model.id }} {{ model.model }} {% endfor %}

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:

1class DynamicChoiceField(forms.ChoiceField):
2 def clean(self, value):
3 return value
4
5class MyForm(forms.Form):
6 make = ModelChoiceField(AutoMake.objects, widget=forms.Select(attrs={'onchange':'FilterModels();'}))
7 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):

1function FilterModels(sel_val) {
2 var modelList = $('id_model')
3 for (var count = modelList.options.length - 1; count > -1; count--) {
4 modelList.options[count] = null
5 }
6 modelList.options[0] = new Option('Loading...', '-1', false, false)
7 modelList.disabled = true
8
9 var makeList = $('id_make')
10 var make_id = makeList.options[makeList.selectedIndex].value
11 if (make_id > 0) {
12 new Ajax.Request('/feeds/models-by-make-id/' + make_id + '/', {
13 method: 'get',
14 onSuccess: function (transport) {
15 var response = transport.responseText || 'no response text'
16 var kvpairs = response.split('\n')
17 for (i = 0; i < kvpairs.length - 1; i++) {
18 m = kvpairs[i].split('\t')
19 var option = new Option(m[1], m[0], false, false)
20 modelList.options[i] = option
21 }
22 modelList.disabled = false
23 if (sel_val > 0) {
24 modelList.value = sel_val
25 }
26 },
27 onFailure: function () {
28 alert('An error occured trying to filter the model list.')
29 modelList.options[0] = new Option('Other', '0', false, false)
30 modelList.disabled = false
31 },
32 })
33 } else {
34 modelList.options[0] = new Option('Select Make', '-1', false, false)
35 modelList.disabled = true
36 }
37}

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:

1<script type="text/javascript" charset="utf-8">
2 FilterModels({{ model_id }});
3</script>

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

1def add_classified(request):
2 if request.method == 'POST':
3 form = AdForm(request.POST)
4 if request.POST.has_key('model'):
5 model_id = request.POST['model']
6 else:
7 model_id = 0
8 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?

Discuss on TwitterEdit post on GitHub

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.

Join the Newsletter



Dustin Davis