简体   繁体   English

从用户界面动态添加新的 WTForms FieldList 条目

[英]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.我有一个 flask + wtforms 应用程序,我希望用户能够输入父 object 和任意数量的子对象。 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.) (注意:这是一个人为的示例,用于突出显示单个 .py 文件中的所有工作部分,这会导致一些非常混乱的代码。抱歉。)

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.我知道如何在服务器端创建新的子条目(BookForm 条目)。 I could just pass empty dictionaries to my form, and WTForms would generate the inputs for me:我可以将空字典传递给我的表单,WTForms 会为我生成输入:

...
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. Bam,现在我的页面有三本书的输入字段,我可以预先填充他们的数据。 WTForms is generating the form input content before the page is rendered. WTForms 在页面呈现之前生成表单输入内容。 It works out all the ID's necessary for each input, etc.它计算出每个输入所需的所有 ID,等等。

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?如果我希望用户能够单击一个按钮并从用户端添加 BookForm 输入的新实例,在页面呈现之后......我该如何 go? Do I have to manually construct the input fields myself in JavaScript, using what was generated by WTForms as a reference?我是否必须使用 WTForms 生成的内容作为参考,自己在 JavaScript 中手动构建输入字段? 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? WTForms 有没有办法根据需要为新输入呈现 HTML,管理输入标签 ID 的唯一性等? 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.我可以将一些内容发回服务器到 append 表单的空白条目并重新呈现页面,但这会丢失我所有现有的用户输入,所以它并没有真正起作用。 This Stackoverflow question suggests the same thing in the comments, and the same objection is raised. 这个Stackoverflow 问题在评论中暗示了同样的事情,并且提出了同样的反对意见。

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.当有更好的(甚至可能是官方的)解决方案时,我只是不想采取糟糕的方法。

I understand PERFECTLY what your question, to use pure Flask to achieve this. 我完全理解您的问题,使用纯Flask来实现这一目标。 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. 但是,如果您希望通过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. 我将向您展示一些我通过视图从HTML前端动态加载数据的工作,如果您改变主意并决定合并一些jQuery,希望对您有所帮助。

This is my form, using HTML and Jinja2 : 这是我的表单,使用HTML和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. 这是我编写的有用的jQuery脚本,您可以通过<script>标记将其包含在HTML中。

<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 : 使用jQuery和getJSON()方法,我能够向该视图发送请求:

@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!! 因此,每次发生更改时,都会从站点动态读取数据,将其发送到Flask视图,然后使用所需的内容进行处理,再发送相同的getJSON()并直接插入表单中! Voila, you have dynamically used jQuery+Flask to manipulate whatever you want to do, using your new best friend, JSON. 瞧,您已经动态地使用jQuery + Flask,并使用了您最好的新朋友JSON来进行您想做的事情。

After a lot of research, i found the simplest way to do what you want above, is through jQuery. 经过大量研究,我发现最简单的方法是通过jQuery完成您想要的操作。 Learn getJSON() and post() methods in jQuery, incorporate it into your HTML and you have your answer. 学习jQuery中的getJSON()post()方法,将其合并到您的HTML中,然后您便有了答案。 And also jsonify() which i believe you can import in Python via import json . 还有jsonify() ,我相信您可以通过import json在Python中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. 将列表提供给jsonify() ,您可以使用该列表以与发送数据相同的getJSON()作为视图返回的视图。

Sorry for reviving this question, but I wanted to point out that one can implement a similar strategy without using javascript.很抱歉再次提出这个问题,但我想指出,可以在不使用 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).这个想法是在提交时预处理表单数据,然后使用处理后的数据重新加载表单(使用 wtforms 的内置功能从其数据重新创建表单)。

One way of doing it can be as follows.一种方法可以如下。 Add an extra input field to the form and define an update_self method:在表单中添加一个额外的输入字段并定义一个update_self方法:

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)

Note: formdata=None is necessary to prevent the FlaskForm to automatically read the data in the form.注意: formdata=None是防止 FlaskForm 自动读取表单数据的必要条件。 If you omit it, the form will receive formdata when running the .__init__ method and hence ignore your newly processed data.如果您省略它,表单将在运行.__init__ formdata时接收表单数据,因此忽略您新处理的数据。

In the template, display the new input:在模板中,显示新输入:

{{ form.addline() }}

In the definition route, call the update_self method as first thing on submit:在定义路由中,调用update_self方法作为提交时的第一件事:

@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!我希望这有帮助!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM