pythonflaskwtforms

Adding multiple WTF-fields to FlaskForm from frontend


I'd like to make a form with an inconstant number of fields so than the user can add as many new fields as he needs.

How can I get all the form data on the Python side (via WTForms), without knowning the nnumber of fields in advance?

forms.py

class Form(FlaskForm):
    field = StringField('field')
    submit = SubmitField('submit')

HTML

<!-- I want my form to be viewed in Bootstrap Modal. -->

<div class="modal" tabindex="-1">
  <div class="modal-body">

    <form name="create-form" method="POST">
      <div class="input-group">
        {{ form.hidden_tag() }}
        {{ form.field(class="form-control") }}
        <!-- The place to insert additional fields -->
    
        {{ form.submit(class="btn btn-outline-secondary) }}
      </div>
    </form>
    <button id="add" type="button" class="btn btn-outline-secondary">Add</button>

  </div>
</div>

<template id="new-field">
  {{ form.field(class="form-control") }}
</template>

JS

modal.querySelector('#add').addEventListener('click', (ev) => { 
    modal.querySelector(/*Place to insett*/).before(/*new field from <template>*/);
});

modal.querySelector('form').addEventListener('submit', async (ev) => {
    ev.preventDefault();
    let form = modal.querySelector('form');
    let resp = await fetch('/ajax', {method: 'POST', body: new FormData(form)});
});
                       
routes.py

@bp.route('/ajax', methods=['GET', 'POST'])
def ajax():
    # request.form contains all values 
    form = Form()
    
    if form.validate():
        # form.data has the only one value (from first field)
        return {}
    return {}

Yep, it seems I need unique wtf-field for each input. Ok, back to the drawing board.

forms.py

class Form(FlaskForm):
    field = StringField('field')
    submit = SubmitField('submit')

    def add_filed(self, i):
        field = StringField(f'field{i}')
        setattr(self.__class__, f'field{i}', field)
        return self.__class__()
            
HTML

<template id="new-field">
  {{ form.add_field(7)|attr("field7")(class="form-control") }}
</template>

And nothing. TypeError: 'UnboundField' object is not callable.

And one more attempt:

modal.querySelector('#add').addEventListener('click', async (ev) => {
    // At this point I'm sending ajax-request to call Form.add_field(7)
    // on the Python side, bu as result I got {'field': 'value', 'field7': None}.
    modal.querySelector(/*Place to insett*/).before(/*new field from <template>*/);
});

What am I missing?

I've read a lot of topics about this, but I didn't get it.


Solution

  • The following solution uses a FieldList with at least one field. Within the list, the ID and name attributes are supplemented by a consecutive number.
    Using JavaScript, the last group consisting of label and field can now be cloned. The attributes can be extracted, the number increased and the new group element added.
    The inputs can be queried using the data attribute of the FieldList as well as by iterating the fields it contains.

    from flask import (
        Flask,
        render_template,
    )
    from flask_wtf import FlaskForm
    from wtforms import FieldList, StringField, SubmitField 
    
    app = Flask(__name__)
    app.secret_key = 'your secret here'
    
    class MyForm(FlaskForm):
        fields = FieldList(StringField('Field'), min_entries=1)
        submit = SubmitField('Submit')
    
    @app.route('/', methods=['GET', 'POST'])
    def index():
        form = MyForm()
        if form.validate_on_submit(): 
            print(form.fields.data)
        return render_template('index.html', **locals())
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Index</title>
    </head>
    <body>
    
        <form method="post">
            {{ form.hidden_tag() }}
            {% for field in form.fields -%}
            <div>
                {{ field.label() }}
                {{ field() }}
            </div>
            {% endfor -%}
            {{ form.submit() }}
        </form>
        <button id="add" type="button">Add</button>
    
        <script>
            (function() {
                document.getElementById('add').addEventListener('click', () => {
                    const fieldGroup = document.querySelector('form div:last-of-type')
                    const newFieldGroup = fieldGroup.cloneNode(true);
                    newFieldGroup.querySelectorAll('input[name^="fields-"]').forEach(field => {
                        const nameAttr = field.name, 
                            newNameAttr = nameAttr.replace(
                                /^fields-(\d+)$/, 
                                (match, p1) => `fields-${parseInt(p1)+1}`
                            );
                        field.id = field.name = newNameAttr; 
                        field.value = '';
    
                        const label = newFieldGroup.querySelector(`label[for="${nameAttr}"]`);
                        label.setAttribute('for', newNameAttr);
                    });
                    fieldGroup.after(newFieldGroup)
                });
            })();
        </script>
    </body>
    </html>