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! ❤️
Post a Comment →