web-dev-qa-db-fra.com

ajouter dynamiquement un champ à un formulaire

J'ai 3 champs dans mon formulaire . J'ai un bouton d'envoi et un bouton pour "Ajouter un champ supplémentaire" . Je comprends que je peux ajouter des champs en utilisant la méthode __init__ dans la classe de formulaire.

Je suis nouveau sur Python et Django et je suis coincé avec une question de débutant: Ma question est la suivante:

Lorsque je clique sur le bouton "Ajouter un champ supplémentaire", quel est le processus pour ajouter le champ supplémentaire? 

Le formulaire doit-il être rendu à nouveau?

Comment et quand appeler __init__ ou dois-je même l'appeler?

Comment passer des arguments à __init__?

51
Afshin

Votre formulaire devrait être construit en fonction de certaines variables qui lui ont été transmises par votre POST (ou à l’aveuglette pour vérifier les attributs). Le formulaire lui-même est construit chaque fois que la vue est rechargée, qu’il s’agisse d’une erreur ou non, le code HTML doit donc contenir des informations sur le nombre de champs disponibles pour construire le nombre correct de champs à valider.

Je regarderais ce problème de la façon dont fonctionne FormSets: il y a un champ caché qui contient le nombre de formulaires actifs, et chaque nom de formulaire est précédé de l'index de formulaire. 

En fait, vous pouvez créer un champ FormSet

https://docs.djangoproject.com/en/dev/topics/forms/formsets/#formsets

Si vous ne voulez pas utiliser une FormSet, vous pouvez toujours créer ce comportement vous-même.

En voici un fabriqué à partir de rien - il devrait vous donner quelques idées. Il répond également à vos questions sur le passage d'arguments à __init__ - vous ne passez que des arguments à un constructeur d'objets: MyForm('arg1', 'arg2', kwarg1='keyword arg')

Formes

class MyForm(forms.Form):
    original_field = forms.CharField()
    extra_field_count = forms.CharField(widget=forms.HiddenInput())

    def __init__(self, *args, **kwargs):
        extra_fields = kwargs.pop('extra', 0)

        super(MyForm, self).__init__(*args, **kwargs)
        self.fields['extra_field_count'].initial = extra_fields

        for index in range(int(extra_fields)):
            # generate extra fields in the number specified via extra_fields
            self.fields['extra_field_{index}'.format(index=index)] = \
                forms.CharField()

Vue

def myview(request):
    if request.method == 'POST':
        form = MyForm(request.POST, extra=request.POST.get('extra_field_count'))
        if form.is_valid():
            print "valid!"
    else:
        form = MyForm()
    return render(request, "template", { 'form': form })

HTML

<form>
    <div id="forms">
        {{ form.as_p }}
    </div>
    <button id="add-another">add another</button>
    <input type="submit" />
</form>

JS

<script>
form_count = Number($("[name=extra_field_count]").val());
// get extra form count so we know what index to use for the next item.

$("#add-another").click(function() {
    form_count ++;

    element = $('<input type="text"/>');
    element.attr('name', 'extra_field_' + form_count);
    $("#forms").append(element);
    // build element and append it to our forms container

    $("[name=extra_field_count]").val(form_count);
    // increment form count so our view knows to populate 
    // that many fields for validation
})
</script>
57

J'ai eu un cas où je devais créer dynamiquement des formulaires avec des champs dynamiques. C'est ce que j'ai fait avec cette astuce:

from Django import forms

...

dyn_form = type('DynForm',  # form name is irrelevant
                (forms.BaseForm,),
                {'base_fields': fields})

Reportez-vous à ce lien pour plus d’informations: Dynamic Forms

Mais en plus de cela, je devais aussi injecter des champs, c’est-à-dire ajouter dynamiquement des champs à une classe de formulaire une fois celle-ci créée.

dyn_form.base_fields['field1'] = forms.IntegerField(widget=forms.HiddenInput(), initial=field1_val)
dyn_form.base_fields['field2'] = forms.CharField(widget=forms.HiddenInput(), initial=field2_val)

Et cela a fonctionné.

