Dynamic Filtered Drop-Down Choice Fields With Django

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 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">
<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:

{% 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 get invalid choice errors:

class DynamicChoiceField(forms.ChoiceField):     def clean(self, value):         return valueclass 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({{ model_id }});
</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?

Related posts:

  1. Django: Login Form on Every Page Up to the point, when it has come to Django...
  2. Django: Filter Model on Date Range I’m sure this is in the documentation *somewhere* but it...
  3. Django & Djson… er, JSON I must admin this is my first attempt at even...
  4. Django: ProgrammingError: Can’t Adapt I find I get an error like this far too...

Related posts brought to you by Yet Another Related Posts Plugin.

October 3, 2008  Tags: , ,   Posted in: Programming & Internet

7 Responses

  1. Kevin from Great Wall of China - October 15, 2008

    I like the the Django network too! I am glad someone shares the same love!

  2. Nastya from Low Calling Rates - November 4, 2008

    I always wonder, how these long and curly codes transform into functional websites :) Programmers make a great deal of work.

  3. Ed - December 15, 2008

    Looks like exactly what I’m looking for but I’m not sure how to use all this code… Any way of downloading an example with all the relevant js files for example? Thanks

  4. owen - January 16, 2009

    Thanks! Just what I needed for my Django app. I haven’t used AJAX before, so it took me a while to figure out that I needed the prototype.js library, but after that it worked perfectly.

  5. racz - April 28, 2009

    any idea how to use this when working with app engine … i dont know what is with the URL and the VIEW things

  6. Old enough to know better » Dynamic select fields with JQuery and django - May 19, 2009

    [...] blog post and this blog post set me off in the right direction by showing how to use JQuery’s getJSON function and how to [...]

  7. Cheap call - July 6, 2009

    Thank you! Just what I needed for my Django app.

Comments are closed for this entry.