2010-07-13 4 views
9

Je souhaite créer un sélecteur Pays/État. D'abord vous choisissez un pays, et les Etats pour ce pays sont affichés dans la 2ème boîte de sélection. Faire cela en PHP et jQuery est assez facile, mais je trouve que les formulaires de Django sont un peu restrictifs dans ce sens.Boîtes de sélection en cascade Django/jQuery?

Je pourrais définir le champ Etat pour être vide lors du chargement de la page, puis le remplir avec un peu de jQuery, mais s'il y a des erreurs de formulaire, il ne pourra pas "se souvenir" quel état vous avez sélectionné. Je suis également assez sûr qu'il va lancer une erreur de validation parce que votre choix n'était pas l'un de ceux énumérés dans le formulaire sur le côté Python des choses.

Alors, comment contourner ces problèmes?

+0

essayer cette https://github.com/digi604/django-smart-selects/ –

Répondre

8

Vous pouvez définir un champ caché pour avoir la valeur "state" réelle, puis utiliser jQuery pour créer la liste <select> et, sur .select(), copier sa valeur dans le champ caché. Ensuite, au chargement de la page, votre code jQuery peut extraire la valeur du champ masqué et l'utiliser pour sélectionner le bon élément dans l'élément <select> après son remplissage.

Le concept clé ici est que le menu contextuel État est une fiction créée entièrement dans jQuery et ne faisant pas partie du formulaire Django. Cela vous donne un contrôle total sur elle, tout en laissant tous les autres champs fonctionner normalement.

EDIT: Il existe une autre façon de le faire, mais il n'utilise pas les classes de formulaire de Django.

De l'avis:

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) 

Ensuite, dans le modèle:

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

Vous aurez également besoin de JavaScript habituel pour recharger le sélecteur d'états lorsque le pays est changé.

Je n'ai pas testé cela, donc il y a probablement quelques trous dedans, mais ça devrait faire passer l'idée.

Ainsi, vos choix sont:

  • Utilisez un champ caché sous la forme Django pour la valeur réelle et ont les menus de sélection créés côté client via AJAX, ou
  • Ditch stuff formulaire de Django et initialiser le menus vous-même.
  • Créer un custom Django form widget, que je n'ai pas fait et donc ne commenterai pas. Je n'ai aucune idée si cela est faisable, mais il semble que vous aurez besoin d'un couple Select dans un MultiWidget, ce dernier étant non documenté dans les documents réguliers, de sorte que vous aurez à lire la source.
+0

C'est une idée intelligente. Ça semble un peu sale, mais je peux vivre avec. – mpen

+0

Ce n'est pas sale s'il est correctement documenté.^_- –

+0

Il ne me semble pas que je devrais avoir un élément caché pour contourner certaines des bizarreries de Django. – mpen

0

Basé sur la suggestion 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 ... je n'ai pas encore passé de fonction à jQuery, donc je ne suis pas sûr de ce que ça fait. Habituellement, je vois '(function ($) {...}) (jQuery);' quand vous voulez que '$' soit autre chose que 'jQuery'. De plus, dans la boucle 'for (i in provinces)' j'utiliserais '$ ('

+0

@Mike: '$ (function()' est raccourci pour '$ (document) .ready (function()'. J'utilise délibérément une classe car il peut y avoir plusieurs sélecteurs de pays/province sur une page. – mpen

14

Voici ma solution. Il utilise la méthode formulaire non documentée _raw_value() pour accéder aux données de la requête. Cela fonctionne pour les formulaires, qui ont aussi un préfixe.

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 

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

Code Callback, qui retourne 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

Cela a fonctionné pour moi .. Bonne approche –

+1

Cela m'a beaucoup aidé, mais _raw_value() est supprimé depuis la version 1.9, j'utilise ce qui suit: [link] (https: //stackoverflow.com/a/39349085/5324537) Est-ce que quelqu'un sait un meilleur moyen d'attraper la valeur non soumise? Merci beaucoup. –

Questions connexes