Using HTMX in Flask without Flask-Bootstrap

Posted on Fri 18 December 2020 in Tech • 5 min read

The Backstory - Flask With Minimal Javascript

I've been playing around with Flask recently, which is a Python framework for web applications. Ever since I learned Python in college, it's been the programming language I've had the easiest time using. I know there are problems that people have with Python, but the syntax clicks with me in a way that other languages don't. Since Python is usually not the first thing recommended for web apps, I've tried Javascript over the years but I always end up hating it and feeling like I'm fighting against it instead of using it. While using Flask, my goal has been to write minimal Javascript so I enjoy the process more.

This mindset has led me to use a few different tools to avoid directly touching Javascript. One tool is called HTMX (formerly known as Intercooler.js), which lets you use AJAX commands directly in HTML attributes. I also attempted to use Brython for this, which I think would have worked, but it involved having a whole bunch of Python code directly visible in the browser instead of just passing stuff back to Flask for doing the processing. Here's what one of my HTMX attributes looks like:

<input class="form-control" type="text" name="search" 
placeholder="Begin Typing To Search Exercises..." 
hx-post="/lift_stats_search/" hx-trigger="keyup changed delay:500ms" 
hx-target="#search-results" hx-indicator=".htmx-indicator">

This attribute is taking user input every half-second and POSTing it to a path I've defined in Flask that does a search in my database and returns the results to the HTML section with an id of "search-results". Having a couple HTML attributes that are powered by my python logic makes this really useful for my Javascript avoidance.

One other tool for avoiding Javascript is Bootstrap, which makes it easy to add a couple of lines to your HTML to make the pages look "better than bad". There's a Flask plugin called Flask-Bootstrap that can add this through a base HTML template for even easier use in a Flask site. Some things I didn't realize when installing Flask-Bootstrap are:

  • It's using Bootstrap 3, which is out-of-date
  • It's no longer maintained, so it's not going to get upgraded to Bootstrap 4 (or 5 when released)
  • There's other Flask plugins, like Bootstrap-Flask and Flask-Bootstrap4 (not confusing at all, right?)
  • It's not saving a bunch of effort compared to implementing Bootstrap directly

The Problem - Removing Flask-Bootstrap Broke HTMX

I had my HTMX calls working fine with Flask-Bootstrap. In an effort to upgrade to Bootstrap 4, I tried removing any dependency on Flask-Bootstrap. Overall, this went well, except when it came to my HTMX functions. Any of my HTMX code that was issuing POST commands broke due to a missing CSRF token. Any Flask form that was powered by the Flask-WTF plugin worked fine since I could just add the form.csrf_token as a part of the template. I couldn't figure out a way to add this token to the HTMX POST calls.

What I Tried

I attempted a few things that I thought would help:

  • It looked like the 'X-CSRF-Token' request header was missing, but I couldn't find a way to add a custom header to the HTMX call.
  • I (relucantly) tried using JQuery to modify the call. I still think this might be a viable option, but I don't know enough JQuery to get it to work and I was starting to hate it.
  • I tried modifying the "before_request()" function in my routes.py file to add the token before any requests were made. This method seems to work OK for modifying the response headers after a request, but I couldn't figure out how to add the header to the request itself before it was processed.
  • I exempted CSRF protection from those routes that were receiving the HTMX POST calls. I think this would work, but I didn't like the idea of removing CSRF protection if I could help it.

The Solution - Wrapping In A Form

During this flurry of trying anything I could find, I started putting a bunch of lines of hidden inputs around in my templates that looked like this:

<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>

Just having this hidden input didn't fix anything, but I found that wrapping the search box in a form field (even without a submit button) got me further.

<form>
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
    <h3>Pick An Exercise:</h3>
    <input class="form-control" type="text" name="search" 
    placeholder="Begin Typing To Search Exercises..." 
    hx-post="/lift_stats_search/" hx-trigger="keyup changed delay:500ms" 
    hx-target="#search-results" hx-indicator=".htmx-indicator">
</form>

This made the search box work! However, I had also implemented an HTMX call for each of the results of the search that made them clickable. With the above fix, the results couldn't be clicked. This is what the search results template looks like:

{% for ex in exercises %}
    <tr hx-get="/get_lift_stats/{{ ex.id }}" hx-trigger="click" hx-target="#lift-stats-results">
        <td>{{ ex.id }}</td>
        <td>{{ ex.name }}</td>
    </tr>
{% endfor %}

I tried wrapping the results in individual form tags, but since they are table rows, it didn't seem to work. In semi-frustration, I thought that maybe I should just wrap the whole page in a form. Well, it actually seemed to work! I didn't end up doing the whole page, but I did wrap the search bar, search results, and the pane where the data from clicking each result would go. It ended up like this:

<h1> Lift Stats for {{ current_user.username }} </h1>
<form>
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
    <div class="container">
        <div class="row">
            <div class="col-md-3">
                <h3>Pick An Exercise:</h3>
                    <input class="form-control" type="text" name="search" 
                    placeholder="Begin Typing To Search Exercises..." 
                    hx-post="/lift_stats_search/" hx-trigger="keyup changed delay:500ms" 
                    hx-target="#search-results" hx-indicator=".htmx-indicator">
                <table class="table">
                    <thead>
                        <tr>
                            <th>ID</th><th>Exercise</th>
                        </tr>
                    </thead>
                    <tbody id="search-results">
                        <!-- filled with search results when ready -->
                    </tbody>
                </table>
            </div>
            <div class="col-md-9" id="lift-stats-results">
                <!-- will be filled with lift stats results -->
            </div>
</form>

I'm hoping this might help someone else who is trying to use HTMX with Flask without using Flask-Bootstrap. There's not much information about HTMX, but it's really great for making POST calls without having to write JQuery or Javascript. If you have a better idea of how to implement CSRF protection for these kinds of calls, let me know!

Update: Another Solution

After posting this article to twitter, Adam Johnson suggested using the hx-vals attribute to pass the csrf token. I had to play with the quotes a bit, but I was able to remove the outer form from above and replace it with this:

<input class="form-control" type="text" name="search" 
placeholder="Begin Typing To Search Exercises..." 
hx-post="/lift_stats_search/" hx-trigger="keyup changed delay:500ms" 
hx-target="#search-results" hx-indicator=".htmx-indicator" hx-vals='{"csrf_token": "{{ csrf_token() }}" }'>

This seems to be a much cleaner way of keeping everything contained in the one attribute instead of having a formless-form that encapsulated everything.