Filtering HX-Trigger Server Events In HTMX And ColdFusion
When building Single-Page Applications (SPA) with Angular and ColdFusion, one common pattern that I used was to return an event
in every mutation-based API response. This event was then triggered on a client-side event-bus; and, various Angular components could respond to it as needed. In an HTMX multi-page application (MPA), we can use the HTTP response header, HX-Trigger
, to achieve similar outcomes. And, we can use the filtering capabilities of the client-side hx-trigger
attribute to limit the scope of this client-side response.
Using the HTMX hx-trigger
attribute, we can define which events trigger an AJAX request. For example, we can bind mouseenter
on a button
to be the trigger for the associated hx-get
request:
<button hx-trigger="mouseenter" hx-get="...">
Trigger events can be filtered using [filter]
notation. The contents within the [
and ]
characters are passed to an eval()
call internally; and, if the evaluation returns false
, the request is not triggered. Which means we can disable the above call by adding [false]
:
<button hx-trigger="mouseenter[false]" hx-get="...">
Obviously, this is a nonsense example; but, it gives us the mechanics to filter any event for any reason, including the ability to filter on the .details
property of a server event delivered via the HX-Trigger
HTTP response header.
To explore this idea, I've created a simple api.cfm
ColdFusion end-point that accepts an i
parameter and then does nothing but return an HX-Trigger
header with the same i
value echoed in the event details:
<cfscript> | |
param name="form.i" type="numeric"; | |
// Return an event that outlines which button was pressed (based on "i"). | |
header | |
name = "HX-Trigger" | |
value = serializeJson({ | |
buttonClicked: { | |
i: val( i ), | |
timestamp: getTickCount() | |
} | |
}) | |
; | |
</cfscript> |
The HX-Trigger
header can take several forms, the most robust of which is a stringified object in which each key is an event name and each value is an object that gets appended to the propagated client-side event.details
property. In the above API response, we'll be able to inspect the event.details.i
property to see which client-side element triggered the request.
On the client-side, I'm using an index-loop (with i
as the iteration variable) to output a list of buttons. Each button will trigger a request to the above API end-point upon click:
<button | |
hx-post="./api.cfm" | |
hx-vals="#encodeForHtmlAttribute( serializeJson({ i: i }) )#" | |
hx-swap="none"> | |
Click #i# | |
</button> |
As you can see, the i
iteration variable becomes the i
expected by the API end-point (sent via the hx-vals
attribute). And, the the i
that is included in the subsequent buttonClicked
event encoded into the HX-Trigger
response header coming back from the ColdFusion server.
When HTMX sees the HX-Trigger
header in the AJAX response, it dispatches the event on the trigger element. We can therefore listen for that buttonClicked
event on a sibling element using something like buttonClicked from:previous button
:
<button /> | |
<span | |
hx-get="./clicked.cfm?from=previous" | |
hx-trigger="buttonClicked from:previous button" | |
></span> |
HTMX uses the Document Object Model's (DOM) native event system to dispatch the returned event. Which means that DOM is the event bus; and, the event bubbles up through the DOM eventually reaching the body
. We can therefore listen to the buttonClicked
event from anywhere in the DOM by using the from:body
modifier:
<span | |
hx-get="./clicked.cfm?from=body" | |
hx-trigger="buttonClicked from:body" | |
></span> |
This will listen for any buttonClicked
event returned to any button
. But, as we discussed above, we can filter the event based on the event details. For example, we can listen for events triggered specifically by the 3rd button by appending a [filter]
clause to the buttonClicked
event name:
<span | |
hx-get="./clicked.cfm?from=body" | |
hx-trigger="buttonClicked[ event.detail.i === 3 ] from:body" | |
></span> |
Now, the associated hx-get
will only trigger when the 3rd button triggers an API call. This clicked.cfm
is just a simple API end-point in its own right that returns some text:
<cfscript> | |
param name="url.from" type="string"; | |
</cfscript> | |
<cfoutput> | |
Clicked (from: #encodeForHtml( url.from )#)! | |
</cfoutput> |
Putting this all together, we have a list of buttons that trigger an API call. This API call returns an HX-Trigger
event header which is consumed on the client-side by two different spans. One span will trigger its own hx-get
when the sibling button emits the buttonClicked
event; and, the other span will trigger its own hx-get
when the buttonClicked
event bubbles up to the DOM:
<cfoutput> | |
<h1> | |
Filtering Server Events | |
</h1> | |
<!--- | |
Since HTTP response header HX-Trigger events are triggered on the initiating | |
element, they bubble up through the DOM to the body. Which means we can listen for | |
events anywhere in the DOM hierarchy (without having to use "from:"). | |
---> | |
<ul hx-on:button-clicked="console.log( 'Clicked!', event )"> | |
<cfloop index="i" from="1" to="5"> | |
<li> | |
<!--- | |
The api end-point responds with an HX-Trigger header which will emit a | |
"buttonClicked" event on the client. The event contains the "i" that | |
was used to trigger the API call. | |
Note: The hx-vals attribute includes the parameters in the form POST | |
body, not the URL search parameters. | |
---> | |
<button | |
hx-post="./api.cfm" | |
hx-vals="#encodeForHtmlAttribute( serializeJson({ i: i }) )#" | |
hx-swap="none"> | |
Click #i# | |
</button> | |
<!--- This listens for "buttonClicked" on the PREVIOUS BUTTON. ---> | |
<span | |
hx-get="./clicked.cfm?from=previous" | |
hx-trigger="buttonClicked from:previous button" | |
hx-sync="this" | |
style="color: darkred ;" | |
></span> | |
<!--- | |
This listens for "buttonClicked" on the BODY but filters the event | |
based on the "i" in the event details. This way, this hx-get is only | |
triggered by the associated button (and not all buttons that trigger | |
this event on the body). | |
---> | |
<span | |
hx-get="./clicked.cfm?from=body" | |
hx-trigger="buttonClicked[ event.detail.i === #i# ] from:body" | |
hx-sync="this" | |
style="color: darkcyan ;" | |
></span> | |
</li> | |
</cfloop> | |
</ul> | |
<button hx-on:click="reset()"> | |
Reset Spans | |
</button> | |
<script type="text/javascript"> | |
function reset() { | |
for ( var node of htmx.findAll( "li span" ) ) { | |
node.innerHTML = ""; | |
} | |
} | |
</script> | |
</cfoutput> |
Each span (per iteration) is programmed to only trigger based on the local buttonClicked
event. One applies this limitation by using a local CSS selector; and the other applies this limitation by using the event filtering mechanics. And, when we run this HTMX and ColdFusion demo, clicking the buttons gives us the following output:

As you can see, even though we have 5 span
elements that all listen for the buttonClicked
event using from:body
, each span
only triggers a fetch relevant to the current iteration thanks to the [filter]
mechanics that matches against the i
iteration variable.
This kind of approach will be very useful if I need to re-fetch a page's HTML content, such a "detail" page, based on a given event; but, only in the case in which the "detail ID" matches the ID embedded within the HTTP HX-Trigger
event header.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️