9
Al Conrad

Cette réponse est basée sur le @ Yuji'Tomita'Tomita avec plusieurs améliorations et changements. 

Bien que la réponse de @ Yuji'Tomita'Tomita soit excellente et illustre de manière simple et précise la direction à suivre pour créer la fonctionnalité "Ajouter un champ supplémentaire dans un formulaire Django", j'ai constaté quelques problèmes avec certaines parties du code.

Ici, je fournis mon code de travail basé sur la proposition initiale de @ Yuji'Tomita'Tomita:

Vues (dans le fichier view.py)

Rien ne change vraiment dans les vues:

def myview(request):

  if request.method == 'POST':

    form = MyForm(request.POST, extra=request.POST.get('total_input_fields'))

      if form.is_valid():
        print "valid!"
      else:
        form = MyForm()
return render(request, "template", { 'form': form })

Form (dans le fichier form.py)

class MyForm(forms.Form):

    empty_layer_name = forms.CharField(max_length=255, required=True, label="Name of new Layer")

    total_input_fields = forms.CharField(widget=forms.HiddenInput())


    def __init__(self, *args, **kwargs):

      extra_fields = kwargs.pop('extra', 0)

      # check if extra_fields exist. If they don't exist assign 0 to them
      if not extra_fields:
         extra_fields = 0

      super(MyForm, self).__init__(*args, **kwargs)
      self.fields['total_input_fields'].initial = extra_fields

      for index in range(int(extra_fields)):
        # generate extra fields in the number specified via extra_fields
        self.fields['extra_field_{index}'.format(index=index)] = forms.CharField()

Modèle HTML

<form id="empty-layer-uploader" method="post" enctype="multipart/form-data" action="{% url "layer_create" %}">
        <div id="form_empty_layer">
          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
            {{ form.errors }}
            {{ form.non_field_errors }}
            {% if errormsgs %}
              {% for value in errormsgs %}
                </p>  {{ value }} </p>
              {% endfor %}
            {% endif %}
            {% for error in form_empty_layer.non_field_errors %}
              {{ error }} </br>
            {% endfor %}
            </br>
            {% for field in form_empty_layer.visible_fields %}
              {{ field }} </br>
            {% endfor %}
        </div>
        </br>
        <button type="button" id="add-another">add another</button> </br> </br>
        <button type="submit" id="empty-layer-button" name="emptylayerbtn">Upload</button>
        </br></br>
        // used in order to save the number of added fields (this number will pass to forms.py through the view)
        <input type="text" name="total_input_fields"/>
</form>

Template Jquery

// check how many times elements with this name attribute exist: extra_field_*
form_count = $('input[name*="extra_field_*"]').length;

// when the button 'add another' is clicked then create a new input element
$(document.body).on("click", "#add-another",function(e) {
  new_attribute = $('<input type="text"/>');
  // add a name attribute with a corresponding number (form_count)
  new_attribute.attr('name', 'extra_field_' + form_count);
  // append the new element in your html
  $("#form_empty_layer").append(new_attribute);
  // increment the form_count variable
  form_count ++;
  // save the form_count to another input element (you can set this to invisible. This is what you will pass to the form in order to create the Django form fields
  $("[name=total_input_fields]").val(form_count);

})
4
user1919

Une manière sans javascript et le type de champ n'est pas décrit dans le js:

PYTHON

 def __init__(self, *args, **kwargs):
        super(Form, self).__init__(*args, **kwargs)

        ##ajouts des champs pour chaque chien
        for index in range(int(nb_dogs)):
            self.fields.update({
                'dog_%s_name' % index: forms.CharField(label=_('Name'), required=False, max_length=512),
            })

 def fields_dogs(self):
        fields = []
        for index in range(int(nb_dogs)):
            fields.append({
                'name': self['dog_%s_name' % index],
            })
        return fields

MOD&EGRAVE;LE

