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
.valueof 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 →