pythonpython-3.xflaskflask-wtforms

Adding a new subform on click with flask_wtf


I have a form, made with flask_wtf. This form contains multiple fields. One of them is called plantes and is a FieldList of FormField.

class InsertPlantesForm(FlaskForm):
    siterec=SelectField(label='Site de recolte',choices=[],validators=[DataRequired()],render_kw={'class':'search'})

    plantes=FieldList(FormField(plantesPresentesForm),min_entries=1)

    ajoutFlore=submit=SubmitField("Ajout d'une autre plante présente",render_kw={
        'id':'add-plante'})

    submit=SubmitField('Création de la floraison')


class plantesPresentesForm(FlaskForm):
    plante=SelectField(label='Nom de plante',choices=[],validators=[],render_kw={'class':'search'})
    diametre=FloatField('Diametre', validators=[DataRequired()])
    hauteur=FloatField('Diametre', validators=[DataRequired()])
    nom=StringField(label="Nom vernaculaire, si rempli sera utilisé à la place du nom scientifique",render_kw={
        'class':'careful'})
    statutplante=SelectField(label='Statut de la plante',choices=[],validators=[DataRequired()],render_kw={'class':'search'})
    partiezone=SelectField(label='Zone où se trouve la plante',choices=[],validators=[DataRequired()],render_kw={'class':'search'})
    quantite=IntegerField(label="Quantité de plantes présentes",validators=[DataRequired()])

I would like to use the ajoutFlore button in order to add a sub form to the page (so basically a new form in plantes).

Right now, I'm using Flask to append an entry. Basically, I check if the user clicked on the ajoutFlore button. If it's true, I'm adding an entry.

def insertPlantesPresentes():
    form=InsertPlantesForm()
    message=""

    if form.ajoutFlore.data:
        form.plantes.append_entry()
        print(form.plantes.data)

    elif form.validate_on_submit():

        # adding data check to insert data in database

    return render_template('html/plantesPresentes.html', form=form,name="Ajout de plantes sur un site de récolte",
                           message=message)

Unfortunately, this solution reload the page when the user click on the button. This lead to multiple issues concerning Javascript. I would like to find a way to not reload the page and still be able to add a form when the user click on the button. I accept any solution, even if it uses javascript or other framework.

Thank you so much


Solution

  • In the following example, the form fields of the last existing nested form are duplicated via JavaScript and the attributes name, id of the field and the for attribute of the associated label are replaced. To do this, the counter is extracted from the existing attribute and incremented.

    from flask import (
        Flask, 
        render_template, 
        request, 
    )
    from flask_wtf import FlaskForm
    from wtforms import (
        FieldList, 
        FloatField, 
        FormField, 
        IntegerField, 
        SelectField, 
        StringField
    )
    from wtforms.validators import DataRequired
    
    app = Flask(__name__)
    app.secret_key = 'your secret here'
    
    class PlantForm(FlaskForm):
        class Meta:
            csrf = False
    
        plant = SelectField(label='Nom de plante', choices=[('', 'Select any')], validators=[], default='')
        diameter = FloatField('Diametre', validators=[DataRequired()])
        height = FloatField('Diametre', validators=[DataRequired()])
        name = StringField(label="Nom vernaculaire, si rempli sera utilisé à la place du nom scientifique")
        statute_plants = SelectField(label='Statut de la plante', choices=[('', 'Select any')], default='')
        partzone = SelectField(label='Zone où se trouve la plante', choices=[('', 'Select any')], default='')
        quantity = IntegerField(label="Quantité de plantes présentes", validators=[DataRequired()])
    
    class PlantsForm(FlaskForm):
        plants = FieldList(FormField(PlantForm), min_entries=1)
    
    @app.route('/', methods=['GET', 'POST'])
    def index():
        form = PlantsForm(request.form)
        if form.validate_on_submit():
            print(form.plants.data)
        else: 
            print(form.errors)
        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.csrf_token }}
    
            <div id="plants">
                {% for subform in form.plants %}
                <div class="plant" style="display: flex; margin-bottom: 1.3rem">
                    <div style="flex-grow: 1">
                        {% for f in subform -%}
                        <div style="display: flex; flex-direction: column">
                            {{ f.label() }}
                            {{ f() }}
                        </div>
                        {% endfor -%}
                    </div>
                    <div>
                        <button type="button" class="btn-remove">-</button>
                    </div>
                </div>
                {% endfor -%}
            </div>
    
            <div style="display: flex;">
                <div style="flex-grow: 1;">
                    <button type="submit">Submit</button>
                </div>
                <div>
                    <button type="button" id="btn-add">+</button>
                </div>
            </div>
        </form>
    
        <script>
            (function() {
                const plantsDiv = document.getElementById('plants');
    
                // Remove the closest element and any form fields it contains, 
                // as long as at least one element remains.
                const removePlantDiv = (event) => {
                    if (plantsDiv.childElementCount > 1) {
                        event.target.closest('div.plant').remove();
                    }
                };
    
                const addBtn = document.getElementById('btn-add');
                addBtn.addEventListener('click', () => {
                    if (plantsDiv.childElementCount >= 1) {
                        // As long as an element exists, duplicate it.
                        const plantDiv = plantsDiv.lastElementChild, 
                            newPlantDiv = plantDiv.cloneNode(true); 
    
                        // For each contained field, extract the name attribute and generate a new one from it.
                        const fields = newPlantDiv.querySelectorAll('input[name^="plants-"], select[name^="plants-"]');
                        fields.forEach(field => {
                            const nameAttr = field.name, 
                                newNameAttr = nameAttr.replace(
                                    /^plants-(\d+)-(\w+)$/, 
                                    (match, p1, p2) => {
                                        return `plants-${parseInt(p1)+1}-${p2}`;
                                });
    
                            // Set the new attribute for the respective input field and the corresponding label.
                            field.id = field.name = newNameAttr; 
                            field.value = '';
    
                            const label = newPlantDiv.querySelector(`label[for="${nameAttr}"]`);
                            label.setAttribute('for', newNameAttr);
                        });
    
                        // Register an event handler to delete the associated element.
                        const rmBtn = newPlantDiv.querySelector('.btn-remove');
                        rmBtn.addEventListener('click', removePlantDiv);
    
                        // Add the new element.
                        plantsDiv.appendChild(newPlantDiv);
    
                    }
                });
    
                // Register handlers to remove existing elements after clicking the corresponding button.
                document.querySelectorAll('.btn-remove').forEach(btn => {
                    btn.addEventListener('click', removePlantDiv);
                });
            })();
        </script>
    
    </body>
    </html>