Overview

I was recently looking to quickly add notifications in an internal tool. Nothing fancy, just a simple toast message I could easily pop up when necessary. Usually this is something like “I didn’t load the resource you wanted to look at because it isn’t available”.

There are a ton of ways to plumb notifications in, but I wanted to write the minimal amount of code possible. Polling, SSE, websockets are all options – and I plan to dig into those at some point! I already knew about the Flask flashing system so I opted to start there and build something out using HTMX hx-swap-oob.

The basic flow is this:

  • During a request, call flash(<some_message>) to generate a notification
  • While processing the response, either the normal template will render the messages OR we will add the messages to the rendered HTML before sending it back to the browser
  • Initialize any new toast elements that show up in the DOM

Message Flashing

This part is pretty simple. During request handling, use the flash function to record a message. The message is added to the session cookie and can be retrieved during template rendering. This is fine for my current use case where I just want to give simple feedback on user actions, not a full blown notification subsystem!

Simple, Contrived Example:

from flask import flash
from uuid import uuid4

from my_app import app  # Just an example so we can pretend to define a route

@app.route('/message')
def message():

    # THIS IS ALL IT TAKES
    flash(f'Random message: {str(uuid4())}')
    # NO REALLY ^^ this becomes a notification for the current user!

    return 'ok'

HTML Rendering

I have hx-boost set on the <body> tag in my base template. There are some elements that are added directly to the document body by 3rd part JS, and these need to be left alone when navigating. So I already have hx-target and hx-select in place in my base template. Since I want to attach new messages to any HTMX request, I can’t rely on my base template.

Instead, I created a new template for my notifications. This template is included in my base template, outside my main content. This allows me to render any notifications during a normal page load, but it will be ignored during an HTMX request.

{% set category_icon_map = {
    "error": "fa-exclamation-triangle",
    "info": "fa-info-circle",
} %}
<div id="alerts" class="toast-container position-absolute top-0 end-0 p-3" style="z-index: 100;"
     hx-swap-oob="afterbegin">
    {% with messages = get_flashed_messages(with_categories=True) %}
        {% for category, message in messages %}
            <div class="toast align-items-center bg-secondary text-white bg-gradient border-0" role="alert"
                 aria-live="assertive" aria-atomic="true">
                <div class="d-flex">
                    <div class="toast-body">
                        <span class="fa fa-s {{ category_icon_map[category] }} fa-lg me-1"></span>
                        <span class="fw-normal">
                        {{ message }}
                        </span>
                    </div>
                    <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
                            aria-label="Close"></button>
                </div>
            </div>
        {% endfor %}
    {% endwith %}
</div>

Yeah yeah ignore the bootstrap stuff ;)

Now that I have notifications being in a standalone template, I can render this template in isolation. This is where hx-swap-oob comes in: I can add a Flask after_request handler to render this template and add it to the response. Since the rendered HTML is at the top level of whatever response is provided, HTMX will see the hx-swap-oob attribute and go update the appropriate element wherever it actually exists in the DOM based on the id. I am using "afterbegin" to append new notifications to the ones already present.

The handler is pretty simple (and probably needs refined):

@app.after_request
def render_messages(response: Response) -> Response:
    if request.headers.get("HX-Request") and response.data.find(b"div id=\"alerts\"") == -1:
        messages = render_template("includes/alerts.jinja2")
        response.data = response.data + messages.encode("utf-8")
    return response

Voila, now I can call flash(<some_message>) during a request and it automatically shows up whether during a normal request or an HTMX request.

Notification Display

Not too tricky, and an area I’d like to improve on. That said… it works and I’ve spent more time on this post than on the actual implementation :P

// Normal page load
$('.toast').map(function (index, element) {
    if (element.classList.contains('hide')) { return; }
    let toast = new bootstrap.Toast(element);
    toast.show();
    setTimeout(function () { element.remove(); }, 15 * 1000);
});

// HTMX requests
htmx.onLoad(function (content) {
    [content].map(el => {
        if (el.classList.contains('toast')) {
            let toast = new bootstrap.Toast(el);
            toast.show();
            setTimeout(function () {
                el.remove();
            }, 15 * 1000);
        }
    });
});

Basically, on a normal page load find any elements with a toast class and init the Toast. On an HTMX request, just inspect the new content and do the same. This way we don’t get old toasts popping up again :D

End

There you have it, stupid simple toast notifications without extra tools/processes/etc. This is definitely not an ideal state, but it works reasonably well now and I can tackle a more elegant solution later. Probably.