Using A No-Content URL To Help Manage DOM Caching In HTMX And ColdFusion
When you hx-boost
/ AJAX'ify your ColdFusion application with HTMX, HTMX will cache the state of the DOM (Document Object Model) as you navigate from page to page. This way, when you hit the browser's back button, HTMX can restore the previous DOM state, pulling it out of the LocalStorage
API. The caveat being, HTMX only does this in response to an AJAX request. Which means, if we want to cache the current state of the DOM, we must issue an AJAX request and we must change the URL.
Of course, we don't always want to navigate away from the current page to do this. Consider the example of closing a modal window. Since the vast majority of modal windows should be deep linkable within our applications, closing a modal window should update the URL to remove the modal window and expose the main page below. And then, hitting the browser's back button should re-render the modal window.
But, this back-button mechanic only works if HTMX took a snapshot of the DOM prior to closing the modal window. And, HTMX will only take a snapshot of the DOM after an AJAX request is issued. Which means, in order for this to work in the most natural way possible (from the user's perspective), closing the modal window has to be managed by an AJAX request/response life-cycle.
This sounds complicated; but, we can keep it relatively simple by creating a ColdFusion end-point that does nothing and can be cached by the browser. Here's my noContent.cfm
CFML page. It does nothing and can be cached for a month (including a stale-while-revalidate
grace period):
<cfscript> | |
HOUR_SECONDS = ( 60 * 60 ); | |
DAY_SECONDS = ( HOUR_SECONDS * 24 ); | |
WEEK_SECONDS = ( DAY_SECONDS * 7 ); | |
MONTH_SECONDS = ( WEEK_SECONDS * 4 ); | |
// Whatever unique URL we use for this end-point, it's going to be cached. As such, | |
// the first request to it may take some time; but, every subsequent request to the | |
// same URL signature will be instantaneously pulled from the browser cache. | |
header | |
name = "Cache-Control" | |
value = "max-age=#MONTH_SECONDS#, stale-while-revalidate=#MONTH_SECONDS#" | |
; | |
// ------------------------------------------------------------------------------- // | |
// ------------------------------------------------------------------------------- // | |
// These will likely be handled by the HX-attributes on the client; but, I'm allowing | |
// overrides to be passed-in on the URL just in case. | |
param name="url.reSwap" type="string" default=""; | |
param name="url.reTarget" type="string" default=""; | |
param name="url.reSelect" type="string" default=""; | |
if ( url.reSwap.len() ) { | |
header | |
name = "HX-Reswap" | |
value = url.reSwap | |
; | |
} | |
if ( url.reTarget.len() ) { | |
header | |
name = "HX-Retarget" | |
value = url.reTarget | |
; | |
} | |
if ( url.reSelect.len() ) { | |
header | |
name = "HX-Reselect" | |
value = url.reSelect | |
; | |
} | |
// ... this page serves no content. ... // | |
</cfscript> |
This ColdFusion page serves no content. It only exists to allow the URL to be updated via AJAX; and, when pulled from the cache, this update happens instantaneously.
This no-content end-point has value because HTMX allows us to override the experience of the interaction using hx-*
attributes. So, while we might make the AJAX request to noContent.cfm
behind the scenes, we can use the hx-push-url
attribute in the HTML to alter the URL that goes into the history API.
Going back to our modal window scenario, we could make a request to the noContent.cfm
page, but use the hx-push-url
attribute to simply strip-out the deep-link, modal-window flag that's currently in the URL. I intent to cover that specific scenario in a future blog post; but for now, let's keep things simple.
To demonstrate the power of the noContent.cfm
end-point, I've created a ColdFsion page that has a number of buttons. Each button:
Triggers an AJAX request to the
noContent.cfm
page.Supplies a new, unique URL to be pushed into the browser history via an
hx-push-url
attribute.Uses an
hx-swap="outerHTML"
attribute to remove the button from the DOM.
This combination of actions means that every time we remove a button from the DOM, HTMX with snapshot the DOM into the LocalStorage
API. And then, will restore said DOM snapshot state when we use the browser's pop-state operations (hitting either the Back or Forward buttons).
<cfoutput> | |
<body> | |
<div class="buttons"> | |
<cfloop index="i" from="1" to="12"> | |
<!--- | |
Our HX-GET attribute points to our no-op end-point that is cached in the | |
browser. And, our HX-PUSH-URL attribute tells HTMX to push a URL into the | |
history API such that the current state of the DOM is cached before the | |
swap takes-place. | |
---> | |
<button | |
hx-get="noContent.cfm" | |
hx-push-url="#cgi.script_name#?removed=#i#" | |
hx-swap="outerHTML"> | |
Remove (#i#) | |
</button> | |
</cfloop> | |
</div> | |
<script type="text/javascript"> | |
// By default, the DOM snapshot history length is 10. For this demo, let's | |
// increase it since we have more buttons. | |
htmx.config.historyCacheSize = 20; | |
</script> | |
</body> | |
</cfoutput> |
If we load this ColdFusion page and click to remove each button, we can see that the browser's back button will then restore each button in turn:

As you can see from the network activity, the browser's natural caching behaviors are in place; and each request to the noContent.cfm
end-point is materialized in 1ms, being pulled from cache. This is, from the user's perspective, an instantaneous amount of time; but, it's enough to get HTMX to cache the DOM state. As such, when we click the browser's back button, HTMX will restore the previous DOM state, including the button that we just removed.
In this demo, we're not "deep linking" to the state of the page in which a given set of buttons have been removed. But, this hopefully illustrates how such a "no op" end-point could be used to surgically apply the current page state to the DOM snapshotting provided by HTMX. In a follow-up post, I'll look at this in a modal-window scenario that should make this mechanism even more clear.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️