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:
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.
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.
Very nice and well explained - thank you very much !!!
Is there a book for advanced topics in Django that you could recommend to me?
Thank you Lakshmi!! You saved me.
Perfect!!!