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.