Handling Multiple Forms on the Same Page in Django

Published Jan 23, 2018Last updated Jan 26, 2018
Handling Multiple Forms on the Same Page in Django

Recently, I had to implement the handling of two different forms on
the same page in Django. Though this use case is surprisingly common, I
couldn't find many examples on how to do it the "Django" way.

The premise

Let's say there are two forms on the page. One form for the user to
subscribe to, which only accepts email, and the other a contact form
that takes in a title and a description. We have to handle both forms
in the same view.

class ContactForm(forms.Form):
    title = forms.CharField(max_length=150)
    message = forms.CharField(max_length=200, widget=forms.TextInput)


class SubscriptionForm(forms.Form):
    email = forms.EmailField()

First cut

Let's process them with Django's function based views.

def multiple_forms(request):
    if request.method == 'POST':
        contact_form = ContactForm(request.POST)
        subscription_form = SubscriptionForm(request.POST)
        if contact_form.is_valid() or subscription_form.is_valid():
            # Do the needful
            return HttpResponseRedirect(reverse('form-redirect') )
    else:
        contact_form = ContactForm()
        subscription_form = SubscriptionForm()

    return render(request, 'pages/multiple_forms.html', {
        'contact_form': contact_form,
        'subscription_form': subscription_form,
    })

The main issue with this approach is that there is no surefire way to
only trigger the validation and submission of the form that was
submitted. Also, it is clumsy to refactor the post-submission logic to
this code.

How about using class-based views? In order to understand how we can use
a class-based view to solve this, let's look at how a single form is
handled using CBVs.

The Django FormView is the staple class for handling forms this way.
At the minimum, it needs:

  • A form_class attribute that points to the class whose form we
    want to process.
  • A success_url attribute to indicate which URL to redirect to upon
    successful form processing.
  • A form_valid method to do the actual processing logic.

Here's how Django's class-based form processing hierarchy looks like:

form-inheritance.png

Designing our multiform handling mechanism

Let's try and imitate the same for multiple forms. Our design criteria
are:

  • Ease of use or better developer experience in terms of refactoring
    and testing.
  • Intiutive, i.e., if it works and behaves like existing form handling.
    CBVs, it would be easy to understand and consume
  • Modular, where I can reuse this across different projects
    and apps.

Instead of a single class, we will have a dict of form classes. We
follow the same rule for success URLs as well. It's quite plausible that
every form on the page has its own URL to be redirected upon successful
submission.

A typical usage pattern would be:

class MultipleFormsDemoView(MultiFormsView):
    template_name = "pages/cbv_multiple_forms.html"
    form_classes = {'contact': ContactForm,
                    'subscription': SubscriptionForm,
                    }

    success_urls = {
        'contact': reverse_lazy('contact-form-redirect'),
        'subscription': reverse_lazy('submission-form-redirect'),
    }

    def contact_form_valid(self, form):
        'contact form processing goes in here'

    def subscription_form_valid(self, form):
        'subscription form processing goes in here'

We design a similar class hierarchy for multiple form handling as well.

multiple-form.png

There are two notable changes while handling multiple forms. For the
correct form processing function to kick in, we have to route during the
POST using a hidden parameter in every form, called action. We embed
this hidden input in both of the forms. It could also be done in a more
elegant manner, as in:

class MultipleForm(forms.Form):
    action = forms.CharField(max_length=60, widget=forms.HiddenInput())


class ContactForm(MultipleForm):
    title = forms.CharField(max_length=150)
    message = forms.CharField(max_length=200, widget=forms.TextInput)


class SubscriptionForm(MultipleForm):
    email = forms.EmailField()

The value of the action attribute is typically the key name in the
form_classes. Notice how the prefix for each form~valid~function is
mapped with the key name in the form_classes dict. This is taken from
the action attribute.

The second change is to make sure that this action attribute is
prefilled with the correct form name from the form_classes dict. We
slightly alter get_initial function to do this while giving provision
for developers to override this on a per form basis.

Seriously, I had a
requirement to develop multiple forms with each form having
its own set of initial arguments.

def get_initial(self, form_name):
    initial_method = 'get_%s_initial' % form_name
    if hasattr(self, initial_method):
        return getattr(self, initial_method)()
    else:
        return {'action': form_name}

The actual form validation function will be called from post().

def _process_individual_form(self, form_name, form_classes):
    forms = self.get_forms(form_classes)
    form = forms.get(form_name)
    if not form:
        return HttpResponseForbidden()
    elif form.is_valid():
        return self.forms_valid(forms, form_name)
    else:
        return self.forms_invalid(forms)

There is a lot of scope for improvement, like resorting to some default
behaviour when the form's form~valid~ function does not exist, or
throwing exceptions, but this should suffice for most cases.

We can refer to these forms in the template by the dict key name. For
example, the above forms would be rendered in the template as:

<form method="post">{% csrf_token %}
    {{ forms.subscription }}
    <input type="submit" value="Subscribe">
</form>
<form method="post">{% csrf_token %}
    {{ forms.contact }}
    <input type="submit" value="Send">
</form>

Conclusion

Looks like we met our design criteria by making form handling
more modular, which can be reused across projects. We can also extend
this to add more forms in our view and refactor our logic for just a
single form if needed. Also, the code is very similar to the existing form's
CBVs without much of a learning curve.

You can find the code for multiforms, along with sample usage, here.

Discover and read more posts from Lakshmi Narasimhan
get started