61

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?

Ram Rachum
  • 84,019
  • 84
  • 236
  • 374

5 Answers5

76

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.

Lutz Prechelt
  • 36,608
  • 11
  • 63
  • 88
GDorn
  • 8,511
  • 6
  • 38
  • 37
  • 1
    How would you pass initial data in this case? – mgPePe May 13 '14 at 17:39
  • 1
    Nice link, but this use-case is specifically what formsets are for. – mkoistinen Nov 29 '17 at 18:51
  • This does not work when deserializing the form when the user submits it. `MyDynamicForm(request.POST)` would be missing the fields that were added after instantiation in the `self.fields` dictionary. – Neil Sep 24 '22 at 02:38
  • @Neil if you add those fields during the `__init__()` function, before calling the parent, the form will be able to deserialize them. You'll need to add those fields both displaying and processing the form; the form class does not magically create those fields for you based on the POST data. – GDorn Oct 01 '22 at 03:17
39

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" />
Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245
  • 1
    If you have more fields in your model, /__prefix__/g instead of '__prefix__' might be quite useful ;) – Michael Angelos Simos Jul 01 '14 at 15:11
  • some minor typo's (1) in last line views: return render(request, 'template', {'formset': formset})) (one bracket too much) (2) in template, forgetting to remove a ' in line: html = $("#form_template").clone().html().replace(/__prefix_/g', i); must be: $("#form_template").clone().html().replace(/__prefix_/g, i); – Jellema Apr 03 '15 at 08:36
  • great example, learned a lot by it, just fixing some errors that occured, a third one (3): in views you also have to call the function form.is_valid() to provide the forms with cleaned_data attributes – Jellema Apr 03 '15 at 08:37
  • So how does the form get submitted? I can't seem to get the value of the form fields in my view. I've even added a submit button inside the form. My `cleaned_data` in both `'form` and `view` are empty. How do I send the `POST` request? – chidimo Nov 20 '18 at 17:41
  • Its submitting now. This line `print "You've picked {0}".format(form.cleaned_data['color'])` results in a `KeyError`. My `formset.cleaned_data` is not getting called. Every field returns empty. The formset returns as valid but I can't get any values field values – chidimo Nov 20 '18 at 17:59
22

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

Guillaume Lebreton
  • 2,586
  • 16
  • 25
owenwater
  • 811
  • 1
  • 6
  • 18
  • This works great. If you pass a list of FormFields instead of just 'n', you can modify the loop a bit to dynamically generate a arbitrary form! (that's what I did) - the example above comes from the link in the chosen answer BTW. – FizxMike Nov 02 '16 at 02:12
  • Does this go in the views.py file? – Prakhar Rathi Apr 26 '22 at 18:19
11

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.

OZ13
  • 256
  • 3
  • 4
  • Really like this approach. Haven't personally tried it yet, but I have a similar problem and am researching possible solutions, and this looks like the simplest and most straightforward solution. Thanks. – Nico Bako Mar 29 '22 at 12:34
1

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
bob
  • 681
  • 5
  • 17