Retrieve hx-include value from HTMX request with Flask

Question:

I’m trying to send a client side value to the server when a HTMX request is made. I can’t seem to get the value using request.args.get('cardCount')or request.form.get('cardCount').

I’m using flask_htmx to catch HTMX requests server side and render partial html. I need to get the cardCount from the dom so I can increment the new card being added correctly. If I don’t add ‘cards-3-front’ and ‘cards-3-back’ to the html id and name the wtforms FieldList will not save dynamically added textarea content to the db.

Here is the form:

 # NOTECARD FORMS
class CardForm(Form):
    front = TextAreaField('front', validators=[InputRequired()])
    back = TextAreaField('back', validators=[InputRequired()])

class NoteForm(FlaskForm):
    title = StringField('Title', validators=[InputRequired(), Length(max=35)])
    cards = FieldList(FormField(CardForm), min_entries=3)

Here is the view:

@views.route('/create', methods=['GET', 'POST'])
@login_required
def create():
    user_img = get_user_img()
    form = NoteForm()
    if htmx:
        count = request.args.get('cardCount')
        return render_template("./partials/notecard.html", count=count)
    if request.method == 'POST':
        if form.validate_on_submit():
            fronts = [bleach.clean(card.front.data) for card in form.cards]
            backs = [bleach.clean(card.back.data) for card in form.cards]
            set = dict(zip(fronts, backs))
            db.session.add(Notes(title=bleach.clean(form.title.data), content=set, user_id=current_user.id))
            db.session.commit()
            flash('New Notecard Set Created!', 'success')
            return redirect(url_for('views.dashboard'))
        else:
            flash('All notecard fields must be filled in', 'warning')
    return render_template("create.html", user_img=user_img, form=form)

Here is the htmx request I’m trying to send from the template:

<div name="cardCount">3</div>

<button hx-get="/create" hx-include="[name='cardCount']" hx-target="#newCards" hx-swap="beforeend" class="btn"></button>
Asked By: Rug

||

Answers:

It seems to me that your approach using hx-include fails due to the fact that you want to include the content of a div element and not the value of an input element.

Below is an example where you can add and remove form fields using htmx. The entire form is replaced by an htmx call.

I use an input field of type hidden to determine the number of input fields.
To determine whether fields should be added or removed, an additional header is used using hx-headers.
Depending on the data transmitted via htmx, the form is changed and replaced as a whole, with the entries made being retained.

Flask (app.py)
from flask import (
    Flask, 
    redirect,
    render_template, 
    request, 
    url_for
)
from flask_htmx import HTMX
from flask_wtf import FlaskForm, Form
from wtforms import (
    FieldList, 
    FormField, 
    StringField, 
    TextAreaField
)
from wtforms.validators import (
    DataRequired, 
    Length
)

app = Flask(__name__)
app.secret_key = 'your secret here'
htmx = HTMX(app)

class CardForm(Form):
    front = TextAreaField('Front', validators=[DataRequired()])
    back = TextAreaField('Back', validators=[DataRequired()])

class NoteForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired(), Length(min=3, max=35)])
    cards = FieldList(FormField(CardForm), min_entries=1)

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NoteForm(request.form)
    num_fields = max(0, request.form.get('count', 1, type=int))
    if htmx:
        form.validate()
        hx_func = request.headers.get('Hx-Func', 'DEL')
        if hx_func.lower() == 'del':
            num_fields = max(1, num_fields - 1)
            form.cards.min_entries = num_fields
            form.cards.entries = form.cards.entries[:form.cards.min_entries]
        else:
            num_fields += 1
            form.cards.min_entries = num_fields
            while len(form.cards.entries) < form.cards.min_entries: 
                form.cards.append_entry()
        return render_template('partials/form.html', **locals())
    else: 
        if form.validate_on_submit():
            print(form.title.data, form.cards.data)
            # Use the form data here.
    return render_template('index.html', **locals())
HTML Template (index.html)
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Index</title>
</head>
<body>
    {% include 'partials/form.html' %}
    <script src="https://unpkg.com/[email protected]"></script>
</body>
</html>
HTML Template (partials/form.html)
<form name="my-form" method="post">
    {{ form.csrf_token }}

    <div>
        {{ form.title.label() }}
        {{ form.title() }}
        {% if form.title.errors -%}
        <ul>
            {% for error in form.title.errors -%}
            <li>{{ error }}</li>
            {% endfor -%}
        </ul>
        {% endif -%}
    </div>

    {% for subform in form.cards -%}
            {% for field in subform -%}
            <div>
                {{ field.label() }}
                {{ field() }}
                {% if field.errors -%}
                <ul>
                    {% for error in field.errors -%}
                    <li>{{ error }}</li>
                    {% endfor -%}
                </ul>
                {% endif -%}
            </div>
            {% endfor -%}
        {% endfor -%}

    <input type="hidden" name="count" value="{{ num_fields }}" />
    <button 
        type="button" 
        hx-post="/" 
        hx-target="[name='my-form']" 
        hx-headers='{"Hx-Func": "DEL"}'
        hx-swap="outerHTML"
    >Less</button>
    <button 
        type="button" 
        hx-post="/" 
        hx-target="[name='my-form']" 
        hx-headers='{"Hx-Func": "ADD"}'
        hx-swap="outerHTML"
    >More</button>
    <button type="submit">Submit</button>
</form>
Answered By: Detlef
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.