77

I have 3 fields in my form. I have a submit button and a button to "Add additional Field". I understand I can add fields using __init__ method in the form class.

I am new to Python and Django and am stuck with a beginner question; my question is:

When I click the "Add additional field" button, what is the process to add the additional field?

Does the form have to be rendered again?

How and when do I call __init__ or do I even have to call it?

How do I pass arguments to __init__?

John R Perry
  • 3,916
  • 2
  • 38
  • 62
Afshin
  • 779
  • 1
  • 6
  • 3
  • 2
    You might want to separate out your questions. Your first question has been asked before. http://stackoverflow.com/questions/2599893/django-form-creation-on-init http://stackoverflow.com/search?q=django+dynamic+form – Scott Gottreu May 26 '11 at 16:46
  • 1
    You can't dynamically add fields to a form. You can add other forms to a formset. Is that what you want? (Mind you, the "form" can be just one field) – Chris Pratt May 26 '11 at 17:00

5 Answers5

75

Your form would have to be constructed based on some variables passed to it from your POST (or blindly check for attributes). The form itself is constructed every time the view is reloaded, errors or not, so the HTML needs to contain information about how many fields there are to construct the correct amount of fields for validation.

I'd look at this problem the way FormSets work: there is a hidden field that contains the number of forms active, and each form name is prepended with the form index.

In fact, you could make a one field FormSet

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

If you don't want to use a FormSet you can always create this behavior yourself.

Here's one made from scratch - it should give you some ideas. It also answers your questions about passing arguments to __init__ - you just pass arguments to an objects constructor: MyForm('arg1', 'arg2', kwarg1='keyword arg')

Forms

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

View

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>
let 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 ++;

    let 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>
John R Perry
  • 3,916
  • 2
  • 38
  • 62
Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245
  • 3
    I would also add a reasonable default maximum to the number of additional fields a user can request. Otherwise, someone could submit an arbitrarily large number which leaves your application to deal with. – Scott Woodall Oct 26 '12 at 15:56
  • If `extra_fields` is taken from post you need to cast it to `int` for a `range` function to not show error. – Ogrim Jan 13 '14 at 12:22
  • This code has mistakes. form_count = $("[name=extra_field_count"); doesn't make any sense as later you do this: form_count ++; Secondly, if you don't create any extra input elements then you get an error. – user1919 Jun 27 '16 at 11:47
  • @user1919 the purpose of incrementing the form count is so that the server knows how many fields to start with, and thus what the next form field number is. I do see that it would result in duplicate fields and form_count++ should have been placed first. – Yuji 'Tomita' Tomita Jun 27 '16 at 15:54
  • @Yuji 'Tomita' Tomita thanks for your answer. But still I don't understand what is the purpose of this line: form_count = $("[name=extra_field_count"); First there is a ']' missing. And secondly why we can not just set this to 0? $("[name=extra_field_count"); is not supposed to return an integer. How can this be incremented later? – user1919 Jun 27 '16 at 16:58
  • Moreover, I haven't managed to fetch the data from the generated fields and pass it in my view. It seems like that only the last value is passed in the: extra_field_count field. – user1919 Jun 27 '16 at 17:00
  • 1
    @user1919 Good find. This is mostly an example of a concept of how you could do this, where you fill in the blanks. Updated. The purpose is to receive the current number of fields existing. For example, let's say you POST the form, and there are errors because some of the form elements didn't validate. The returned HTML response needs to contain the field count, so that when you add another input via JS, the correct input name is appended. I.e. Render form. Add 3 fields. POST. Add another field. 4th field needs the proper index vs 0. As for your issue passing data to the view, let me look. – Yuji 'Tomita' Tomita Jun 27 '16 at 23:06
  • Thanks. A couple of more remarks. There is a missing parenthesis in: MyForm(request.POST, extra=request.POST.get('extra_field_count'). Also I think its necessary to declare the type of button - for the 'add another' cause as it is inside the form it performs a submit when clicked. Last your latest fix: Number($("[name=extra_field_count]")); doesn't actually return a number. Still NaN! – user1919 Jun 28 '16 at 08:44
  • I actually think that you need to initialize the form_count as: form_count = $('input[name*="extra_field_*"]').length; So you actually counting the occurences of elements with name="extra_field_*") – user1919 Jun 28 '16 at 08:56
  • @user1919 should be `.val()` to get field value. I haven't gotten a chance to paste this into an environment quite yet. – Yuji 'Tomita' Tomita Jun 28 '16 at 13:51
  • This adds them one after the other. Is there no way for new "fields" to be generated on the page? E.g. [Name] : [Enter-Name] [Siblings] : [Enter-Sibling-Name] <-- Here there is a button, "add more siblings" (and if I click it twice) [Siblings] : [Enter-Sibling-Name] [Siblings] : [Enter-Sibling-Name] – DUDANF Jun 11 '19 at 16:00
  • 1
    This is a **great solution** if you want to **add fields using JavaScript on client-side** (without sending request to server). – Jakub Holan May 22 '23 at 12:46
15

I've had a case when I had to dynamically create forms with dynamic fields. That I did with this trick:

from django import forms

...

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

Refer to this link for more info: Dynamic Forms

But in addition to that I had to inject fields as well i.e. dynamically add fields to a form class once it was created.

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)

And that worked.

Al Conrad
  • 1,528
  • 18
  • 12
  • 1
    This actually works! I needed to create a form class dynamically, to pass it to a formset, and it works like a charm! Thanks! – Andriy Stolyar Feb 21 '18 at 13:54
  • Works well! thanks @Al Conrad. Also, zerowithdot, in his blog at https://zerowithdot.com/django-dynamic-forms/ illustrates a variant of this technique using a derived Form class with fields directly injected as it's class attributes, as follows: DynamicIngredientsForm = type( 'DynamicIngredientsForm', (IngredientsForm,), new_fields_dict) – Snidhi Sofpro Dec 20 '19 at 11:55
4

A way without javascript and the field type is not describe in the 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

TEMPLATE

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

This answer is based on the of @Yuji'Tomita'Tomita with several improvements and changes.

Although @Yuji'Tomita'Tomita answer is great and illustrates nicely and simple the direction to follow in order to build the "add extra field in a django form" functionality, I found that there are some issues with some parts of the code.

Here I provide my working code based on the initial proposal of @Yuji'Tomita'Tomita:

Views (in the view.py file)

Nothing really changes in the views:

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 (in the form.py file)

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

Template 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);

})
user1919
  • 3,818
  • 17
  • 62
  • 97
3

Yuji 'Tomita' Tomita's solution is the acutally best you will find, but assuming you have a multiple step form and you use the django-formtools app you will have some issues you will have to take care of. Thank you Yuji 'Tomita' Tomita, you helped me a lot :)

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)

For a multiple-step form, you will not need the extra field, in this code we use othercolumsvalue field in the first-step.

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],
        })

By overriding the get_form() and get_context_data() functions you can override the form befor it gets rendered. You will not need JavaScript anymore either for your template-file:

            {% 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 %}

The fields from step2 the were made dynamically were also reconized from the formtools because of the same name. But to get there you will have to work around the for-each template loops as you can see:

from the get_context_data()-function

        othercolums_count = []
        for i in range(0, data_step1['othercolumsvalue']):
            othercolums_count.append(str(i))
SamPhoenix
  • 99
  • 8