htmlpython-3.xflaskhtmx

Bulk submit a table created with jinja2 template to flask


I am trying to allow my users to upload a csv file, edit the contents in a table, and then save the updated contents to my database. I am able to upload the file, generate the table, and edit its contents, but when I try to submit it, the request.form object is empty. Below is a snippet of the relevant code (the rest is essentially from the flask tutorial).

import_data.html


<form method="post">
<table name="import-table" contenteditable="true" border="1" cellspacing="0">
<thead>
  <tr>
  {% for h in headers %}
    <th scope="col">{{h}}</th>
  {% endfor %}
  </tr>
</thead>
<tbody>
  {% for r in dat %}
  <tr name="rows">
    {% for d in r %}
    <td>{{d}}</td>
    {% endfor %}
  </tr>
  {% endfor %}
</tbody>
</table>
  <input type="submit" value="Bulk Import">
</form>

auth.py


import csv
import os
from flask import(
    Blueprint, current_app, flash, g, redirect, render_template, request, session, url_for
)

from flaskr.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')

...


@bp.route('/import_data', methods=('GET', 'POST'))
def import_data():
    if request.method == 'POST':
        print(request.form)
        return redirect(url_for('index'))

    with open('testfile.csv', newline='') as f:
        contents = list(csv.reader(f, delimiter=',', quotechar='|'))

    return render_template('auth/import_data.html', headers=contents[0], dat=contents[1:])

I tried adapting the code from htmx's bulk-update example but ended up having the same issue.


Solution

  • The example you used only considers input fields within the table that have a name attribute and a value. Since you're using contenteditable and therefore don't use any input fields, the form data remains empty.

    However, if you intercept the corresponding event, you can read and transfer the table rows and columns with just a few lines of JavaScript.

    from flask import (
        Flask, 
        render_template, 
        request
    )
    
    app = Flask(__name__)
    app.secret_key = 'your secret here'
    
    @app.route('/')
    def index():
        head = ('firstname', 'lastname',)
        data = (
            ('Bruce', 'Wayne'), 
            ('Clarke', 'Kent'), 
            ('Peter', 'Parker'), 
        )
        return render_template('index.html', **locals())
    
    @app.post('/import-data')
    def import_data():
        print(request.form)
        head = ('firstname', 'lastname',)
        data = tuple(zip(*[request.form.getlist(f'{h}[]') for h in head]))
        print(data)
        return 'Update finished'
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Index</title>
        <style>
            #toast.htmx-settling {
                opacity: 100;
            }
    
            #toast {
                opacity: 0;
                transition: opacity 3s ease-out;
            }
        </style>
    </head>
    <body>
        <form
            hx-post="/import-data"
            hx-swap="innerHTML settle:3s"
            hx-target="#toast"
            hx-on::config-request="collectRows(event)"
        >
            <table>
                <thead>
                    <tr>
                        {% for col in head -%}
                        <th>{{ col }}</th>
                        {% endfor -%}
                    </tr>
                </thead>
                <tbody>
                    {% for row in data -%}
                    <tr>
                        {% for col in row -%}
                        <td name="{{head[loop.index0]}}[]" contenteditable>{{ col }}</td>
                        {% endfor -%}
                    </tr>
                    {% endfor -%}
                </tbody>
            </table>
            <input type="submit" value="Bulk update" />
            <output id="toast"></output>
        </form>
    
        <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
        <script>
            function collectRows(event) {
                const formData = new FormData()
                const rows = event.target.querySelectorAll('tbody > tr > td[name]');
                rows.forEach(td => {
                    formData.append(td.getAttribute('name'), td.innerText.trim());
                });
                event.detail.parameters = formData;
            }
        </script>
    </body>
    </html>