Flask WTF forms Dynamic fields using Javascript

In this blog, we will build dynamic forms using wtf forms in Flask. Sometimes we don't know how many fields the form will have and we have to add dynamics field to form on runtime either by javascript or by the backend. You can check out the complete code used in this blog here

In this blog, we will build a dynamic form where we don't know exactly how many fields there going to be. Let's take an example. We want to build a small application in which users will submit a list of their favorite movies. But, some users can submit one movie or 5 movies. For this, we need to build dynamic number of inputs on go.

For this purpose, WTF forms provide the FieldList field. We will use this field with javascript to build our dynamic fields. Here is the starter code.

# app.py
from flask import Flask, render_template

app = Flask(__name__, template_folder='./templates')

@app.route('/')
def index():
    return render_template('index.html')
<!-- templates/index.html  -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flask dynamic WTF forms</title>
</head>
<body>
    <h1>Hello World</h1>
</body>
</html>

If You ever used flask WTF forms you might already know i. To create a form we need to create a subclass of FlaskForm. Let's first create a simple form without any dynamic fields and then we will make them dynamic.

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, FieldList
from wtforms.validators import DataRequired


class FavouriteMoviesForm(FlaskForm):
    username = StringField("Your name", validators=[DataRequired()])
    movies = FieldList(StringField('Movie Name'), min_entries=1, max_entries=5)

app = Flask(__name__, template_folder='./templates')
app.config['SECRET_KEY'] = "mysupersecretkey"

@app.route('/', methods=['GET', 'POST'])
def index():
    favouriteMoviesForm  = FavouriteMoviesForm()
    if favouriteMoviesForm.validate_on_submit():
        return render_template('submit.html', username=favouriteMoviesForm.username.data, movies=favouriteMoviesForm.movies.entries)
    return render_template('index.html', form=favouriteMoviesForm)

we added Secret key in app's config because flask wtf forms uses secret key to create CSRF token. Don't store secret key in plain text in a real application. We here doing here because it's just an example app.

First, we created FavouriteMoviesForm class which is a subclass of FlaskForm. and we are using it in our index route.

The important thing here to take note of is here we are passing StringField as an argument to FieldList field with min_entries and max_entries values. What that will do is tell flask forms that we want to have a minimum of 1 and a maximum of 5 movie name fields. But this will not automatically generate those fields for us. This will only make sure we don't break min and max constraints.

Let's render the form and see what we will get.

Screenshot_2021-03-25 Flask dynamic WTF forms.png

Here you can see that we have two field one is username field and another is movies list. But as you can see it is only showing one movie name field. Here is html code of index.html

<!-- templates/index.html  -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flask dynamic WTF forms</title>
</head>
<body>
    <h1 style="text-align: center;">My Favourite movies</h1>
    <form method="POST" action="">
        {{ form.csrf_token }}
        {{form.username.label}}
        {{ form.username() }} <br>
        <h3>Favourite Movies List</h3>
        {{ form.movies.label }}
        {{ form.movies() }}
        <button type="submit">Post Favourite movies</button>
    </form>
</body>
</html>

Let's take a look at how Fieldlist renders the list of movie name fields.

Screenshot from 2021-03-25 11-54-08.png

As you can see the movies list contains an input field with movies-0. Because we have min_entries set to 1 that's why it's only rendered one field if we set it 2 we will two fields render by default. let's see if have two fields what would be the names of movie input fields.

Screenshot from 2021-03-25 11-59-32.png

as you can see that the second field name is movies-1. Now you can see the patterns. If had three movies the name of the third field would have been movies-2.

Let's write some javascript that will allow users to click a button to add a new movie field. We will also make sure that there will be at least one field and a maximum of 5 fields because that's what our forms accept. We will add one button by clicking that button user can add a new field.

<script>
    window.onload = function() {
        let addMovieFieldBtn = document.getElementById('add-movie-field');
        addMovieFieldBtn.addEventListener('click', function(e){
            e.preventDefault();
            let allMoviesFieldWrapper = document.getElementById('movies');
            let allMoviesField = allMoviesFieldWrapper.getElementsByTagName('input');
            if(allMoviesField.length > 4) {
                alert('You can  have only five movies name');
                return;
            }
            let movieInputIds = []
            for(let i = 0; i < allMoviesField.length; i++) {
                movieInputIds.push(parseInt(allMoviesField[i].name.split('-')[1]));
            }
            let newFieldName = `movies-${Math.max(...movieInputIds) + 1}`;
            allMoviesFieldWrapper.insertAdjacentHTML('beforeend',`
            <li><label for="${newFieldName}">Movie Name</label> <input id="${newFieldName}" name="${newFieldName}" type="text" value=""></li> 
            `);
        });
    }
</script>

Here you can see the javascript we need to add a new movie field when the user clicks add add new movie button.

Let's understand the tricky part of Javascript. Herelet allMoviesField = allMoviesFieldWrapper.getElementsByTagName('input'); we are fetching all the movies input fields. We are doing that so we can know how many fields are there. If the number of fields is already 5 then we will show the user an error. If not we will get names of the all the inputs. and create a new name for our new input field. We don't want to have two fields with the same name. That's why we are getting the integer part of all names then adding one to the max value for new input fields. Using this way we will avoid making two fields with the same name. Here is what the final form looks like. You can check out the complete code used in this blog here.

The important point is that FieldList doesn't care about the input being sequential. You can have one field name movies-0 and another movies-100. It only cares about the field name must start with the name of the field which is in our case is movies.

Screenshot_2021-03-25 Flask dynamic WTF forms(1).png