dynamically add field to a form

PythonDjangoDjango Forms

Python Problem Overview


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

Python Solutions


Solution 1 - Python

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>

Solution 2 - Python

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.

Solution 3 - Python

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

Solution 4 - Python

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

})

Solution 5 - Python

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAfshin View Question on Stackoverflow
Solution 1 - PythonYuji 'Tomita' TomitaView Answer on Stackoverflow
Solution 2 - PythonAl ConradView Answer on Stackoverflow
Solution 3 - PythonJonathan Le CornichoneView Answer on Stackoverflow
Solution 4 - Pythonuser1919View Answer on Stackoverflow
Solution 5 - PythonSamPhoenixView Answer on Stackoverflow