It's Very Sucky!
January 8, 2025
We don't often add a new tool to our development stack, but when we do it's htmx.
Some years back we started a side project to implement a RESTful api and corresponding web application. Knowing virtually nothing of how the web worked, we remember being shocked and outraged that browsers would not, and would probably never, implement PUT and DELETE. There were some reasonable arguments put forth at the time as to why this should be so, and perhaps they are still reasonable. Anyway, we took the not uncommon path of putting hidden values our html forms for the real method we wanted the server to process. Then one day, around the summer of 2020, we ran into this mild-mannered, unassuming professor in Bozeman who mentioned he was the developer of a JavaScript project called htmx that, what do you know, provided a way for the browser to do PUTs and DELETEs. What are the odds! We immediately investigated htmx and made a branch in our project to start playing around with it. More pressing work soon got in the way, and the effort was abandoned for five years. Now however we are back looking at htmx, which has gone from internet obscurity to Github sensation in the intervening years. And the mild-mannered professor has transformed into a Montana wild man.
Deceptively simple, htmx is actually quite complex. There are many ways to do things. We need to extract some principles or techniques and narrow down the number of features, parameters, and options that we are going to use.
There is this distinction between in-band and out-of-band swaps, which we find not particulary useful. More useful is the distinction between request-driven and response-driven swaps. The attributes hx-swap, hx-target, hx-select, and hx-select-oob are all request-driven. These sit in the HTML in the DOM in the browser along side hx-post
and its companions. They are known to the browser before the request is fired off, and instruct htmx how to handle the response.
On the other hand, hx-swap-oob is a response-driven attribute. It has no purpose before the request. This attribute is received in the response body in the HTML, processed by htmx, stripped out of the HTML, and then thrown away never to be seen again. In this sense it functions like a response header. In fact it could easily be implemented as a response header that contained a list of ids.
Then there are actual htmx response headers that convert a request-driven swap to a response-driven swap. HX-Reswap
, HX-Retarget
and HX-Reselect
let the server overide the values of hx-swap
, hx-target
, and hx-select
respectively.
In addition there is a core htmx extension named Response Targets. This supports attributes in the form of hx-target-[code]
where [code]
is a three digit HTTP response code. This provides another way to implement response-driven swaps.
So response-driven swaps can be implemented either as header-driven overrides or as content-driven HTML, or using the Response Targets extension or some unholy mixture of the three.
The single most obvious advantage of response-driven swaps is that they are dynamic; the server receives a request and can define what gets swapped where back on the client, depending on whatever is appropriate at the moment. The response can contain zero, one or many HTML fragments to be swapped in.
The advantage of relying only on hx-swap-oob
for swaps is that all the logic is in one place. We know exactly what is going to happen by looking at the content of the response. There is no need to inspect the response headers or the client side HTML.
Otherwise there is potentially logic in three places: the HTML in the browser, the headers in the response, and the content of the response. All of these directives scattered about can interact and conflict. There are even configuration settings to tweak the default behavior.
It is possible to use hx-select-oob
to somewhat mimic the flexibility of hx-swap-oob
. We can specifiy multiple ids, and the server can respond with zero of more fragments with those ids. In this case the client must know ahead of time the set of ids that might be swapped, but the server is free to respond with none, some, or all.
Now let's say we want to take it up a notch and add a little JavaScript. On the client side we could use hx-on* to fire off a JavaScript snippet after the request. Or, on the server side, we can use one of the trigger response headers, HX-Trigger
, HX-Trigger-After-Settle
, or HX-Trigger-After-Swap
to fire an event when response is received. Alternatively, we can just include a script element in the response.
In other words, hx-swap-oob
can replace using all of:
hx-swap
hx-target
hx-select
hx-select-oob
HX-Reswap
HX-Retarget
HX-Reselect
HX-Trigger*
hx-target-[code]
hx-on*
htmx.config.[various]
No doubt there are use cases for all of these attributes and headers, but if one is in total control of the back end, it seems simpler to just use hx-swap-oob
.
Let's make all this a little more concrete. Consider a button that fires off a POST. The basic pattern is a request-driven swap using hx-target
:
b←New 'button'
b.hx_post←'/SomeURL'
b.hx_target←'#MyId'
Let the response body be:
<div>
This is the response.
</div>
The response body is swapped into the element with id MyId
. There is only one target element, and the entire body of the response is swapped in to this target. The target must be known and specified before the request is sent off.
Multiple elements may be swapped in to different locations by using hx-select-oob
:
b←New 'button'
b.hx_post←'/SomeURL'
b.hx_select_oob←'#MyId1,#MyId2,#MyId3'
Let the response body be:
<div id=MyId1>One</div>
<div id=MyId2>Two</div>
<div id=MyId3>Three</div>
These are called out-of-band swaps. The request is again driving the swap. It specifies a list of ids that may be swapped. In order to swap an element, the response must have an element with a corresponding id. The response does not need to contain all or indeed any of the ids specified in hx-select-oob
If there is no corresponding id in the response, it's a no-op. This allows for a little bit of response-driven swapping. The server decides what it wants to swap, but within the universe of ids explicitly specified by the request.
A basic in-band swap may be combined with an array of out-of-band swaps, using hx-target
and hx-select-oob
together.
We can move the explicit list of ids for out-of-band swaps from the client to the server by using hx-swap-oob
in the response:
b←New 'button'
b.hx_post←'/SomeURL'
Let the response body be:
<div hx-swap-oob='true' id=MyId1>One</div>
<div hx-swap-oob='true' id=MyId2>Two</div>
<div hx-swap-oob='true' id=MyId3>Three</div>
Now the server has complete control over what gets swapped. The server can send any set of ids in the response. We can combine hx-target
and hx-select-oob
on the client side, with hx-swap-oob
in the response to... confuse ourselves.
So for now, our strategy is to exclusively use hx-swap-oob
, use script tags to execute a little JavaScript if necessary, and to configure htmx to swap on any and all response codes.