Making a Django form class with a dynamic number of fields

PythonDjangoForms

Python Problem Overview


I'm working on something like an online store. I'm making a form in which the customer buys an item, and she can choose how many of these item she would like to buy. But, on every item that she buys she needs to choose what its color would be. So there's a non-constant number of fields: If the customer buys 3 items, she should get 3 <select> boxes for choosing a color, if she buys 7 items, she should get 7 such <select> boxes.

I'll make the HTML form fields appear and disappear using JavaScript. But how do I deal with this on my Django form class? I see that form fields are class attributes, so I don't know how to deal with the fact that some form instance should have 3 color fields and some 7.

Any clue?

Python Solutions


Solution 1 - Python

Jacob Kaplan-Moss has an extensive writeup on dynamic form fields: http://jacobian.org/writing/dynamic-form-generation/

Essentially, you add more items to the form's self.fields dictionary during instantiation.

Solution 2 - Python

Here's another option: how about a formset? Since your fields are all the same, that's precisely what formsets are used for.

The django admin uses FormSets + a bit of javascript to add arbitrary length inlines.

class ColorForm(forms.Form):
    color = forms.ChoiceField(choices=(('blue', 'Blue'), ('red', 'Red')))

ColorFormSet = formset_factory(ColorForm, extra=0) 
# we'll dynamically create the elements, no need for any forms

def myview(request):
    if request.method == "POST":
        formset = ColorFormSet(request.POST)
        for form in formset.forms:
            print "You've picked {0}".format(form.cleaned_data['color'])
    else:
        formset = ColorFormSet()
    return render(request, 'template', {'formset': formset}))

    
JavaScript
    <script>
        $(function() {
            // this is on click event just to demo.
            // You would probably run this at page load or quantity change.
            $("#generate_forms").click(function() {
                // update total form count
                quantity = $("[name=quantity]").val();
                $("[name=form-TOTAL_FORMS]").val(quantity);  

                // copy the template and replace prefixes with the correct index
                for (i=0;i<quantity;i++) {
                    // Note: Must use global replace here
                    html = $("#form_template").clone().html().replace(/__prefix_/g', i);
                    $("#forms").append(html);
                };
            })
        })
    </script>
Template
    <form method="post">
        {{ formset.management_form }}
        <div style="display:none;" id="form_template">
            {{ formset.empty_form.as_p }}
        </div><!-- stores empty form for javascript -->
        <div id="forms"></div><!-- where the generated forms go -->
    </form>
    <input type="text" name="quantity" value="6" />
    <input type="submit" id="generate_forms" value="Generate Forms" />

Solution 3 - Python

you can do it like

def __init__(self, n,  *args, **kwargs):
  super(your_form, self).__init__(*args, **kwargs)
  for i in range(0, n):
    self.fields["field_name %d" % i] = forms.CharField()

and when you create form instance, you just do

forms = your_form(n)

it's just the basic idea, you can change the code to whatever your want. :D

Solution 4 - Python

The way I would do it is the following:

  1. Create an "empty" class that inherits from froms.Form, like this:

     class ItemsForm(forms.Form):
         pass
    
  2. Construct a dictionary of forms objects being the actual forms, whose composition would be dependent on the context (e.g. you can import them from an external module). For example:

     new_fields = {
         'milk'  : forms.IntegerField(),
         'butter': forms.IntegerField(),
         'honey' : forms.IntegerField(),
         'eggs'  : forms.IntegerField()}
    
  3. In views, you can use python native "type" function to dynamically generate a Form class with variable number of fields.

     DynamicItemsForm = type('DynamicItemsForm', (ItemsForm,), new_fields)
    
  4. Pass the content to the form and render it in the template:

     Form = DynamicItemsForm(content)
     context['my_form'] = Form
     return render(request, "demo/dynamic.html", context)
    

The "content" is a dictionary of field values (e.g. even request.POST would do). You can see my whole example explained here.

Solution 5 - Python

Another approach: Rather than breaking the normal field initialization flow, we can override fields with a mixin, return an OrderedDict of dynamic fields in generate_dynamic_fields which will be added whenever its set.

from collections import OrderedDict

class DynamicFormMixin:
    _fields: OrderedDict = None

    @property
    def fields(self):
      return self._fields

    @fields.setter
    def fields(self, value):
        self._fields = value
        self._fields.update(self.generate_dynamic_fields())

    def generate_dynamic_fields(self):
        return OrderedDict()

A simple example:

class ExampleForm(DynamicFormMixin, forms.Form):
    instance = None

    def __init__(self, instance = None, data=None, files=None, auto_id='id_%s', prefix=None, initial=None,
                 error_class=ErrorList, label_suffix=None, empty_permitted=False, field_order=None,
                 use_required_attribute=None, renderer=None):
        self.instance = instance
        super().__init__(data, files, auto_id, prefix, initial, error_class, label_suffix, empty_permitted, field_order,
                         use_required_attribute, renderer)

    def generate_dynamic_fields(self):
        dynamic_fields = OrderedDict()
        instance = self.instance
        dynamic_fields["dynamic_choices"] = forms.ChoiceField(label=_("Number of choices"),
                                                              choices=[(str(x), str(x)) for x in range(1, instance.number_of_choices + 1)],
                                                              initial=instance.initial_choice)
        return dynamic_fields

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
QuestionRam RachumView Question on Stackoverflow
Solution 1 - PythonGDornView Answer on Stackoverflow
Solution 2 - PythonYuji 'Tomita' TomitaView Answer on Stackoverflow
Solution 3 - PythonowenwaterView Answer on Stackoverflow
Solution 4 - PythonOZ13View Answer on Stackoverflow
Solution 5 - PythonbobView Answer on Stackoverflow