Skip to main content
Ben Nadel at BFusion / BFLEX 2010 (Bloomington, Indiana) with: Ben Dalton and Simon Free
Ben Nadel at BFusion / BFLEX 2010 (Bloomington, Indiana) with: Ben Dalton Simon Free

ColdFusion And HTMX Contact App

By
Published in , , Comments (11)

A couple of weeks ago, I read Hypermedia Systems by Carson Gross. The book is an introduction to HTMX - a JavaScript library that extends the HTML specification and brings dynamic single-page application (SPA) type features to a traditional multi-page application (MPA) architecture. Some of the aspects of HTMX are fairly simple; but, some require a sizable mindset shift. As such, I thought it would be helpful (for me) to take the "contacts app" that Carson builds in the book and translate it into a ColdFusion application context.

View this code in my ColdFusion + HTMX Contacts App project on GitHub.

In the book, Carson starts out with a vanilla HTML application. Then, iteratively layers-in one HTMX capability at a time. The book is very well written; and it illustrates the HTMX features quite effective. In my ColdFusion version, I try to take the same path laid out in the book; but, I do diverge from Carson's implementation from time to time. Essentially, I follow the same spiritual steps; but, my personal journey is slightly different due to a variety of subjective choices.

As I was putting my GitHub repository together, I didn't know if I wanted to implement each iteration as a branch or as a separate directory. In the end, I decided to make each iteration it's own directory (with some code shared across all the iterations). I felt that this made it easier to explore - I wasn't having to constantly git checkout different branches to jump between the various implementations.

The downside of making each iteration its own directory is that you can't simply git diff to see what changed in a given iteration. As such, I gave each iteration it's own README.md file to help highlight what I was attempting to do in the given iteration. It leaves a lot to be desired from an educational standpoint; but, seeing as this effort was primarily for my own learning, I accepted the trade-off.

What follows is just a replication of the various README.md files from each iteration.

v1 - The Vanilla ColdFusion Application

This iteration of the ColdFusion + HTMX application provides the pre-HTMX baseline. It's just a vanilla ColdFusion application that provides simple CRUD (Create, Read, Update, Delete) operations on a list of contacts.

See relevant chapter in Hypermedia Systems.

v2 - Initial HTMX Version With hx-boost

This iteration of the ColdFusion + HTMX application installs the htmx JavaScript library and adds hx-boost to the <body> tag. This directive automatically intercepts the link and form posts triggered within the current page and implements them via AJAX, swapping the entire contents of the page (less the <head>) with the ColdFusion server response.

Above and beyond what happens in the book, I also had to override the error handling configuration. In this version of the application, I'm returning 422 status codes (Unprocessible Entity) for model validation errors. By default, htmx ignores non-2xx responses; which means that, by default, our 422 responses were being ignored. I had to explicitly tell htmx to execute a swap on 422:

htmx.config.responseHandling = [
{ code: "204", swap: false },
{ code: "[23]..", swap: true },
// Adding this one to allow 404 responses to be merged into the page.
{ code: "404", swap: true, error: true },
// Adding this one to allow 422 responses to be merged into the page.
{ code: "422", swap: true },
{ code: "[45]..", swap: false, error: true },
{ code: "...", swap: true }
];
view raw snippet-1.js hosted with ❤ by GitHub

I've also added a configuration to tell it to swap on 404 (Not Found) errors. This isn't immediately relevant in this application. However, if you view a contact in a new browser tab, and then delete that contact, attempting to view the contact in the original browser tab will result in a 404 response. I think it makes sense to render this error to the page.

See relevant chapter in Hypermedia Systems.

v3 - Adds hx-delete To The Delete Button

This iteration of the ColdFusion + HTMX application adds the hx-delete action attribute to the Delete Contact button on the Edit page. This removes the need to have the button wrapped in a <form> tag; and, turns the button itself into a hypermedia control.

This is the feature of HTMX that I care for the least. Using the DELETE method makes working with ColdFusion harder. In CFML, it's easy to differentiate between a GET and a POST because they populate two different scopes: url and form, respectively. As such, including submitted flags and verifying XSRF (Cross-Site Request Forgery) tokens, for example, is fairly easy.

Once we introduce the DELETE verb, life gets harder. With a DELETE request, there is currently no request body. Which means that all form values are submitted in the URL and therefore populate the url scope. As such, if we wanted to check for something like a submitted flag, we'd have to do so in the url scope (not the traditional form scope). This, in turn, makes the attack surface a little easier to access.

Now, the counter-argument to this concern is that forms don't currently support DELETE; so, such a request would have to be made via JavaScript; and, a properly configured CORS (Cross Origin Resource Sharing) setting should prevent a malicious off-site DELETE request. But, this feels like a coincidental alleviation, not an intentional one. After all, if a browser were to introduce method="DELETE" support, then suddenly this would be an issue.

To ensure the proper HTTP request, I'm now having to explicitly check to see if the incoming request was performed via DELETE. This is doable; but, it requires more code than simply checking to see if a value was populated in the form scope.

If we had a more robust router in the demo app - one that would only route DELETE requests here - then that might be less of an issue. But, in my app, I use a common post-back pattern; which means that the delete-contact page is also capable of rendering a form (and should also respond to a GET request).

Another issue with this is that custom hypermedia controls don't play well with hx-boost, which we added in the previous iteration. As such, we have to add hx-target and hx-push-url attributes to the button. Ultimately, our button ends up looking like this:

<button
hx-delete="delete.cfm?id=#contact.id#&submitted=true"
hx-target="body"
hx-push-url="true"
hx-confirm="Are you sure you want to delete this contact?"
type="button">
Delete Contact
</button>
view raw snippet-2.cfm hosted with ❤ by GitHub

Long story short, this is not a real value-add feature of the HTMX framework for me. I understand how it makes sense academically; but, I don't feel like it makes sense practically in the way I think about application development. Frankly, I think it makes the code more complicated and cuts against the grain of how CFML wants to work.

Aside: To see how the hx-delete works with error responses, I've gone back and added logic to prevent deletes on any contact whose first name is "Diva" (ex, "Diva Jones"). Since this kind of a test generally makes sense, I've added it to the previous iterations as well.

See relevant chapter in Hypermedia Systems. It's the section labeled, "A Second Step: Deleting Contacts With HTTP DELETE".

v4 - Adds hx-get Realtime Email Validation

This iteration of the ColdFusion + HTMX application adds the realtime email validation to the Add / Edit contact form. It does this by including an hx-get on the email input field which hits a new endpoint specifically created for email validation.

Since the realtime validation works by replacing the contents of a <span> on the page, it works by returning an empty string if the given email is available; or, an error string if the email is already taken by another contact. In both cases, the response HTML of the validation request is transcluded into the rendered page.

Here's the updated form field for the email:

<p class="form-field">
<label for="email">
Email:
</label>
<input
id="email"
type="text"
name="email"
value="#encodeForHtmlAttribute( form.email )#"
hx-get="validateEmail.cfm"
hx-trigger="input delay:200ms"
hx-target="next .inline-error"
/>
<span class="inline-error">
<!--- Populated by the HTMX call above. --->
</span>
</p>
view raw snippet-3.cfm hosted with ❤ by GitHub

Aside: In future iterations, I added hx-sync="this:replace" on the input to quarantine the hx-get request to the input. However, I didn't go back and add it to the previous iterations.

In the book, they use the change event within the hx-trigger attribute. But, I don't quite understand this choice. As such, I've opted to use the input event instead as I believe it accomplishes the same outcome with less complexity.

I've opened a discussion on the GitHub forum to ask about this input vs. change event selection. I've seen this pattern (using the change event) used in more than just this book, even for realtime validation. As such, I wonder if there's some mechanics that I'm just not understanding at this time.

Update: In the GitHub discussion, I was told that using the input is fine; and that the input event is being used more frequently in the HTMX documentation now.

On the add contact page, the validation URL doesn't include a contact ID since we're adding a new contact. However, on the edit page, I'm including the current contact ID in the hx-get attribute so that I can ignore email conflicts on the given record:

  • Add: hx-get="validateEmail.cfm"

  • Edit: hx-get="validateEmail.cfm?id=#contact.id#"

Aside: I originally tried using the hx-include attribute to include the rest of the form inputs along with the hx-get request. However, on the edit page, the contact ID is being defined in the form's action attribute; and parameters encoded into the action attribute don't seem to get included by the hx-include directive.

As an aside, this inline error rendering felt like a good place to progressively enhance with the :has() CSS selector. In my approach, the <span class="inline-error"> starts out as an inline element with no margins and no inherent size. However, if it contains any other element, I'm using :has(*) to change the span to a block element with some top margin:

.inline-error {
color: red ;
font-size: 80% ;
}
.inline-error:has(*) {
display: block ;
margin-top: 0.5rem ;
}
view raw snippet-4.css hosted with ❤ by GitHub

Then, in my email validation endpoint, the error message - if any - is returned in another span:

<span class="inline-error-content">
This email address is already taken.
</span>
view raw snippet-5.cfm hosted with ❤ by GitHub

For browsers that don't support :has(), this is just two inline span tags. However, for browsers that do support :has(), the error message will be rendered as a block element with some additional breathing room.

See relevant chapter in Hypermedia Systems. It's the section labeled, "Next Steps: Validating Contact Emails".

v5 - Adds Pagination To Contacts List

This iteration of the ColdFusion + HTMX application adds basic pagination to the list of contacts. The pagination is driven by a page and pageSize parameters. So as not to break the previous iterations of the application (which did not using pagination), I've created a new search method on the model: getBySearchPagination(). It is not that robust; and merely makes sure that you don't go beyond the bounds of the results.

Since the application is already being augmented via hx-boost, there's nothing else to do to get this to feel snappy. I just added the prev/next links:

<div class="pagination">
<cfif pagination.hasPrevious>
<a href="index.cfm?q=#encodeForUrl( url.q )#&page=#( pagination.page - 1 )#&pageSize=#( pagination.pageSize )#">
&larr; <u>Prev</u>
</a>
<cfelse>
&larr; Prev
</cfif>
<cfif pagination.hasNext>
<a href="index.cfm?q=#encodeForUrl( url.q )#&page=#( pagination.page + 1 )#&pageSize=#( pagination.pageSize )#">
<u>Next</u> &rarr;
</a>
<cfelse>
Next &rarr;
</cfif>
</div>
view raw snippet-26.cfm hosted with ❤ by GitHub

See relevant chapter in Hypermedia Systems. It's the section labeled, "Another Application Improvement: Paging".

v6 - Adds A "load more" Button To The Contacts List

This iteration of the ColdFusion + HTMX application updates the basic pagination of the contacts list to include a "load more" button at the bottom of the list. The load more button will use the existing pagination mechanics; and will take the new <tr> nodes returned in the paged response and append them to the table:

<cfif pagination.hasNext>
<tr>
<td colspan="4">
<button
hx-get="index.cfm?q=...."
hx-target="closest tr"
hx-swap="outerHTML"
hx-select="tbody > tr"
type="button"
class="load-more">
Load More
</button>
</td>
</tr>
</cfif>
view raw snippet-6.cfm hosted with ❤ by GitHub

