Dynamically add new WTForms FieldList entries from user interface

Question:

I have a flask + wtforms app where I want users to be able to input both a parent object, and an arbitrary number of child objects. I’m not sure what the best way to dynamically create new child form input fields from the user interface.

What I’ve got so far

Below is a complete working example. (Note: this is an artificial example to highlight all the working parts inside a single .py file, which makes for some pretty messy code. Sorry.)

from flask import Flask, render_template_string
from flask_wtf import FlaskForm
from wtforms import FieldList, FormField, StringField, SubmitField
from wtforms.validators import InputRequired


# Here I'm defining a parent form, AuthorForm, and a child form, BookForm.
# I'm using the FieldList and FormField features of WTForms to allow for multiple
# nested child forms (BookForms) to be attached to the parent (AuthorForm).
class BookForm(FlaskForm):
    title = StringField('title', validators=[InputRequired()])
    genre = StringField('genre', validators=[InputRequired()])

# I'm defining a min_entry of 1 so that new forms contain a blank BookForm entry
class AuthorForm(FlaskForm):
    name = StringField('name', validators=[InputRequired()])
    books = FieldList(FormField(BookForm), min_entries=1)
    submit = SubmitField('Save')


# Here's my Jinja template
html_template_string = """
<html>
    <head><title>stackoverflow example</title></head>
    <body>
        <form action="" method="post" role="form">
            {{ form.hidden_tag() }}
            {{ form.name.label }} {{ form.name() }}
            {% for book in form.books %}
                <p>
                {{ book.title.label }} {{ book.title() }}
                {{ book.genre.label }} {{ book.genre() }}
                {{ book.hidden_tag() }}
                </p>
            {% endfor %}
            {{ form.submit() }}
        </form>
    </body>
</html>
"""

# Alright, let's start the app and try out the forms
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'

@app.route('/', methods=['GET', 'POST'])
def index():
    form = AuthorForm()
    if form.validate_on_submit():
        for book in form.books.data:
            print(book)
    return render_template_string(html_template_string, form=form)

if __name__ == '__main__':
    app.run()

Where I get stuck

I know how to create new child entries (BookForm entries) on the server side. I could just pass empty dictionaries to my form, and WTForms would generate the inputs for me:

...
form = AuthorForm()
form.books.append_child({})
form.books.append_child({})
form.books.append_child({})
...

Bam, now my page has input fields for three books, and I could’ve prepopulated their data.
WTForms is generating the form input content before the page is rendered. It works out all the ID’s necessary for each input, etc.

If I want a user to be able to click a button and add new instances of BookForm inputs from the user side, after the page has been rendered … how do I go about it? Do I have to manually construct the input fields myself in JavaScript, using what was generated by WTForms as a reference? This seems messy and prone to breakage, or at least to ugly code.

Is there a way for WTForms to render the HTML for new inputs as needed, managing the uniqueness of input tag ID’s and such? I could post something back to the server to append blank entries to the form and re-render the page, but that would lose me all of my existing user input, so it doesn’t really work. This Stackoverflow question suggests the same thing in the comments, and the same objection is raised.

If this has to be done the ugly manual way, then I can manage that. I just don’t want to take a poor approach when there’s a better (and maybe even official) solution to this.

Answers:

I understand PERFECTLY what your question, to use pure Flask to achieve this.
But I was at your same situation a couple of months back. And after a lot of research and odd and redundant methods, I found, implementing it will be not only painstaking, but long and hard and any small change or debugging efforts just breaks the whole thing.

I know this isnt exactly what you asked. But, if you are looking to do this through Flask+ jQuery. I’ll show you some of the work I did to dynamically load data from HTML front-end via views, hope this can be helpful to you, IF you change your mind and decided to incorporate some jQuery.

This is my form, using HTML and Jinja2 :

<form method="POST" action="">
            {{ form.hidden_tag() }}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">XYZ</legend>

                <div>
                    {{ form.standard.label(class="form-control-label") }}
                    {{ form.standard(class="form-control form-control-lg") }}
                </div>

                <div>
                    {{ form.wps.label(class="form-control-label") }}
                    {{ form.wps(class="form-control form-control-lg") }}
                </div>
            ... 
            </fieldset>
            <div class="form-group">
                {{ form.submit(class='btn btn-outline-success') }}
            </div>

        </form>

This is the view this calls this form :

@app.route("/newqualification/<welder_id>", methods=['GET', 'POST'])
def newqualification(welder_id=None):
    form = #passformhere

        #writemethod.

    return render_template('xyz.html', title='xyz', form=form)

And this is the little helpful jQuery Script I wrote which you can include in the HTML via <script> tag.

