Dynamic select fields with JQuery and django

I don’t post much about my experiences with django as I’m generally slowly learning and not really coming up with any revelations. However, this week, I’ve come up against a problem that has half an answer, but not a satisfactory one, and one that no-one on the django-users mailing list seems to have been able to resolve.

Your form has nested categories that depend on each other: a top category and a sub category, rather like eBay. Rather than reloading the whole page to get a list of subcategories when a top category is selected, it would look much nicer just to populate the subcategory select field. This is a job for javascript, specifically AJAX, and to save reinventing the wheel, it’s useful to use a library, which in my case is currently JQuery.

This 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 craft custom fields for the lookup.

First, you’ll need to define a form. In this case I’m using a ModelForm with a couple of custom field definitions. This is probably slightly redundant as the field choices will be looked up by the ModelForm factory, but we are changing the definition of the prodsubcat field by disabling its widget, so I’ve also included the field choices for completeness:

class ProductForm(ModelForm):
    prodtopcat = forms.ModelChoiceField(ProductTopCategory.objects, widget=forms.Select)
    prodsubcat = forms.ModelChoiceField(ProductSubCategory.objects, widget=forms.Select(attrs={'disabled': 'true'}))

    class Meta:
        model = Product

Something I bashed my head against for a while was from Dustin’s post: setting the choices option on the widget as follows to display a message in the disabled subcategory field:

prodsubcat = forms.ModelChoiceField(ProductSubCategory.objects, widget=forms.Select(attrs={'disabled': 'true'}), choices=(('-1','Select Make'),))

which looks neat, but wipes out the choices dictionary that is loaded at render time. This is important as it’s that dictionary that the form submission process validates against. I got stuck on that for a day or so.

The javascript to make the JSON request sits in the SCRIPT section in the HEAD part of your template and is pretty simple. It also needs a current version of jQuery to be present:

      $.getJSON("/products/feeds/subcat/"+$(this).val()+"/", function(j) {
        var options = '<option value="">---------- </option>';
        for (var i = 0; i < j.length; i++) {
          options += '<option value="' + parseInt(j[i].pk) + '">' + j[i].fields['longname'] + '</option>';
        $("#id_prodsubcat option:first").attr('selected', 'selected');
        $("#id_prodsubcat").attr('disabled', false);
      $("#id_prodtopcat").attr('selected', 'selected');

This calls a django view that returns a JSON object using HTTP GET. The view looks like this:

def feeds_subcat(request, cat_id):
	from django.core import serializers
	json_subcat = serializers.serialize("json", ProductSubCategory.objects.filter(prodcat = cat_id))
	return HttpResponse(json_subcat, mimetype="application/javascript")

This returns an object filtered on the ProductTopCategory relationship and passes it through the serialiser. The jQuery function(j) formats the JSON object as a HTML SELECT field and writes it to the form using JQuery’s document.write function.

You should now have a form that among other things has an active top category select field and a disabled sub category select field. Selecting an option in the top category field should enable the sub category and populate it with a filtered set of options.

Submit the form, and if you’re using the standard django method of submission and validation, it should pass and you can continue to process the form.

The thing that I got stuck on was the validation aspect: in retrospect it’s fairly obvious that the form will validate against the object that it has in memory, but I got sidetracked for a while in other solutions such as attempting to replace the submitted form data, which can’t be done as request.POST is read only (you could make a copy but that really just adds to the codebase), and also creating custom validation for the field that did a lookup for a record that matched the selected option, thus overriding validation of the rendered field.

It’s still a little bit hacky in that it needs two lookups but on the other hand the alternative is to load a potentially big directory structure into the browser memory in order to filter the choices.


2 responses to “Dynamic select fields with JQuery and django

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s