DEV Community

loading...

Combining multiple forms in Flask-WTForms but validating independently

Sam Partington
Tech Lead at White October
Originally published at blog.whiteoctober.co.uk ・5 min read

Introduction

We have a Flask project coming up at White October which will include a user profile form. This form can be considered as one big form or as multiple smaller forms and can also be submitted as a whole or section-by-section.

As part of planning for this work, we did a proof-of-concept around combining multiple subforms together in Flask-WTForms and validating them.

Note that this is a slightly different pattern to "nested forms". Nested forms are often used for dynamic repeated elements - like adding multiple addresses to a profile using a single nested address form repeatedly. But our use case was several forms combined together in a non-dynamic way and potentially processed independently. This article also doesn't consider the situation of having multiple separate HTML forms on one page.

This document explains the key things you need to know to combine forms together in Flask-WTF, whether you're using AJAX or plain postback.

Subforms

The first thing to know is how to combine multiple WTForms forms into one. For that, use the FormField field type (aka "field enclosure"). Here's an example:

from flask_wtf import FlaskForm
import wtforms

class AboutYouForm(FlaskForm):
    first_name = wtforms.StringField(
        label="First name", validators=[wtforms.validators.DataRequired()]
    )
    last_name = wtforms.StringField(label="Last name")

class ContactDetailsForm(FlaskForm):
    address_1 = wtforms.StringField(
        label="Address 1", validators=[wtforms.validators.DataRequired()]
    )
    address_2 = wtforms.StringField(label="Address 2")

class GiantForm(FlaskForm):
    about_you = wtforms.FormField(AboutYouForm)
    contact_details = wtforms.FormField(ContactDetailsForm)

As you can see, the third form here is made by combining the first two.

You can render these subforms just like any other form field:

{{ form.about_you }}

(Form rendering is discussed in more detail below.)

Validating a subform

Once we'd combined our forms, the second thing we wanted to prove was that they could be validated independently.

Normally, you'd validate a (whole) form like this:

if form.validate_on_submit()
    # do something

(validate_on_submit returns true if the form has been submitted and is valid.)

It turns out that you can validate an individual form field quite easily. For our about_you field (which is a subform), it just looks like this:

form.about_you.validate(form)

Determining what to validate