Notice that the hx-target is the closest tr - it will replace the entire row that contains the button itself. And, the content that it injects is the hx-select="tbody tr.

I don't love that the "load more" button is physically inside the table - this doesn't feel like a semantically correct pattern to me. But, keeping the button in the table makes it easy to both add new rows and to conditionally stop showing the button when there are no more rows to show. Perhaps when I learn more about OOB (out of band) swaps, I'll be able to handle this more elegantly.

I'm leaving the prev/next functionality in place to make this change additive.

See relevant chapter in Hypermedia Systems. It's the section labeled, "Click To Load".

v7 - Adds Infinite Scrolling

This iteration of the ColdFusion + HTMX application updates the "load more" button at the bottom of the list to use the revealed synthetic event. This way, any time the button is in the browser viewport, it will automatically trigger a load of additional rows. This gives us an "infinite scroll" experience with almost no changes to the core mechanics of the pagination.

All I had to do was add the hx-trigger="revealed" to the existing button. I also changed the text to read "Loading more..." since the user should never actually have to click on it.

<button
hx-get="index.cfm?q=....."
hx-trigger="revealed"
hx-target="closest tr"
hx-swap="outerHTML"
hx-select="tbody > tr"
type="button"
class="load-more">
Loading More....
</button>
view raw snippet-7.cfm hosted with ❤ by GitHub

Again, I'm leaving the prev/next functionality in place to make this change additive.

See relevant chapter in Hypermedia Systems. It's the section labeled, "Infinite Scroll".

v8 - Adds Realtime Search

