Skip to main content
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: James Allen
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: James Allen

Filtering HX-Trigger Server Events In HTMX And ColdFusion

By
Published in ,

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:

Browser capture showing that two different spans show up next to a button when it is clicked.

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel