Smarter Templating with HTMX and Flask

Published: 2023-06-22 12:00 AM

Category: Code | Tags: HTMX, flask, python, tutorial, pattern, example, demo


I've been using HTMX for a couple years now and I'm still loving the patterns and interaciton it allows without relying on writing Javascript for everything. The developer experience is great and I can just write HTML and CSS templates to make everything work nicely.

HTMX works by sending AJAX requests and then dynamically inserting the responses into your page so it feels like a SPA without relying on weird Javascript syntax to make it all work. The server handles all of the requests and spits out HTML (which is the way things should work).

There are two other libraries I use to make things easier in development:

  • The Flask-HTMX library makes it super easy to check if something is an AJAX request from HTMX or if it's a browser action.
  • Jinja Partials by Michael Kennedy allows you to reuse small blocks of HTML, almost like a component library.

The problem that comes up has to do with browser refreshes - since most routes return partial HTML, a browser refresh on a particular route will return unstyled HTML. That's bad.

In order to prevent this, I came up with the following pattern:

example_template

<div>
    <h1>{{ name }}</h1>
    <p>{{ text }}</h1>
</div>

flask route

@app.route('/example')
def example():
    template = "example_template.html"
    resp_data = {
        "id": 1,
        "name": Example 1,
        "text": "This is some string text to render."
    }

    # Flask-HTMX creates a property on the request object if an HX-* header is present.
    # If it is present, render the partial template directly and let HTMX insert it into the DOM.
    if request.htmx:
        resp = render_template(template, **resp_data)
    else:
        # If it is not present, render the partial template in a full-page wrapper to include all CSS and script tags again
        resp = render_template(
            "shared/layout_wrapper.html",
            partial=template,
            data=resp_data
        )

    return resp

If a request comes from HTMX, then return the template partial as expected. If it's not from HTMX - in other words, a browser action of some sort - then wrap the response in a template and return the wrapped response. Here's the wrapper:

layout_wrapper.html

{% extends '_layout.html' %}
{% block main_content %}

<!-- render_partial takes the passed template and extracts the data dict into the template variables -->
{{ render_partial(partial, **data) }}

{% endblock %}

This all works well and good, but now it means that my routes all have the if...else... block as part of the function. It's super repetitive and I felt like there had to be a better way.

Decorators

Since the response needs to be modified before rendering, flask.after_request() wouldn't work because there response already has the built HTML string. This solution uses a decorator to modify the response before returning the function results.

The Flask docs actually include a templating decorator example that got me most of the way there. Now, instead of using an if...else... inside the view, I can wrap any refreshable route with a decorator which will then return the appropriate response:

wrapper.py

from functools import wraps
from flask import request, render_template

def templated(template=None):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            template_name = template
            # Catch the data returned by the view
            ctx = f(*args, **kwargs)
            if request.htmx:
                resp = render_template(template_name, **ctx)
            else:
                resp = render_template(
                    "shared/layout_wrap.html",
                    partial=template_name,
                    data=ctx
                )
            return resp
        return decorated_function
    return decorator

flask route

from wrappers import templated

@app.route('/example')
@templated(template="example_template.html")
def example():
    resp_data = {
        "id": 1,
        "name": Example 1,
        "text": "This is some string text to render."
    }

    return resp_data

Each route shrinks significantly. The added benefit is that the returned template is defined right at the top of the route and isn't dependent - in this view - on where the request came from, the right one is returned either way.

It's a small change, but the reusability of the decorator paired with the template patterns have made my code more consice and more readable in general. Plus, it's just cool to be able to make these quality of life improvements as I learn more.

Share this post
Previous: My Best Getting Started Strategy Next: Humanize the LMS with Feedback

Comments