{% for field_dog in f.fields_dogs %}
        <thead>
            <tr>
                <th style="background-color: #fff; border-width: 0px;"></th>
                <th>{% trans 'Dog' %} #{{forloop.counter}}</th>
                <th>{% trans 'Name' %}</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td style="background-color: #fff; border-width: 0px;"></td>
                <td style="background-color: #fff; border-width: 0px;"></td>
                <td>{{field_dog.name.errors}}{{field_dog.name}}</td>
            </tr>
            <tr>
                <td style="padding: 10px; border-width: 0px;"></td>
            </tr>
        </tbody>
{% endfor %}
4

La solution de Yuji 'Tomita' Tomita est ce que vous trouverez de mieux, mais en supposant que vous ayez un formulaire en plusieurs étapes et que vous utilisiez l'application Django-formtools, vous aurez quelques problèmes à résoudre. Merci Yuji 'Tomita' Tomita, vous m'avez beaucoup aidé :)

forms.py

class LicmodelForm1(forms.Form):
     othercolumsvalue = forms.IntegerField(min_value=0, initial=0)
class LicmodelForm2(forms.Form):
    def __init__(self, *args, **kwargs):
    extra_fields = kwargs.pop('extra', 0)

    super(LicmodelForm2, self).__init__(*args, **kwargs)

    for index in range(int(extra_fields)):
        # generate extra fields in the number specified via extra_fields
        self.fields['othercolums_{index}'.format(index=index)] = \
            forms.CharField()
        self.fields['othercolums_{index}_nullable'.format(index=index)] = \
            forms.BooleanField(required=False)

Pour un formulaire en plusieurs étapes, vous n’avez pas besoin du champ supplémentaire. Dans ce code, nous utilisons le champ othercolumsvalue dans le premier pas.

views.py

class MyFormTool(SessionWizardView):
def get_template_names(self):
    return [TEMPLATES[self.steps.current]]

def get_context_data(self, form, **kwargs):
    context = super(MyFormTool, self).get_context_data(form=form, **kwargs)
    data_step1 = self.get_cleaned_data_for_step('step1')
    if self.steps.current == 'step2':

        #prepare tableparts for the needLists
        needList_counter = 0
        for i in self.wellKnownColums:
            if data_step1[i] is True:
                needList_counter = needList_counter + 1
                pass

        #prepare tableparts for othercolums
        othercolums_count = []
        for i in range(0, data_step1['othercolumsvalue']):
            othercolums_count.append(str(i))

        context.update({'step1': data_step1})
        context.update({'othercolums_count': othercolums_count})

    return context

def get_form(self, step=None, data=None, files=None):
    form = super(MyFormTool, self).get_form(step, data, files)

    if step is None:
        step = self.steps.current

    if step == 'step2':
        data = self.get_cleaned_data_for_step('step1')
        if data['othercolumsvalue'] is not 0:
            form = LicmodelForm2(self.request.POST,
                                 extra=data['othercolumsvalue'])
    return form

def done(self, form_list, **kwargs):
    print('done')
    return render(self.request, 'formtools_done.html', {
        'form_data' : [form.cleaned_data for form in form_list],
        })

En remplaçant les fonctions get_form () et get_context_data () , vous pouvez remplacer le formulaire avant son affichage. Vous n'aurez plus besoin de JavaScript pour votre fichier modèle:

            {% if step1.othercolumsvalue > 0 %}
            <tr>
                <th>Checkbox</th>
                <th>Columname</th>
            </tr>
            {% for i in othercolums_count %}
                <tr>
                    <td><center><input type="checkbox" name="othercolums_{{ i }}_nullable" id="id_othercolums_{{ i }}_nullable" /></center></td>
                    <td><center><input type="text" name="othercolums_{{ i }}" required id="id_othercolums_{{ i }}" /></center></td>
                </tr>
            {% endfor %}
        {% endif %}

Les champs de l'étape 2 ont été créés dynamiquement et ont également été reconstitués à partir des outils de formulaire en raison du même nom. Mais pour y arriver, vous devrez contourner les boucles de modèle pour chaque modèle comme vous pouvez le voir:

à partir de la fonction get_context_data ()

        othercolums_count = []
        for i in range(0, data_step1['othercolumsvalue']):
            othercolums_count.append(str(i))
0
user2823842