2010-07-13 10 views
9

Quiero crear 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.Django/jQuery Cascading Select Boxes?

Podría configurar el campo de Estado para que esté vacío en la carga de página, y luego rellenarlo con algo de 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 evitar estos problemas?

+0

prueba este https://github.com/digi604/django-smart-selects/ –

Respuesta

8

Puede establecer un campo oculto para tener el valor de "estado" real, luego use jQuery para crear la lista <select> 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 <select> 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 forma parte de la forma Django. Esto le da un control total sobre él, al tiempo que permite que todos los otros campos funcionen normalmente.

EDITAR: Hay otra forma de hacerlo, pero no utiliza 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:

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

también necesitará el código JavaScript habitual para volver a cargar el selector de estados cuando se cambia el país.

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

Así que sus opciones son:

  • utilizar un campo oculto en el formulario de Django para el valor real y tienen los selectos menús creados en el cliente a través de AJAX, o
  • cosas Formulario de
  • Zanja Django e inicializar el menús usted mismo.
  • Crea un custom Django form widget, que no he hecho y por lo tanto no comentaré. No tengo idea si esto es factible, pero parece que necesitarás un par de Select s en un MultiWidget, este último no está documentado en los documentos normales, por lo que tendrás que leer la fuente.
+0

Eso es una idea ingeniosa Parece un poco sucio, pero puedo vivir con eso. – mpen

+0

No está sucio si está debidamente documentado.^_- –

+0

Simplemente no parece que deba tener un elemento oculto para evitar algunas de las peculiaridades de Django. – mpen

0

Sobre la base de la sugerencia de Mike:

// the jQuery 
$(function() { 
     var $country = $('.country'); 
     var $provInput = $('.province'); 
     var $provSelect = $('<select/>').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('<option value="'+provinces[i][0]+'">'+provinces[i][1]+'</option>'); 
         } 
         $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') 
+0

Hmm ... aún no he pasado una función a jQuery, así que no estoy seguro de qué es lo que hace. Usualmente veo '(function ($) {...}) (jQuery);' para cuando quieras que '$' sea algo diferente a 'jQuery'. Además, en el bucle 'for (i in provinces)' usaría '$ ('

+0

@Mike: '$ (función()' es short-hand para '$ (document) .ready (function()'. Estoy usando deliberadamente una clase porque puede haber varios selectores de país/provincia en una página ... No es que este script funcione con varios como está. – mpen

14

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.Código

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:

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

Código de devolución de llamada, 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') 
+0

Esto funcionó para mí. Gran enfoque –

+1

Esto me ayudó mucho. Pero _raw_value() se eliminó desde 1.9. Estoy usando lo siguiente: [link] (https: //stackoverflow.com/a/39349085/5324537). ¿Alguien sabe una mejor manera de capturar el valor no enviado? Muchas gracias. –