Skip to content

WTForms integration

Get started

1. Installation

# Install the WTForms version with Poetry
poetry add tna-frontend-jinja[wtforms]

# Install the WTForms version with pip
pip install tna-frontend-jinja[wtforms]

The dependencies added for WTForms are done through Flask-WTF.

Flask-WTF includes some helpful extras to WTForms, like CSRF and file upload. Both are maintained as part of the Pallets Ecosystem.

tna-frontend-jinja depends on the email version of Flask-WTF so that email-validator is also included, allowing you to validate email addresses.

The dependency chain looks like:

tna-frontend-jinja[wtforms]
  ↳ Flask-WTF[email]
    ↳ Flask
    ↳ WTForms
    ↳ email_validator
    ↳ ...

2. Add the TNA Frontend Jinja WTForms helpers

from flask import Flask
from tna_frontend_jinja.wtforms.helpers import WTFormsHelpers


app = Flask(__name__)
WTFormsHelpers(app)

3. Form setup

3.1. Create a form with fields

from flask_wtf import FlaskForm
from wtforms import (
    EmailField,
    StringField,
    SubmitField,
)


class TextInputForm(FlaskForm):
    username = StringField(
        "Username",
        description="This will be used to log in",
    )

    email = EmailField(
        "Email address",
    )

    submit = SubmitField(
        "Continue",
    )

3.2. Add some validation

from flask_wtf import FlaskForm
from wtforms import (
    EmailField,
    StringField,
    SubmitField,
+     validators,
)


class TextInputForm(FlaskForm):
    username = StringField(
        "Username",
        description="This will be used to log in",
+         validators=[
+             validators.InputRequired(message="Enter a username"),
+             validators.Length(
+                 max=32, message="Usernames must be 32 characters or fewer"
+             ),
+         ],
    )

    email = EmailField(
        "Email address",
+         validators=[
+             validators.InputRequired(message="Enter an email address"),
+             validators.Email(message="Enter a valid email address"),
+         ],
    )

    submit = SubmitField(
        "Continue",
    )

3.3. Use the TNA widgets

Use render_kw to set defaults for the attributes on the field. These can be overwritten by params in the template.

The available options can be found under the "Nunjucks options" dropdown of the Nunjucks examples in the components section of the National Archives Design System.

from flask_wtf import FlaskForm
+ from tna_frontend_jinja.wtforms import (
+     TnaEmailInputWidget,
+     TnaTextInputWidget,
+ )
from wtforms import (
    EmailField,
    StringField,
    validators,
)


class TextInputForm(FlaskForm):
    username = StringField(
        "Username",
        description="This will be used to log in",
        validators=[
            validators.InputRequired(message="Enter a username"),
            validators.Length(
                max=32, message="Usernames must be 32 characters or fewer"
            ),
        ],
+         widget=TnaTextInputWidget(),
    )

    email = EmailField(
        "Email address",
        validators=[
            validators.InputRequired(message="Enter an email address"),
            validators.Email(message="Enter a valid email address"),
        ],
+         widget=TnaEmailInputWidget(),
+         render_kw={"size": "m"},
    )

    submit = SubmitField(
        "Continue",
+         widget=TnaSubmitWidget(),
    )

4. Routing

Create a route to display your form and accept POST requests and another route to handle the success state.

This allows us to use the Post/Redirect/Get pattern.

# Accept GET and POST requests
@app.route("/my-form/", methods=["GET", "POST"])
def my_form():
    # Instantiate a form
    form = MyForm()

    # flask_wtf provides a validate_on_submit which runs on POST and runs the validation
    if form.validate_on_submit():
        # Redirect to the success page
        return redirect(url_for("success"))

    # If the request is a GET or validation is not successful, render the form
    return render_template("my-form.html", form=form)

@app.route("/success/")
def success():
    # Render the success/next page
    return render_template("success.html")

5. Templates

Add a my-form.html template to render the form.

Output the form fields in whatever order or structure you need.

Customise the components by adding a params parameter to the field constructor. These can be any documented in the National Archives Design System for that component.

{%- from 'components/error-summary/macro.html' import tnaErrorSummary -%}

{% if form.errors %}
  {{ tnaErrorSummary(wtforms_errors(form)) }}
{% endif %}

<h1 class="tna-heading-xl">My form</h1>

<form action="{{ url_for('my_form') }}" method="post" novalidate>
  {{ form.csrf_token }}

  <!-- Add the username field -->
  {{ form.username }}

  <!-- Add the email field and customise it with parameters -->
  {{ form.email(params={'headingLevel': 2}) }}

  <div class="tna-button-group">
    {{ form.submit }}
  </div>
</form>

Supported fields and widgets

WTForms field TNA widget(s)
BooleanField TnaCheckboxWidget
ColorField 🔧 [not yet supported]
DateField ❌ [not supported] - use TnaDateField
DateTimeField 🔧 [not yet supported]
DateTimeLocalField 🔧 [not yet supported]
DecimalField TnaNumberInputWidget
DecimalRangeField 🔧 [not yet supported]
EmailField TnaEmailInputWidget
FieldList 🔧 [not yet supported]
FileField TnaFileInputWidget or TnaDroppableFileInputWidget
FloatField TnaNumberInputWidget
FormField TnaFieldsetWidget (optional)
HiddenField ✅ [none needed]
IntegerField TnaNumberInputWidget
IntegerRangeField 🔧 [not yet supported]
MultipleFileField TnaFilesInputWidget or TnaDroppableFilesInputWidget
MonthField ❌ [not supported] - use TnaMonthField
PasswordField TnaPasswordWidget
RadioField TnaRadiosWidget
SelectField TnaSelectWidget
SearchField TnaSearchFieldWidget
SelectMultipleField TnaCheckboxesWidget
SubmitField TnaSubmitWidget
StringField TnaTextInputWidget
TelField TnaTelInputWidget
TimeField 🔧 [not yet supported]
TextAreaField TnaTextareaWidget
URLField TnaUrlInputWidget