Flask: Document with dynamic lines addition using FieldList

This short tutorial shows how to create FieldList in Flask and add new lines dynamically.

Adding with first row

In the first step w will crate a list inside form, and allow user to enter and save the first row.

In the forms.py we should define a nested form, and link it to the main form as FieldList.

class ResTagForm(FlaskForm):
    def __init__(self, *args, **kwargs):
        super().__init__(meta={'csrf': False}, *args, **kwargs)

    ocr = StringField('OCR')


class ResEdForm(FlaskForm):
    # ...
    tags = FieldList(FormField(ResTagForm))

Furhter we extend routing in route.py to add the first entry for the user to enter

@bp.route('/<int:id_pub>/res_new', methods=['POST', 'GET'])
@login_required
def res_new():
    form = ResEdForm()
    form.tags.append_entry()    # add first row
    return render_template('res_ed.html', form=form, pub=pub)

Then we embade the table for lines in the html template:

<form action="" method="post">
{#    ... #}
    <div class="mt-5">
        <table id="tags" class="table table-striped tabele-responsive table-inverse table-sm">
            <thead>
                <tr>
                    <td>OCR</td>
                </tr>
            </thead>
            <tbody>
                {% for tag in form.tags %}
                <tr data-toggle="fieldset-entry">
                    <td>{{ tag.ocr(class_='form-control form-control-sm') }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>

After this step we should have a form for a document wiht ability to enter the first row. To save the data we need to handle it in routes.py eg.:

route.py

def res_new():
    form = ResEdForm()
    if form.validate_on_submit():
        res = Resource(name=form.name.data) # Creating the document
        for tagf in form.tags:  # Saving the lines
            if not tagf.ocr.data.rstrip(): # ommit empty row
                continue
            tag = Tag(ocr=tagf.ocr.data)
            res.tags.append(tag)
        db.session.add(res)
        db.session.commit()
        return redirect(url_for('pub.res_list', id_pub=pub.id))
    # ...

Adding new lines

Add button for adding, and mark, the file list with data-toggle atribute

 <div class="mt-5">
        <div class="text-end">
            <a id="tag-add" href="#"> <span data-feather="plus-circle"></span> Add </a>
        </div>

        <table id="tags" class="table table-striped tabele-responsive table-inverse table-sm">

...
{% for tag in form.tags %}
    <tr data-toggle="fieldset-entry">

implement the add function in the <script> section

       $('#tag-add').click(function () {
            let list = $('#tags');
            let lastrow = list.find("[data-toggle=fieldset-entry]:last");
            let newrow = lastrow.clone(true, true);
            let elem_id = newrow.find(":input")[0].id;
            let elem_num = parseInt(elem_id.replace(/.*-(\d{1,4})-.*/m, '$1')) + 1;
            newrow.attr('data-id', elem_num);
            newrow.find(":input").each(function () {
                console.log(this);
                var id = $(this).attr('id').replace('-' + (elem_num - 1) + '-', '-' + (elem_num) + '-');
                $(this).attr('name', id).attr('id', id).val('').removeAttr("checked");
            });
            lastrow.after(newrow);
        });

Editing

To edit the data we need to load sub forms to the Field list, and for submit update/delete/insert rows. We assume that the order of the item on the list is the same as the one stored in database.

def res_ed(id_res):
    res = Resource.query.get_or_404(id_res)
    form = ResEdForm()
    if form.validate_on_submit():
        # ... Saving the data
        for i, tagf in enumerate(form.tags):
            try:
                res.tags[i].ocr = tagf.ocr.data # update rows
            except IndexError: # add new rows
                if not tagf.ocr.data.rstrip():  # ommit empty row
                    continue
                tag = Tag(ocr=tagf.ocr.data)
                tag.publication = res.publication
                res.tags.append(tag)

        for t in res.tags:  # Delete empty rows (ocr is empty)
            if not t.ocr.rstrip():
                db.session.delete(t)

        db.session.add(res)
        db.session.commit()
        return redirect(url_for('pub.res_list', id_pub=res.publication.id))

    # .... Loading the data

    if not len(res.tags):
        form.tags.append_entry()  # add first empty row if not provided when adding entry
    for t in res.tags:
        tf = ResTagForm()
        tf.ocr = t.ocr
        form.tags.append_entry(tf)
    return render_template('res_ed.html', form=form, pub=res.publication, res=res)