<script>
 $(document).ready(function(){

            $("#standard").change(function(){
                var std = $(this).find('option:selected').val(); //capture value from form.

                $.getJSON("{{url_for('modifywps')}}",{'std':std}, function(result){ //use captured value to sent to view via a dictionary {'key':val}, via url_for('view_name')

                    $.each(result, function(i, field){ //value you want will be back here and gone through..
                        $.each(field, function(j,k){
                            $("#wps").append('<option value="'+j+'">'+k+'</option>'); // use jQuery to insert it back into the form, via the form name you gave in jinja templating
                        });
                    });
                });
            });


            $("#wps").change(function(){

                    var std = $('#wps').find('option:selected').val(); // capture value from form.
                    $.getJSON("{{url_for('modifyprocess')}}",{'std':std}, function(result)

                        $.each(result, function(i, field){
                            $.each(field, function(j,k){
                                $("#processes").append('<option value="'+j+'">'+k+'</option>');
                            });
                        });
                    });
            });
</script>

Using jQuery and the getJSON() method, i am able to send a request to this view :

@app.route("/modifywps", methods=['POST', 'GET'])
def modifywps():
    application_standard_id = request.args.get('std') # gets value from the getJson()

   # process it however you want.. 

    return #whatever you want.

So every time there is a change, data is read dynamically off the site, sent to a Flask view, processed there with whatever you want, sent back the the same getJSON() and inserted right into the form!!
Voila, you have dynamically used jQuery+Flask to manipulate whatever you want to do, using your new best friend, JSON.

After a lot of research, i found the simplest way to do what you want above, is through jQuery.
Learn getJSON() and post() methods in jQuery, incorporate it into your HTML and you have your answer.
And also jsonify() which i believe you can import in Python via import json.

Feed a list to jsonify() which you can use to return as the view in the same getJSON() you used to send the data.

Answered By: jojostev97

Sorry for reviving this question, but I wanted to point out that one can implement a similar strategy without using javascript. The idea is to pre-process the form data on submit and then reload the form with the processed data (using the built-in capabilities of wtforms to recreate a form from its data).

One way of doing it can be as follows. Add an extra input field to the form and define an update_self method:

class AuthorForm(FlaskForm):
    name = StringField('name', validators=[InputRequired()])
    books = FieldList(FormField(BookForm), min_entries=1)
    submit = SubmitField('Save')
    addline = SubmitField('Add new line')  # This is the new input

    def update_self(self):
        # read the data in the form
        read_form_data = self.data

        # modify the data as you see fit:
        updated_list = read_form_data['books']
        if read_form_data['addline']:
            updated_list.append({})
        read_form_data['books'] = updated_list

        # reload the form from the modified data
        self.__init__(formdata=None, **read_form_data)
        self.validate()  # the errors on validation are cancelled in the line above

Note: formdata=None is necessary to prevent the FlaskForm to automatically read the data in the form. If you omit it, the form will receive formdata when running the .__init__ method and hence ignore your newly processed data.

In the template, display the new input:

{{ form.addline() }}

In the definition route, call the update_self method as first thing on submit:

@app.route('/', methods=['GET', 'POST'])
def index():
    form = AuthorForm()

    if form.validate_on_submit():
        form.update_self()  # This reloads the form with the processed data
        for book in form.books.data:
            print(book)

    return render_template_string(html_template_string, form=form)

I hope this helps!

Answered By: Federico Vigolo
class AuthorForm(FlaskForm):
    name = StringField('name', validators=[InputRequired()])
    add_book = SubmitField("Add Book") # ADD THIS
    books = FieldList(FormField(BookForm), min_entries=1)
    submit = SubmitField('Save')

@app.route('/', methods=['GET', 'POST'])
def index():
    form = AuthorForm()

    if form.add_book.data: # ADD THIS
      form.books.append_entry(None)
    elif form.validate_on_submit():
        for book in form.books.data:
            print(book)
    return render_template_string(html_template_string, form=form)

# ADD `add_book` BUTTON TO TEMPLATE:
html_template_string = """
<html>
    <head><title>stackoverflow example</title></head>
    <body>
        <form action="" method="post" role="form">
            {{ form.hidden_tag() }}
            {{ form.name.label }} {{ form.name() }}
            {{ form.add_book }}
            {% for book in form.books %}
                <p>
                {{ book.title.label }} {{ book.title() }}
                {{ book.genre.label }} {{ book.genre() }}
                {{ book.hidden_tag() }}
                </p>
            {% endfor %}
            {{ form.submit() }}
        </form>
    </body>
</html>
"""
Answered By: Neil McGuigan