Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically add new WTForms FieldList entries from user interface

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.

like image 271
daveruinseverything Avatar asked Aug 13 '18 07:08

daveruinseverything


1 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.

like image 138
jojostev97 Avatar answered Sep 22 '22 12:09

jojostev97