Django / jQuery Cascading Select Boxes?

Quiero construir un selector de país / estado. Primero elige un país, y los Estados para ese país se muestran en el segundo cuadro de selección. Hacer eso en PHP y jQuery es bastante fácil, pero creo que las formas de Django son un poco restrictivas en ese sentido.

Podría configurar el campo de Estado para que esté vacío en la carga de la página, y luego rellenarlo con jQuery, pero luego, si hay errores de formulario, no podrá “recordar” qué Estado ha seleccionado. También estoy bastante seguro de que arrojará un error de validación porque su elección no fue una de las enumeradas en el formulario en el lado de Python.

Entonces, ¿cómo puedo solucionar estos problemas?

Puede establecer un campo oculto para tener el valor de “estado” real, luego use jQuery para crear la lista y, en .select() , copie su valor en el campo oculto. Luego, al cargar la página, su código jQuery puede recuperar el valor del campo oculto y usarlo para seleccionar el elemento correcto en el elemento después de que se haya rellenado.

El concepto clave aquí es que el menú emergente de estado es una ficción creada completamente en jQuery y no parte de la forma de Django. Esto le da un control total sobre él, al tiempo que permite que todos los otros campos funcionen normalmente.

EDITAR: Hay otra manera de hacerlo, pero no usa las clases de formulario de Django.

En la vista:

 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) 

Luego en la plantilla:

   

También necesitará el JavaScript habitual para volver a cargar el selector de estados cuando se cambie el país.

No lo he probado, por lo que probablemente haya un par de agujeros en él, pero debería transmitir la idea.

Entonces tus elecciones son:

  • Utilice un campo oculto en el formulario de Django para el valor real y tenga los menús de selección creados en el lado del cliente a través de AJAX, o
  • Ditch Django’s Form e inicializa los menús tú mismo.
  • Cree un widget de formulario de Django personalizado , que no he hecho y por lo tanto no comentaré. No tengo idea de si esto es factible, pero parece que necesitarás un par de Select en un MultiWidget , este último no está documentado en los documentos regulares, por lo que tendrás que leer la fuente.

Aquí está mi solución. Utiliza el método de formulario no documentado _raw_value () para buscar en los datos de la solicitud. Esto funciona para formularios, que también tienen un prefijo.

 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 

código jquery:

 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($('').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'); }) }); 

Código de callback, que devuelve 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') 

Basado en la sugerencia de Mike:

 // the jQuery $(function () { var $country = $('.country'); var $provInput = $('.province'); var $provSelect = $('').insertBefore($provInput).change(function() { $provInput.val($provSelect.val()); }); $country.change(function() { $provSelect.empty().addClass('loading'); $.getJSON('/get-provinces.json', {'country':$(this).val()}, function(provinces) { $provSelect.removeClass('loading'); for(i in provinces) { $provSelect.append(''); } $provSelect.val($provInput.val()).trigger('change'); }); }).trigger('change'); }); # the form country = CharField(initial='CA', widget=Select(choices=COUNTRIES, attrs={'class':'country'})) province = CharField(initial='BC', widget=HiddenInput(attrs={'class':'province'})) # the view def get_provinces(request): from django.utils import simplejson data = { 'CA': CA_PROVINCES, 'US': US_STATES }.get(request.GET.get('country', None), None) return HttpResponse(simplejson.dumps(data), mimetype='application/json')