We added multiple submit buttons to the form so that either individual subforms or the whole thing could be processed. If you give the submit buttons different names, you can easily check which one was pressed and validate and save appropriately (make sure you only save the data you've validated):

<input type="submit" name="submit-about-you" value="Just submit About You subform">
<input type="submit" name="submit-whole-form" value="Submit whole form">

And then:

if "submit-about-you" in request.form and form.about_you.validate(form):
    # save About You data here
elif "submit-whole-form" in request.form and form.validate():
    # save all data here

If you have one route method handling both HTTP GET and POST methods, there's no need to explicitly check whether this is a postback before running the above checks - neither button will be in request.form if it's not a POST.

Alternative approaches

You could alternatively give both submit buttons the same name and differentiate on value. However, this means that changes to the user-facing wording on your buttons (as this is their value property) may break the if-statements in your code, which isn't ideal, hence why different names is our recommended approach.

If you want to include your submit buttons in your WTForms form classes themselves rather than hard-coding the HTML, you can check which one was submitted by checking the relevant field's data property - see here for a small worked example of that.

Gotcha: Browser-based validation and multiple submit buttons

There's one snag you'll hit if you're using multiple submit buttons to validate/save data from just one subform of a larger form.

If your form fields have the required property set (which WTForms will do if you use the DataRequired validator, for example), then the browser will stop you submitting the form until all required fields are filled in - it doesn't know that you're only concerned with part of the form (since this partial-submission is implemented server-side).

Therefore, assuming that you want to keep using the required property (which you should), you'll need to add some Javascript to dynamically alter the form field properties on submission.

This is not a problem if you're using AJAX rather than postbacks for your form submissions; see below how to do that.

Rendering subforms in a template

The examples in this section use explicit field names. In practice, you'll want to create a field-rendering macro to which you can pass each form field rather than repeating this code for every form field you have. That link also shows how to render a field's label and widget separately, which gives you more control over your markup.

As mentioned above, the subforms can be rendered with a single line, just like any other field:

{{ form.about_you }}

If you want to render fields from your subforms individually, it'll look something like this:

<label for="{{ form.about_you.first_name.id }}">{{ form.about_you.first_name.label }}</label>
{{ form.about_you.first_name }}

As you see, you can't do single-line rendering of form fields and their labels for individual fields within subforms - you have to explicitly render the label.

Displaying subform errors

For a normal form field, you can display associated errors by iterating over the errors property like this:

{% if form.your_field_name.errors %}
    <ul class=errors>
        {% for error in field.errors %}
            <li>{{ error }}</li>
        {% endfor %}
    </ul>
{% endif %}

In this case, errors is just a list of error strings for the field.

For a subform where you're using the FormField field type, however, errors is a dictionary mapping field names to lists of errors. For example:

{
    'first_name': ['This field is required.'],
    'last_name': ['This field is required.'],
}

Therefore, iterating over it in your template is more complicated. Here's an example which displays errors for all fields in a subform (notice the use of the items method):

{% if form.about_you.errors %}
    <ul class="errors">
        {% for error_field, errors in form.about_you.errors.items() %}
            <li>{{ error_field }}: {{ errors|join(', ') }}</li>
        {% endfor %}
    </ul>
{% endif %}

Doing it with AJAX

So far, we've considered the case of validating subforms when data is submitted via a full-page postback. However, you're likely to want to do subform validation using AJAX, asynchronously posting form data back to the Flask application using JavaScript.

There's already an excellent article by Anthony Plunkett entitled "Posting a WTForm via AJAX with Flask", which contains almost everything you need to know in order to do this.

In this article, therefore, I'll just finish by elaborating on the one problem you'll have if doing this with multiple submit buttons - determining the submit button pressed

Determining the submit button pressed

When posting data back to the server with JavaScript, you're likely to use a method like jQuery's serialize. However, the data produced by this method doesn't include details of the button clicked to submit the form.

There are various ways you can work around this limitation. The approach I found most helpful was to dynamically add a hidden field to the form with the same name and value as the submit button (see here). That way, Python code like if "submit-about-you" in request.form (see above) can remain unchanged whether you're using AJAX or postbacks.

Discussion (6)

Collapse
rozzah profile image
RoZZaH • Edited

While this may be obvious to those more experienced.. if you have a custom validator at the subform/field level you need to define a custom validator outside the block as inline validators (e.g. def validate_somefield) only seem to work on validate_on_submit()

def custom_validator(form,field):
  local_var = field.data
  if some condition:
    raise ValidationError("some error message")

class SomeSubForm(FlaskForm)
    some_field = StringField(validators=[custom_validator])
Collapse
jerrykco profile image
Jerry Kauffman

Hi Sam is their anyway to use session information to create a conditional around sections of the form?
I am using WTForms and have a long calibration data entry form. Their are essentially three parts to the form. Main (every unit we calibrate needs to have this data entered), Option 1 (this data only is needed if the equipment has this option installed), and Option 2 (same as option 1).
I can tell ahead of time by the model of the equipment if what the options are.
I have session variables which I can use to know if the option is true or not.
When I tried to check session.get('option') in the form class for my calibrate data entry, I get this.
raise RuntimeError(_request_ctx_err_msg)
RuntimeError: Working outside of request context.

I am thinking that I should either do what you did or perhaps make three separate forms and get them filled in succession conditionally.

Collapse
jerrykco profile image
Jerry Kauffman

I was able to fix the runtime error.
And I took a completely different approach using wtforms.readthedocs.io/en/2.3.x/sp....
So I have one form but based on the model options I have two functions (one for each option) that do a del form.variable for each of the variables that are not available because the option isn't included. Seems to work well.

Collapse
aalramez16 profile image
aalramez16

Hello, I found this a very useful piece on getting multiple forms to work, but when it comes to passing the actual form data, I find that using form.subform passes a string, which isn't callable, and form.subform() passes an HTMLString, which claims to not have my attributes listed. Any suggestions?

Collapse
toonarmycaptain profile image
toonarmycaptain

How do you test this? It seems flask doesn't like the nested dict you pass when you make the request data for the nested forms?

Collapse
hadasbi profile image
hadasbi

Hi, I would really appreciate some help with this: stackoverflow.com/questions/691149...