Development with htmx focuses on generating content server-side and replacing portions of the existing page (or the entire content) with the result, with no build step and no additional JavaScript apart from the library itself. The only JavaScript required is whatever the application may need for interactivity on the page. This can be tricky, though, when the front-end uses a library that requires a JavaScript initialization call; how do we know when (or how, or whether) to initialize it?
The answer is HTML Events. For regular page loads, the DOMContentLoaded
event is the earliest in the page lifecycle where the content is available; references to elements with particular id
s will work. htmx offers its own events, and the one that corresponds to DOMContentLoaded
is htmx:afterSwap
. (Swapping is the process of updating the existing page with the new content; when it is complete, the old elements are gone and the new ones are available.) The other difference is that DOMContentLoaded
is fired from the top-level Window
element (accessible via the document
element), while htmx:afterSwap
is fired from the body
tag.
All that being said, the TL;DR process is:
- If the page loads normally, initialize it on
DOMContentLoaded
- If the page is loaded via htmx, initialize it on
htmx:afterSwap
and looks something like:
document.addEventListener([event], () => { [init-code] }, { once: true })
When htmx makes a request, it includes an HX-Request
header; using this as the flag, the server can know if it is preparing a response to a regular request or for htmx. (There are several libraries for different server-side technologies that integrate this into the request/response pipeline.) As the server is in control of the content, it must make the determination as to how this initialization code should be generated.
This came up recently with code I was developing. I had a set of contact information fields used for websites, e-mail addresses, and phone numbers (type, name, and value), and the user could have zero to lots of them. ASP.NET Core can bind arrays from a form, but for that to work properly, the name
attributes need to have an array index in them (ex. Contact[2].Name
). To add a row in the browser, I needed the next index; to know the next index, I needed to know how many I rendered from the server.
To handle this, I wrote a function to render a script
tag to do the initialization. (This is in F# using Giraffe View Engine, but the pattern will be the same across languages and server technologies.)
let jsOnLoad js isHtmx = script [] [ let (target, event) = if isHtmx then "document.body", "htmx:afterSettle" else "document", "DOMContentLoaded" rawText (sprintf """%s.addEventListener("%s", () => { %s }, { once: true })""" target event js) ]
Then, I call it when rendering my contact form:
jsOnLoad $"app.user.nextIndex = {m.Contacts.Length}" isHtmx
Whether the user clicks to load this page (which uses htmx), or goes there directly (or hits F5), the initialization occurs correctly.