How to Use FieldList in Flask-WTF

avatar
Anthony Herbert

Get the code for this article here

If you are using Flask-WTF and you want to create forms that allow the user to enter the same type of information over and over, then using the FieldList class from WTForms will give you the easiest way to accomplish that. In this tutorial, I'll demonstrate how the FieldList class works.

Form Set Up

You can create a simple Flask app for this tutorial. In the app, you can create both a main form and a form that will be repeated in the main form as a FieldList.

# app.py
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, FormField, FieldList, IntegerField, Form
from wtforms.validators import Optional
from collections import namedtuple

app = Flask(__name__)
app.config['SECRET_KEY'] = 'keepthissecret'

class ProductForm(Form):
    title = StringField('Title')
    price = IntegerField('Price', validators=[Optional()])

class InventoryForm(FlaskForm):
    category_name = StringField('Category Name')
    products = FieldList(FormField(ProductForm), min_entries=4, max_entries=8)

@app.route('/', methods=['GET', 'POST'])
def index():
    form = InventoryForm()
    return render_template('index.html', form=form)
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>FieldList Example</title>
</head>
<body>
    <form method="POST" action="">
        {{ form.hidden_tag() }}
        {{ form.category_name.label }} {{ form.category_name }}
        <br />
        {% for nested in form.products %}
            {{ nested.label }}{{ nested }}
        {% endfor %}
        <button>Submit</button>
    </form>
</body>
</html>

You want your main form to be a FlaskForm while the form you will use for the FieldList will be a regular Form from WTForms. The reason for this is that the FlaskForm adds a CSRF token to the form, which is unnecessary for the forms that will be nested in the main form.

To have multiple copies of the same form, you need two classes: FormField and FieldList. The FormField allows you to nest another form within a form, and FieldList allows you to create multiple copies of that nested form.

For FieldList, you need to supply the nested form, of course, but you can also supply the min_entries and max_entries. The number of entries comes into play in two cases: when you have a completely empty form, and when you have some data you want to pass to the form after instantiation (think pulling data from the database).

In the case of the blank form, only the min_entries will count. This is how many copies of the form in the FieldList you will see. In this example, you should see four.

For a form that will be loaded with data, WTForms will add blank fields if you don't supply enough data up to the minimum number of fields. So if you supply two forms worth of data, then WTForms will add two blank forms. max_entires will set a limit of the number of copies displayed. If you pass eight copies of the data but your max_entries is six, you will get an error.

Getting the Form Data

To get the form data, you can access it once your form has passed validation.

# app.py
@app.route('/', methods=['GET', 'POST'])
def index():
    form = InventoryForm()
    if form.validate_on_submit():
        for field in form.products:
            print(field.title.data)
            print(field.price.data)

        for value in form.products.data:
            print(value)

    return render_template('index.html', form=form)

There are two ways you can get the data. You can iterate over the FieldList itself. This will give you a list of form objects, and you can access the data by using the names of the fields of the form in the FieldList. This approach works great if you plan to add the data directly into a database.

You can also iterate over the data of the FieldList. This will give you dictionaries that represent the nested form data that was passed by the user.

Load Data Into Form

You can pass in data to the form that will render along with the form using FieldLists as well.

# app.py
@app.route('/', methods=['GET', 'POST'])
def index():
    product = namedtuple('Product', ['title', 'price'])

    data = {
        'category_name' : 'Widgets',
        'products' : [
            product('Unit 1', 100),
            product('Unit 2', 50),
            product('Unit 3', 20),
            product('Unit 4', 80),
            product('Unit 5', 10),
            product('Unit 6', 80),
            product('Unit 7', 150),
            product('Unit 8', 10),
        ]
    }
    form = InventoryForm(data=data)
    if form.validate_on_submit():
        for field in form.products:
            print(field.title.data)
            print(field.price.data)

        for value in form.products.data:
            print(value)

    return render_template('index.html', form=form)

For this, I used a namedtuple just as a way to give structure to the data. In a real use case, you can get this from the database, and the returned data will already have some structure to it.

Each group will represent a copy of the form. Just remember that if you are using max_entires, any extra data past the max_entries will cause an error.

Conclusion

I hope that helped you understand FieldLists in WTForms a little more. If you have any questions or comments feel free to post down below.