This iteration of the ColdFusion + HTMX application adds realtime search functionality to the contacts list. In addition to the existing click-based form submission (which continues to work even if the HTMX library doesn't load), the contact list results will now be proactively updated as the user types in the search input.

I've removed the "infinite scroll" functionality since I wanted to make sure that the prev/next pagination would work in conjunction with this update.

And, speaking of the prev/next pagination, this demo works by swapping out just the .search-results portion of the page. The prev/next links, however, sit outside this container; and, have to be swapped using an OOB (Out of Band) selection. In the end, here's the new input element:

<input
id="search"
type="text"
name="q"
value="#encodeForHtmlAttribute( url.q )#"
hx-get="index.cfm"
hx-push-url="true"
hx-trigger="input delay:200ms"
hx-target=".search-results"
hx-select=".search-results"
hx-select-oob="pagination"
/>
view raw snippet-8.cfm hosted with ❤ by GitHub

When HTMX makes this request, it will select the .search-results out of the AJAX response and merge them into the .search-results (using innerHTML) that is already rendered on the page. It will also swap the #pagination out of the AJAX response and merge it into the #pagination (using outerHTML) that is already rendered on the page.

Note that the the hx-select-oob has some tighter constraints. First, it must be id-driven - it can't use a generic CSS selector like other HTMX attributes. And second, its default swap strategy is outerHTML, not innerHTML; though, this can be overridden in the hx-select-oob attribute value.

See relevant chapter in Hypermedia Systems. It's the section labeled, "Adding Active Search".

v9 - Adds request.htmx Struct Based On HTTP Headers

This iteration of the ColdFusion + HTMX application adds the request.htmx struct to the request processing. When HTMX makes a request to the server, it will include a subset of HTTP headers that are relevant to the current interaction. I'm setting these up in the onRequest() event handler:

component {
public void function onRequest( required string scriptName ) {
request.htmx = {
boosted: ( safelyGetHeader( "HX-Boosted" ) == "true" ),
currentUrl: safelyGetHeader( "HX-Current-URL" ),
historyRestoreRequest: safelyGetHeader( "HX-History-Restore-Request" ),
prompt: safelyGetHeader( "HX-Prompt" ),
request: ( safelyGetHeader( "HX-Request" ) == "true" ),
target: safelyGetHeader( "HX-Target" ),
triggerName: safelyGetHeader( "HX-Trigger-Name" ),
trigger: safelyGetHeader( "HX-Trigger" )
};
}
}
view raw snippet-9.cfc hosted with ❤ by GitHub

These values can then be used to conditionally render smaller partials for optimized database access and over-the-wire latency. My little app, however, isn't really setup to have this kind of differentiation. As such, I'm adding the above struct, but I'm not consuming it in any way at this time.

See relevant chapter in Hypermedia Systems. It's the section labeled, "HTTP Request Headers In Htmx".

v10 - Adds A Loading Indicator During Search

This iteration of the ColdFusion + HTMX application adds a request indicator to the contact list search form to indicate that a request is currently in-flight. These busy indicators are a first-class citizen of HTMX and can be managed through the htmx-indicator CSS class and / or the hx-indicator attribute.

The hx-indicator attribute uses a CSS selector to point to the element (or elements) to be treated as the request indicators. During the in-flight request workflow, HTMX uses special CSS classes to change the visibility (opacity) of the request indicator. The opacity-based styles are automatically injected by the HTXM library; but, you can override the CSS properties in your own CSS if you need to.

For this iteration, I've added a Loading... span after the submit button. Here's part of my search form:

<form method="get" class="linear-form">
<label for="search">
Search contacts:
</label>
<input
id="search"
type="text"
name="q"
value="#encodeForHtmlAttribute( url.q )#"
hx-get="index.cfm"
hx-push-url="true"
hx-trigger="input delay:200ms"
hx-target=".search-results"
hx-select=".search-results"
hx-select-oob="pagination"
hx-indicator=".htmx-indicator"
/>
<button type="submit">
Search
</button>
<span class="htmx-indicator">
Loading....
</span>
</form>
view raw snippet-10.cfm hosted with ❤ by GitHub

Since the htmx-indicator class is applied to an element that is a child of the <form>, HTMX automatically treats it as the request indicator when the form is submitted. However, this isn't true for the hx-get attribute on the search input. To use the same indicator for the active search (ie, the search triggered by the input), we have to use the hx-indicator attribute to point to the Loading.... element.

To make this demo more intelligible, I've also added an artificial 5-second delay to any request that has a populated q value. This gives the Loading.... indicator time to actively contribute to the user experience.

Another change that I made to the application is that I added an hx-sync attribute on the body tag:

<body hx-boost="true" hx-sync="this:replace">
view raw snippet-11.cfm hosted with ❤ by GitHub

By default, the hx-boost does not cancel any in-flight requests as the user navigates around the application. Which means, if the user triggers an "active search" request (which is artificially slowed-down in this iteration), and then immediately navigates to another page (such as a contact detail), the active search request will continue to run in the background. And, will be applied to the DOM and URL when it completes. This can lead to unexpected experiences.

By adding hx-sync="this:replace" to the body tag, it's inherited by the entire app; and, as the user navigates to any page or triggers any AJAX requests, each request will immediately cause any in-flight requests to be aborted. This is more in alignment (in my opinion) with how the browser works natively.

Of course, you can always override this inherited hx-sync behavior by adding an hx-sync attribute lower down in the DOM tree if there's an area of interactions that you want to manage explicitly.

Aside: I opened a discussion about htmx:abort over on GitHub and Vincent/@telroshan had some great feedback about why the hx-sync default behavior was in place (such as debouncing double-clicks); and also pointed out some of the trade-offs in synchronizing requests on the root. I think for the time being, I want to keep the synchronizing on the root (since this is closer to the normal behavior of the browser). But this definitely gave me a lot to think about. There are no "obviously right" solutions.

See relevant chapter in Hypermedia Systems. It's the section labeled, "Adding A Request Indicator".

v11 - Adds Button Disabling During Form Submission

This iteration of the ColdFusion + HTMX application adds the hx-disabled-elt attribute to the add and edit forms to disable the submit buttons while the form is being processed. I've also added a sleep(500) to these forms so that the disabled state of the button can be seen briefly during processing (otherwise the processing happens too quickly).

As I was experimenting with this attribute, I felt like there was a happy middle ground that was missing. Consider the hx-disabled-elt attribute on a form tag. To disable the buttons in the form, I can do this:

<form hx-disabled-elt="button">

This works; but, it will disable all the buttons on the entire page during the form processing, not just the buttons within the form.

To limit the scope to the form, we could try to add the find keyword:

<form hx-disabled-elt="find button">

This will scope the button CSS selector to the form; but, it will only find the first matching button. If the form has multiple submit buttons, all the buttons after the first one would remain enabled during the form processing.

To limit the scope to the form but keep the selection broad, we could use traditional CSS techniques, like giving the form a class and then including that class in our global CSS selector:

<form class="my-form" hx-disabled-elt=".my-form button">

This would disable all buttons within our form thanks to the .my-form scoping.

For the simplicity of the demo, I'm just using the global button selector since I don't have any UIs with unrelated buttons strewn around the page. But, I thought it was worth discussing.

I've also opened an idea discussion on GitHub, suggesting that new CSS helper like findall or findmany might be a value-add for this kind of a scenario.

This wasn't a specific step in the Hypermedia Systems book, it was just something I wanted to consider after adding the hx-sync to the root element in the last iteration (and the trade-off that double-submissions were no longer inherently handled by the implicit hx-sync on each element).

v12 - Adds Lazy-Loaded Contact Count

This iteration of the ColdFusion + HTMX application adds the lazy loading of the total count of contacts. We are artificially slowing this request down (1.5 seconds) to make sure that it's an expensive request. Then, we're using hx-trigger="load" to only make the request after the parent page has loaded.

The index.count.cfm page does nothing but render the snippet of text that outputs the count:

<cfscript>
sleep( 1500 );
contacts = contactModel.getByFilter();
</cfscript>
<cfoutput>
(#numberFormat( contacts.len() )# in total)
</cfoutput>
view raw snippet-12.cfm hosted with ❤ by GitHub

The trigger for this expensive request is being done from within the title tag:

<h1>
Contacts App
<span
hx-sync="this"
hx-get="index.count.cfm"
hx-trigger="load">
<!--- Lazy load the contacts length. --->
</span>
</h1>
view raw snippet-13.cfm hosted with ❤ by GitHub

Recall that in an earlier iteration, I added hx-sync="this:replace" on the <body> tag. This forces all requests to synchronize on the content root (which mirrors the browser's native behavior). However, we don't necessarily want this lazy loading to interact with that pool of requests. As such, I'm including an hx-sync attribute on the lazy loading content in order to isolate it within its own synchronization pool.

This is so easy to do!

But, once I had this in place, I noticed that when I paged through the results, each page was causing the count to re-fetch. This is because the pagination itself is doing a full page refresh. This navigation is still boosted; but, it's clearing out the whole page, causing the lazy loaded contact count to re-fetch.

This was a bit distracting. So, I updated the prev/next links to perform partial page swapping instead of whole page swapping. By adding hx-* attributes to the pagination root, they will be inherited by both the prev and next links:

<div
id="pagination"
class="pagination"
hx-push-url="true"
hx-swap="show:window:top"
hx-target=".search-results"
hx-select=".search-results"
hx-select-oob="pagination">
<cfif pagination.hasPrevious>
<a href="index.cfm?q=....">
&larr; <u>Prev</u>
</a>
</cfif>
<cfif pagination.hasNext>
<a href="index.cfm?q=....">
<u>Next</u> &rarr;
</a>
</cfif>
</div>
view raw snippet-14.cfm hosted with ❤ by GitHub

Here, the hx-target and hx-select attribute perform the partial page swap (instead of the whole page swap). And, on top of that, I'm using the hx-select-oob to also perform an out-of-band swap of the contents of the pagination such that we don't show an erroneous prev or next link when it's no longer needed.

By default, HTMX seems to scroll to the hx-target element upon swap. This was causing my prev/next links to jump down on the page. But, this felt unnatural. I wanted the prev/next links to still look and feel like full page loads. As such, I also added:

hx-swap="show:window:top"

This tells HTMX to scroll to the "top" of the "window" when the content is swapped-in / shown.

Aside: I think there's a way to vastly simplify some of the search interactions; but, I'll look at that in a future iteration.

See relevant chapter in Hypermedia Systems. It's the section labeled, "Lazy Loading".

v13 - Refactor Search Layout To Use "Focus Scroll" Behavior

This iteration of the ColdFusion + HTMX application refactors the search layout now that I've learned about the focus scroll feature of hx-swap. Up till now, I've been trying to be surgical in my selection and swapping during the search results rendering because I was afraid to disrupt the search-input focus. But, it turns out that HTMX will maintain the focus of the currently-focused input during a swap as long as the input has an id attribute (that holds consistently across the swap).

What this means is that I can actually wrap the entire search module - including the form, pagination links, and results table - inside a common container:

<div class="search-section">
<form>
<input />
<nav />
</form>
<table />
</div>
view raw snippet-15.cfm hosted with ❤ by GitHub

Then, I can swap-out this common container for all the main interactions:

  • Form submissions.
  • Active search while typing (thanks to the "focus scroll").
  • Pagination.

Also, I was able to move the active search off of the input and simply add it as an additional hx-trigger on the form itself. Here's my new form:

<form
method="get"
hx-trigger="
submit,
input from:find input delay:200ms
"
hx-target=".search-section"
hx-select=".search-section"
hx-swap="show:window:top"
class="linear-form">
<!--- other stuff --->
</form>
view raw snippet-16.cfm hosted with ❤ by GitHub

Notice that the hx-boost'ed form now responds to both the natural submit event and to the active search input from:find input. Then, from the response, we're selecting the .search-section, our common container, and swapping it. The hx-swap directive tells HTMX to scroll to the top of the window (like a normal boosted operation). Note that the default hx-swap behavior is to scroll to the top of the hx-target.

My pagination is also using a similar tactic:

<div
hx-target=".search-section"
hx-select=".search-section"
hx-swap="show:window:top"
class="pagination">
<a href="...">Prev</a>
<a href="...">Next</a>
</div>
view raw snippet-17.cfm hosted with ❤ by GitHub

Again, we're using the hx-target and hx-select to brute-force swap the entire search area. Note that these attributes are being inherited by both the prev and next pagination links and therefore don't have to be duplicated within both links.

I think this is going to make some of the more dynamic interactions a lot easier if I don't have to worry about input focus (as long as the input has a consistent id on it). This seems like a really handy behavior - I'm surprised it isn't more front-and-center in the public discourse. Hopefully I'm not missing something obvious.

Aside: Note that the value of the focused input still gets updated based on the server response. So, even though the focus is being maintained across the hx-swap, it's possible that the input value will suddenly change out from under the user.

v14 - Adds A Delete Button To The Contacts List

This iteration of the ColdFusion + HTMX application adds a "Delete" action to the list of contacts. This allows each contact to be deleted without leaving the context of the contacts list.

In the book, this feature was implemented by making a request to the original delete page; and then, conditionally changing the response based on the existence of the HTMX HTTP headers. I've chosen to go a different route. Instead of re-purposing the existing delete page - and tightly coupling it to multiple contexts - I've created a new deletion end-point that is specifically designed for the index page.

Now, I have three index-related pages:

  • index.cfm
  • index.count.cfm
  • index.delete.cfm - the new one for this iteration.

So, we already have an established pattern of creating small, sibling utility end-points for page-specific operations.

To implement this, I added a <button> to each row that calls the index.delete.cfm file using an hx-delete request:

<cfloop array="#contacts#" item="contact">
<tr>
<td />
<td />
<td />
<td>
<button
hx-sync="this"
hx-delete="index.delete.cfm?id=#...#"
hx-target="closest tr"
hx-swap="outerHTML"
class="link-button">
Delete
</button>
</td>
</tr>
</cfloop>
view raw snippet-18.cfm hosted with ❤ by GitHub

Notice that the the hx-target replaces the closest <tr>. This new row content is generated by the new index.delete.cfm page:

<cfscript>
// ... truncated validation ...
contactModel.deleteByFilter( id = contact.id );
header
name = "HX-Trigger"
value = "contactDeleted"
;
</cfscript>
<cfoutput>
<tr class="deleted-row">
<td colspan="4" style="text-align: center ;">
#encodeForHtml( contact.name )# as been deleted.
</td>
</tr>
</cfoutput>
view raw snippet-19.cfm hosted with ❤ by GitHub

There are two things to notice in this tailor-made page. First, it's returning the new <tr> that's going to replace the original row in the table. So the inline delete doesn't actually remove the row; more so, it demarcates it as having been deleted. I think this reduces some of the potential jarring user experience.

Second, the deletion end-point is sending back the HTMX HTTP response header, HX-Trigger. This will cause the contactDeleted event to be fired on the client where we can use it to re-fetch the contacts count:

<span
hx-sync="this:replace"
hx-get="index.count.cfm"
hx-trigger="
revealed,
contactDeleted from:body
">
<!--- Lazy load the contacts length. --->
(loading count...)
</span>
view raw snippet-20.cfm hosted with ❤ by GitHub

Now, the (artificially) expensive "contact count" request will be initiated on both revealed and contactDeleted events.

This isn't a perfect solution; but, I do like the idea of creating small sub-pages for actions like this so that I can collocate behavior and reduce coupling across different sections of the application.

See relevant chapter in Hypermedia Systems. It's the section labeled, "Inline Delete".

v15 - Adds Bulk Delete

This iteration of the ColdFusion + HTMX application adds a bulk delete operation to the contacts list. Just as with the previous iteration, I've created a specialized end-point, sibling to the index page, that is designed for the bulk delete. However, unlike the one-off delete in the previous iteration, this version provides a checkbox-based selection; and, performs a full redirect of the page.

To start, I added a checkbox to each row that will report the associated contact ID as part of an array:

<td>
<input
form="bulkForm"
type="checkbox"
name="contactIDs[]"
value="#encodeForHtmlAttribute( contact.id )#"
/>
</td>
view raw snippet-21.cfm hosted with ❤ by GitHub

Note that the input is using the form attribute to associate itself with a non-hierarchical form element. In the book, the authors wrapped the entire search results table in the form tag; but for me, that approach was conflicting with the button elements that I has previously added for the one-off deletes.

Instead, I put the form at the bottom of the page, and allowed the form="bulkForm" attributes to build the necessary associations. Even though there are no checkboxes located physically within the bounds of the form, the form submission will include any checked, associated input.

<form
id="bulkForm"
method="post"
action="index.deleteMany.cfm"
hx-confirm="Are you sure you want to delete the selected contacts?">
<!---
Pass through the query and pagination settings so that we can
re-route to this same page after deletion.
--->
<input type="hidden" name="q" value="..." />
<input type="hidden" name="page" value="..." />
<input type="hidden" name="pageSize" value="..." />
<button type="submit">
Delete Selected Contacts
</button>
</form>
view raw snippet-22.cfm hosted with ❤ by GitHub

Again, I'm diverging from the book slightly. In the book, the authors are using an hx-delete attribute on the submit button itself (and then targeting the body tag). But, to me, it seemed easier to just navigate to the index.deleteMany.cfm file, perform the delete, and then redirect back to the index page.

My approach incurs an additional request to the server; but, my approach also feels less "clever", which I always appreciate. Plus, by redirecting to a separate page, it gives me a clear place to render an error message should I ever need to (not that I am in this implementation).

Ultimately, I'm taking the user to the following deletion page, where I perform the multi-contact delete, and then redirect the user back to the search page. To help the user maintain context in the pagination, I'm passing through the search parameters; and then using them in the redirection as well. This way, if you multi-delete contacts on the 3rd search results page, I bring you back to the 3rd search results page:

<cfscript>
param name="form.contactIDs" type="array" default=[];
param name="form.q" type="string" default="";
param name="form.page" type="string" default="";
param name="form.pageSize" type="string" default="";
for ( id in form.contactIDs ) {
contactModel.deleteByFilter( id = id );
}
goto(
"index.cfm" &
"?flash=contact.deletedMany" &
"&q=#encodeForUrl( form.q )#" &
"&page=#encodeForUrl( form.page )#" &
"&pageSize=#encodeForUrl( form.pageSize )#"
);
</cfscript>
view raw snippet-23.cfm hosted with ❤ by GitHub

Just because I have HTMX, it doesn't mean that I have to HTMX'ify all the things. Part of what makes HTMX so powerful is that is builds on the native web platform. And sometimes that means doing full page redirects.

See relevant chapter in Hypermedia Systems. It's the section labeled, "Bulk Delete".

v16 - Adds A Download Feature

This iteration of the ColdFusion + HTMX application adds a "download contacts" feature. This exploration is a little janky because it attempts to explore a long-running asynchronous process (to generate the file); but, in reality, the process would be instant. As such, I have to jump through a number of hoops to simulate what the workflow would look like.

Ultimately, there's a div at the bottom of the list page that runs the main contents of a different page:

<div
hx-target="this"
hx-select=".downloader"
hx-push-url="false"
class="downloader">
<a href="download.cfm?action=start">Download contact list</a>
</div>
view raw snippet-24.cfm hosted with ❤ by GitHub

When the user clicks the "Download" link, HTMX will "boost" the request; but, instead of targeting the body element like HTMX would normally, we are inheriting the hx-target="this" attribute to contain the requested content. Then, we're using the hx-select to only grab the portion of the content with the matching class name, .downloader.

The download.cfm page then executes a polling operation, making repeated requests until a randomly selected duration has passed. As I mentioned above, there is no actual generation that's happening. As such, this is all theater.

That said, the download page uses the same kind of containment mechanism as above so that the same content will continue to be swapped locally even if the download link is opened in a new tab (ie, the download page uses the same mechanics).

The following was the lightest-weight way to perform this simulation - this is not an example of any best practices. I'm reproducing this file in full down below since I am too tired to explain it in any mediated way:

<cfscript>
param name="url.action" type="string" default="waiting";
param name="url.startedAtMS" type="numeric" default=0;
param name="url.completedAtMS" type="numeric" default=0;
title = "Download Contact List";
switch ( url.action ) {
case "start":
// When we start the download process, we're just going to select a fake
// duration and pass the start/end times through with each request. This is
// just to explore the workflow, not to be meaningful / correct.
fakeDurationMS = randRange( 3000, 7000 );
startedAtMS = getTickCount();
completedAtMS = ( startedAtMS + fakeDurationMS );
percentComplete = 0;
break;
case "run":
// Calculate how close we are to the fake end-time.
total = ( url.completedAtMS - url.startedAtMS );
delta = ( getTickCount() - url.startedAtMS );
percentComplete = fix( delta / total * 100 );
// If we've passed 100% progress, the redirect to the download call to action.
if ( percentComplete >= 100 ) {
goto( "download.cfm?action=done" );
}
break;
}
</cfscript>
<cfsavecontent variable="body">
<cfoutput>
<h1>
#encodeForHtml( title )#
</h1>
<div
hx-target="this"
hx-select=".downloader"
hx-push-url="false"
class="downloader">
<cfswitch expression="#url.action#">
<cfcase value="waiting">
<a href="download.cfm?action=start">Start download</a>.
</cfcase>
<cfcase value="start,run">
<div
hx-get="download.cfm?action=run&startedAtMS=#encodeForUrl( startedAtMS )#&completedAtMS=#encodeForUrl( completedAtMS )#"
hx-trigger="every 500ms">
Generating... #encodeForHtml( percentComplete )#%
</div>
</cfcase>
<cfcase value="done">
<!--- Since this points to a download, we have to DISABLE BOOST. --->
<a href="download.json.cfm" hx-boost="false">Download generated JSON file</a>.
</cfcase>
</cfswitch>
</div>
</cfoutput>
</cfsavecontent>
<cfinclude template="_layout.cfm">
view raw snippet-25.cfm hosted with ❤ by GitHub

It's kind of cool to see it all come together - and with relatively little effort.

See relevant chapter in Hypermedia Systems. It's the section labeled, "A Dynamic Archive UI".

HTMX Is Pretty Dang Cool

By taking this journey, it really underscored how different it is to read about a topic vs. trying to implement it myself. So many of things that seemed so clear as I was reading the book suddenly became so confusing when I went to actually implement them for myself. This is the power of hands-on-learning. And is a lesson that I need to keep in the back of my mind.

HTMX seems pretty dang cool. I was surprised how relatively easy it was to get things up and running once I understood the HTMX mechanics. I still have so much learning to do; but, I feel like I'm moving in the right direction.

Want to use code from this post? Check out the license.

Reader Comments

49 Comments

So, this statement:
implements them via AJAX, swapping the entire contents of the page (less the <head>) with the ColdFusion server response

I would consider that pretty much what browsers do naturally! 🙃

I'll be honest, I didn't read the entire post here, but I'd be curious about your opinion on how you would consider this HTMX style of coding fitting in with other frameworks like Vue or React, if at all.

16,004 Comments

@Will,

So, yeah, that's basically what the browser is doing 😄 and, in fact, there's an interesting article that I came across a few weeks ago were the author was making an argument that in a lot of cases, you should just let the page perform a hard (ie, normal) reload during navigation events:

https://unplannedobsolescence.com/blog/hard-page-load/

And, that HTMX should just be used for things that aren't already going to be on the page during a normal page navigation. I'm probably not explaining that nearly as well as the author is, though.

As far as how this compares to more SPA-like frameworks, it's really a very different set of trade-offs. With the SPA stuff, you usually have to have a separate API that the SPA uses; and, everything has to run off the SPA code. With HTMX (and Hotwire Turbo, and Unpolly), it's more of a traditional server-side rendering approach (ie, what ColdFusion has historically done really well). Then, you're enhancing the experience (sometimes progressively, sometimes not) to have some of that rich interactivity without the complexity of the SPA.

Of course, it's not all free. You have to think differently about the architecture; and, you are giving up some of the very rich interactivity that you get with a SPA. But, you can always have parts of your application use SPA without having to commit entirely to the approach.

Though, to be clear, I'm still learning all this 😂 so, I'm not going to have all the best answers on hand.

4 Comments

Hi Ben,
Thanks for a great article about using HTMX. I am attempting to do the same in a SaaS I'm building. I like the idea of using the DOM to do a lot of simple work.

16,004 Comments

@Mike,

If you haven't, you should take a look at the Hypermedia Systems book. You can read it for free online (or purchase a physical copy):

https://hypermedia.systems/

It really walks through the mindset in a helpful way. I'm still very much just getting my feet wet here; there's a lot to think about.

267 Comments

I love that you're delving into htmx as I've always been curious about it but haven't (yet) taken the time to do a deep dive myself. I'm not (yet) convinced I need it to be honest. It may be a solution looking for a problem, but I'm open (and interested) to learn along with you.

When you delete the rows, would it be trivial to "undo" the deletion? I really love the pattern, which avoids the "confirm" dialog of any sort and just deletes, but offers a painless way to ctrl+z if you made a mistake. I think that's just great UX.

I was just looking at the Datastar (https://data-star.dev/) examples which uses HTMX/Alpine.js and found them to be frustrating when editing forms. If I placed my cursor in the middle of a word, it would refresh an put the cursor at the end of the word. That's just terrible UX. Is this a common problem in HTMX? I noticed you might have experienced this when you were editing the phone number in your contact form, but I couldn't be sure.

4 Comments

Hypermedia Systems is an excellent book, Ben.

The problem I am trying to solve is reducing brittle UI with dependencies on others and constant changes to libraries for large enterprise systems.

The plan.

  1. Use a CF static file generator to render 90% of pages as static HTML, including parts of dynamic pages. (It works a treat) 😃
  2. Use HTMX to do all simple dynamic UI.
  3. Use open-source JavaScript libraries for things like datagrids. (simple as possible)
    Use CFML for the rest. (logged in users only)
8 Comments

Hey Ben,

Boy, that was a deep dive on HTMX, thanks for sharing.

For me, I look at HTMX as a principle, rather than a Javascript library.

It's knowing you can do things like dom changes, active search, and submitting form data simply using bits of vanilla Javascript.

All without shipping tons of JS, using npm, or having to bundle anything. Magical.

You just have to learn Javascript.

But to switch gears, I'd like to comment on what Mike said above.

For years now (since Netlify and Vercel), we have been pushed to believe that non-dynamic pages should always be pre-rendered.

For cost considerations it makes sense, since hosting static sites is often free.

But for anything else, that notion is mostly nonsense.

Mike is absolutely right to remove as many third-party dependencies as possible.

And as admirable as it is to use CF to build static pages, you really don't need to.

Consider this...

Render all your pages using CF, use vanilla JS to control user events.

Cheers

16,004 Comments

@Chris,

These are all great questions! And, to be honest, I don't have great answers for any of them :P Is this all necessary? It's so hard to say. Since this is already skewing more towards a traditional request-response, multi-page application architecture, and leans heavily on the ideas of "progressive enhancement", it's easy to wonder if it's worth the frustration of then having to deal with more complexity.

In one of the earlier comments, I linked to an article in which the author talks about hard-refreshes, and about how we should really just keep doing normal page-to-page navigations and then use HTMX just for the in-page fanciness. And, part of me really loves that idea because it removes a whole bunch of issues (like how to do you effectively handle errors and redirects).

The more I learn about this stuff, the more I just have questions! 😂

re: Undo - I think that would really come down to how you architect the app. You'd have to be able to actually undo the action at the data level as well (like maybe you're using "soft deletes" in the CFWheels framework). In a case like that, the <tr> that is returned in the deletion request, that gets swapped into the DOM, could include a button that clears the deletedAt field, and then returns another <tr> with the "active" row. So, I think you can do that kind of stuff, but it's not just a UI concern at that point.

The datastar stuff is really interesting! I was just yesterday listening to an interview on the HX-Pod (podcast) with the Datastar creator. I even went on to linkedin to ask a question about how Datastar would work with load-balanced servers. I feel like there's a big puzzle piece missing in my head.

re: inputs and focus, one nice thing that HTMX does is that it (seems) to maintain the focus and the carrot position of the currently focused input at the time of the swap. The phone-number issue you saw in the video was me just editing some typos out of the video itself :D

16,004 Comments

@Mike,

I 100% feel your pain about reducing brittle UI. You're talking to someone who worked on a massive AngularJS application, only to see it end-of-life'd (AngularJS) leaving my application living on a non-maintained framework.

One of the things that I like about HTMX and other frameworks in this category is that they are just trying to do so much less. It's not a "batteries included" kind of approach; more like a few primitives that you can build on. So, I think there's much less of an opportunity for things to break. Though, that's just a feeling - not any kind of statement of fact. I'm still learning all this stuff myself.

To help reduce brittleness in general, my best advice is always "prefer copy/paste over coupling". The biggest source of issues for me and my teams is when someone changes something here and it breaks something over there in an unexpected way.

To this end, in my HTMX exploration here, for things that were index-page (list of contacts) specific, I created separate little utility files to aide in the interactions (ex, index.delete.cfm) rather than trying repurpose some existing end-point to also work with the index page. The way I see it, the "controller layer" should be inexpensive to build as long as you're drawing good boundaries around the shared business logic. As such, creating specialized utility / helper pages shouldn't be a big cost; and, should serve to reduce coupling and complexity by a good margin.

As far as static site generation, I've never tried that. I've always been curious to try though. But, I wonder if you're starting to make things too complex at that point. It's one thing to reduce a brittle UI, it's another thing to make the developer experience harder.

16,004 Comments

@Bill,

I tend to agree. I would prefer in-memory caching (in ColdFusion) before I'd worry about static content generation if performance is a major concern. One of the magical things about ColdFusion is how so much of it "just works": you create a CFM file, you deploy it, and viola - you have a dynamic page. No compilation step, no creating Views and code-behinds. No ceremony.

The older I get the more I want things to be simple simple simple!

Now, when I develop, I am constantly paying attention to the pain points that I'm feeling. Every time I have to jump back and forth between files, or between folders, or scroll up and down, I quietly ask myself, is there a way I could reduce this friction? How could I make this simpler? Is there a better way to collocate behaviors and logic?

It's a constant quest for the right trade-offs, finding the ones that make life easier, but without sacrificing too much of something else.

4 Comments

Ben,
I love the experimentation to see what works best with HTMX.

I used HTMX in a "Ribbon" UI Component. It seemed to work oK, but I'm still learning.

I have been using a home-built CF-driven static page generator that uses a database for the source since 2004. The biggest science reference website it rendered was 90,000 + pages. The current version renders 300 Wikipedia-sized HTML pages per/second. One result of using this method is the size of each page. Usually 10-30kb instead of 1 Mb +. A massive gain in simplicity. ❤️

There is no right or wrong in this, it depends on the problem to be solved.

Post A Comment — I'd Love To Hear From You!

Markdown formatting: Basic formatting is supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.
Cancel
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