Django/jQuery Cascading Select Boxes?

20,477

Solution 1

You could set a hidden field to have the real "state" value, then use jQuery to create the <select> list and, on .select(), copy its value to the hidden field. Then, on page load, your jQuery code can fetch the hidden field's value and use it to select the right item in the <select> element after it's populated.

The key concept here is that the State popup menu is a fiction created entirely in jQuery and not part of the Django form. This gives you full control over it, while letting all the other fields work normally.

EDIT: There's another way to do it, but it doesn't use Django's form classes.

In the view:

context = {'state': None, 'countries': Country.objects.all().order_by('name')}
if 'country' in request.POST:
    context['country'] = request.POST['country']
    context['states'] = State.objects.filter(
        country=context['country']).order_by('name')
    if 'state' in request.POST:
        context['state'] = request.POST['state']
else:
    context['states'] = []
    context['country'] = None
# ...Set the rest of the Context here...
return render_to_response("addressform.html", context)

Then in the template:

<select name="country" id="select_country">
    {% for c in countries %}
    <option value="{{ c.val }}"{% ifequal c.val country %} selected="selected"{% endifequal %}>{{ c.name }}</option>
    {% endfor %}
</select>

<select name="state" id="select_state">
    {% for s in states %}
    <option value="{{ s.val }}"{% ifequal s.val state %} selected="selected"{% endifequal %}>{{ s.name }}</option>
    {% endfor %}
</select>

You'll also need the usual JavaScript for reloading the states selector when the country is changed.

I haven't tested this, so there are probably a couple holes in it, but it should get the idea across.

So your choices are:

  • Use a hidden field in the Django form for the real value and have the select menus created client-side via AJAX, or
  • Ditch Django's Form stuff and initialize the menus yourself.
  • Create a custom Django form widget, which I haven't done and thus will not comment on. I have no idea if this is doable, but it looks like you'll need a couple Selects in a MultiWidget, the latter being undocumented in the regular docs, so you'll have to read the source.

Solution 2

Here is my solution. It uses the undocumented Form method _raw_value() to peek into the data of the request. This works for forms, which have a prefix, too.

class CascadeForm(forms.Form):
    parent=forms.ModelChoiceField(Parent.objects.all())
    child=forms.ModelChoiceField(Child.objects.none())

    def __init__(self, *args, **kwargs):
        forms.Form.__init__(self, *args, **kwargs)
        parents=Parent.objects.all()
        if len(parents)==1:
            self.fields['parent'].initial=parents[0].pk

        parent_id=self.fields['parent'].initial or self.initial.get('parent') \
                  or self._raw_value('parent')
        if parent_id:
            # parent is known. Now I can display the matching children.
            children=Child.objects.filter(parent__id=parent_id)
            self.fields['children'].queryset=children
            if len(children)==1:
                self.fields['children'].initial=children[0].pk

jquery Code:

function json_to_select(url, select_selector) {
/*
 Fill a select input field with data from a getJSON call
 Inspired by: http://stackoverflow.com/questions/1388302/create-option-on-the-fly-with-jquery
*/
    $.getJSON(url, function(data) {
    var opt=$(select_selector);
    var old_val=opt.val();
        opt.html('');
        $.each(data, function () {
            opt.append($('<option/>').val(this.id).text(this.value));
        });
        opt.val(old_val);
        opt.change();
    })
}


   $(function(){
     $('#id_parent').change(function(){
       json_to_select('PATH_TO/parent-to-children/?parent=' + $(this).val(), '#id_child');
     })  
    });

Callback Code, which returns JSON:

def parent_to_children(request):
    parent=request.GET.get('parent')
    ret=[]
    if parent:
        for children in Child.objects.filter(parent__id=parent):
            ret.append(dict(id=child.id, value=unicode(child)))
    if len(ret)!=1:
        ret.insert(0, dict(id='', value='---'))
    return django.http.HttpResponse(simplejson.dumps(ret), 
              content_type='application/json')
Share:
20,477

Related videos on Youtube

mpen
Author by

mpen

Updated on December 21, 2020

Comments

  • mpen
    mpen over 3 years

    I want to build a Country/State selector. First you choose a country, and the States for that country are displayed in the 2nd select box. Doing that in PHP and jQuery is fairly easy, but I find Django forms to be a bit restrictive in that sense.

    I could set the State field to be empty on page load, and then populate it with some jQuery, but then if there are form errors it won't be able to "remember" what State you had selected. I'm also pretty sure that it will throw a validation error because your choice wasn't one of the ones listed in the form on the Python side of things.

    So how do I get around these problems?

  • mpen
    mpen almost 14 years
    That's a clever idea. Seems a tiny bit dirty, but I can live with it.
  • Mike DeSimone
    Mike DeSimone almost 14 years
    It's not dirty if it's properly documented. ^_-
  • Mike DeSimone
    Mike DeSimone almost 14 years
    Hmm... haven't passed a function to jQuery yet, so I'm not sure what that does. Usually I see (function($){...})(jQuery); for when you want $ to be something other than jQuery. Also, in the for (i in provinces) loop I would use $('<option/>').val(provinces[i][0]).text(provinces[i][1]).ap‌​pendTo($provSelect);‌​. I'm guessing my way is slower, but it's easier to read when you put the clauses on one line each. Also, IIRC, you can use $('#id_country') instead of $('.country'); same for the province widget, which could let you dump the class stuff in the form.
  • mpen
    mpen almost 14 years
    @Mike: $(function() is short-hand for $(document).ready(function(). I'm deliberately using a class because there might be multiple country/province selectors on one page... not that this script will work with multiple ones as is.
  • mpen
    mpen almost 14 years
    Just doesn't seem like I should need to have a hidden element to get around some of Django's quirks.
  • Mike DeSimone
    Mike DeSimone almost 14 years
    Django doesn't really do mutating forms. Whenever I have one, I just do the form myself.
  • mpen
    mpen almost 14 years
    Not too fond of your edit either, although it is workable :p I'll stick with the hidden input/AJAX sol'n for now.
  • Maxime VAST
    Maxime VAST over 6 years
    This helped me a lot. But _raw_value() is removed since 1.9. I'm using the following: link. Does anybody know a better way to catch the unsubmitted value ? Thanks a lot.
  • AmagicalFishy
    AmagicalFishy almost 6 years
    I know this comment is a bit late, and there might be a better way to do things—but, according to the documentation, one can add a Media asset to forms (which basically seems to be just included Javascript).

Related