Returning JavaScript Tags In HTMX And ColdFusion
The primary mechanic of HTMX is swapping out branches of the DOM (Document Object Model) in response to user interactions. Yesterday, however, I had to update a bunch of form inputs based on a <select>
change. I wasn't sure how to do this in the cleanest "HTMX Way". Eventually, I settled upon a happy medium in which I use HTMX to execute one-off JavaScript task via an Out of Band (OOB) swap of a <script>
tag.
By default, HTMX will transclude a <script>
tag into an active HTML document the same way it would transclude any other HTML element. This behavior can be disabled at the root configuration level; but, for the purposes of this demo, we're gong to leverage this behavior to send "commands" down to the browser in a ColdFusion / HTMX response.
Imagine a form in which you have a menu of contacts. And, when a contact is selected, you want to prepopulate a bunch of other inputs in the same form. You could send that data down with the initial response; and then update the inputs using client-side event bindings. But, let's assume that the amount of data would be impractical to preload.
Instead, we're going to use an hx-get
on the <select>
control. The default trigger for a <select>
element is the change
event; so, we don't even need to include an hx-trigger
attribute. When the user changes the state of the select menu, we'll make a request to the ColdFusion server.
Here's our ColdFusion demo page:
<cfoutput>
<h1>
Returning Script Tags In HTMX
</h1>
<form>
<p>
<label>Contact:</label>
<select
name="contactID"
hx-get="index.contact.cfm"
hx-sync="this:replace">
<option value="0">- Select -</option>
<option value="1">Laura Smith</option>
<option value="2">Dan Hook</option>
<option value="3">Kim Barlow</option>
</select>
</p>
<p>
<label>Street:</label>
<input type="text" name="street" />
</p>
<p>
<label>City:</label>
<input type="text" name="city" />
</p>
<p>
<label>State:</label>
<input type="text" name="state" />
</p>
<p>
<label>Zip:</label>
<input type="text" name="zip" />
</p>
<button type="submit">
Submit
</button>
</form>
<div id="runner" style="position: fixed ;">
<!--- I run random script tags returned from HTMX (via OOB swaps). --->
</div>
</cfoutput>
Notice that at the bottom of the page we have an empty DIV [id=runner]
. This will be our sink hole for random <script>
tags returned in HTMX responses. Now, when the user chooses a select menu option, we'll make a request to index.contact.cfm
. The ColdFusion server will then respond with a <script>
tag that does two things:
Encodes the relevant contact data that we want to translude as a JSON payload. Since this is "user generated content" (UGC) being rendered in a script tag, we have to take extra special care to encode the content as JSON on the ColdFusion side; and then, parse the content as JSON on the browser side. If we don't do this, we could open ourselves up to a persisted XSS (Cross-Site Script) attack.
Updates the
.value
of every input that we want to change in response to the select menu option.
Here's the ColdFusion page that serves up this script tag:
<cfscript>
param name="url.contactID" type="numeric";
// Mock data for the demo.
contacts = [
{ street: "1 Docker St", city: "New York", state: "NY", zip: "10110" },
{ street: "2 Kubernetes Row", city: "Beverly Hills", state: "CA", zip: "90210" },
{ street: "3 Yaml Court", city: "Boston", state: "MA", zip: "02510" }
];
nullContact = { street: "", city: "", state: "", zip: "" };
// Get the requested contact.
info = ( contacts[ url.contactID ] ?: nullContact );
// In this end-point, we don't actually want to perform a swap on the triggering
// element (the dropdown of contacts). We only want to execute a side-effect. As such,
// let's override the swap (to none) and return a single OOB (out of band) swap.
header
name = "HX-Reswap"
value = "none"
;
</cfscript>
<cfoutput>
<!---
On the client-side, we have a div [id=runner] whose sole purpose is a place to
dump one-time script tags from OOB responses. HTMX will inject the script tag into
the HTML content of said container; and then, the browser will execute it (note
that this behavior can be DISABLED in the HTMX config if desired).
--->
<div id="runner" hx-swap-oob="true">
<script type="text/javascript">
(() => {
// Move ColdFusion struct into the JavaScript context via JSON
// serialization. Always be sure to ESCAPE user-provided content,
// especially contact that we merge into a script tag. In this case, we're
// using an ESAPI methods to make sure the content is inert. Then, we
// parse it as a JSON payload on the client-side.
var info = JSON.parse( "#encodeForJavaScript( serializeJson( info ) )#" );
for ( var key of Object.keys( info ) ) {
htmx.find( `[name="${ key }"]` ).value = info[ key ];
}
})();
</script>
</div>
</cfoutput>
Notice that one of the things this ColdFusion page does is return an HTTP header:
header name="HX-Reswap" value="none";
This is one of the available HTMX response headers. It tells HTMX not to perform a "main" swap. The default swapping behavior, triggered from a select menu, would be to change the innerHTML
of the select (ie, replace the set of <option>
tags). But, we don't want to change the select—we just want to execute the given script tag. As such, we're telling HTMX not to perform an "in band" swap; and, instead, focus solely on the "out of band" swaps.
This ColdFusion page only performs one out of band swap, which is to replace the innerHTML
of the [id=runner]
element. This swap will transclude our script tag into the live document; which, in turn, will update the .value
of each input in the form.
If we run this ColdFusion and HTMX demo and change the select menu state, we get the following output:

As you can see, whenever the select menu is changed, a network request to the ColdFusion server is triggered via hx-get
. Then, the browser executes the returned script tag and updates the relevant form inputs.
I know this isn't really the "HTMX Way" of updating a page. But, using a script tag to update the form inputs individually felt like less work than submitting the form and swapping-in an entirely new form. So, I don't love it; but, I also don't hate it.
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 →