Over the previous few months, I have been exploring the Hotwire framework from Basecamp. Hotwire consists of the usage of Turbo for enhanced efficiency and Stimulus for dynamic interactions. There’s something very engaging concerning the Hotwire philosophy and the way in which it drives progressive enhancement. However, dropping Hotwire right into a ColdFusion software is not seamless. File extension limitations and kind processing redirects, for instance, mandate a minimum of some modifications to the way in which you architect your Controller layer. I might like to start out utilizing Hotwire on my ColdFusion weblog; however, I do know my code is not “Hotwire prepared”. As such, I needed to take a look at how I can incrementally apply Hotwire to my present ColdFusion software.
View this code in my ColdFusion + Hotwire Demos undertaking on GitHub.
As I discussed above, Hotwire is not “one factor”, it is an umbrella of applied sciences that work collectively to strive and create SPA (Single-Web page Software)-like experiences on high of MPAs (Multi-Web page Software). Turbo consists of “Turbo Drive”, “Turbo Frames”, and “Turbo Streams”; and, works to reinforce web page navigation, partial web page updates, and lazy-loaded content material. Stimulus is the express JavaScript layer that provides dynamic interactivity to a given a part of the DOM (Doc Object Mannequin).
Incrementally Making use of Hotwire Turbo
Turbo – and, extra particularly, Turbo Drive – is the function that we actually need to concentrate on in the case of upgrading a ColdFusion software. Since Turbo Drive takes over web page navigation, it imposes essentially the most constraints; and, is the most probably facet of Hotwire to supply roadblocks.
Fortunately, we are able to flip Turbo Drive off by default:
// Import core modules.
import * as Turbo from "@hotwired/turbo";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// By default, we do not need Turbo Drive to take over the navigation except explicitly
// enabled inside the ColdFusion markup. This manner, we are able to baby-step our means in direction of a
// appropriate Turbo Drive software.
Turbo.session.drive = false;
By setting .drive
, Turbo will not intercept hyperlink clicks and kind submissions that focus on top-level navigation. Primarily, this makes Turbo Drive inert. And, when part of the brownfield ColdFusion software has been made “Turbo Drive appropriate”, we are able to explicitly allow Drive for a given department of the DOM tree utilizing the [data-turbo="true"]
attribute.
For instance, think about that solely my “About” web page is able to be loaded by way of Turbo Drive. In that case, I can alter the nav merchandise to incorporate data-turbo
:
<!---
As varied pages are up to date to be Hotwire / Turbo Drive appropriate, we
can add the "data-turbo" attribute to their nav hyperlinks. This manner, even when
many of the app continues to be powered by "old skool" full page-refresh
navigation, Turbo Drive can nonetheless kick-in the place it is going to be a value-add.
--
Notice that solely the ABOUT web page right here is Turbo-enabled.
--->
<nav>
<a href="https://www.bennadel.com/weblog/index.htm">House</a>
<a href="about.htm" data-turbo="true">About</a>
<a href="contact.htm">Contact</a>
</nav>
At this level, if I navigate to “House” or “Contact”, Hotwire will not do something – it’s going to permit a full-page navigation to happen. However, if I navigate to “About”, Hotwire nonetheless step in, intercept the hyperlink click on, fetch()
the content material, after which hot-swap it into the present DOM tree:

As you may see from the community exercise, the “House” and “Contact” pages are document-level navigation occasions. And, “About” – which has been enhanced with [data-turbo="true"]
– is intercepted by Turbo Drive.
As extra components of your ColdFusion software develop into Turbo Drive appropriate, you may simply hold annotating the related hyperlinks. And – hopefully – someday all the things is prepared and you’ll take away the entire annotations and take away the Turbo.session.drive=false
setting.
Turbo Frames “Simply Work”
To this point, we have been taking a look at Turbo Drive actions that apply to top-level web page navigations. Turbo Drive additionally impacts navigation and kind submissions situated inside a Turbo Body. These frame-based navigations are usually not disabled utilizing the aforementioned method. If you wrap a <turbo-frame>
aspect round a portion of the DOM tree, Hotwire goes to work! You possibly can view the existence of the <turbo-frame>
because the “enabling” of the function.
For instance, think about that I’ve a small Citation rotation widget that works by refreshing the present web page with a brand new quoteIndex
:
<cfscript>
param title="url.quoteIndex" kind="numeric" default=0;
quote = software.quoteService.getQuote( url.quoteIndex );
</cfscript>
<cfmodule template="./tags/web page.cfm" part="dwelling">
<cfoutput>
<h2>
Welcome to My Website
</h2>
<determine>
<blockquote>
#encodeForHtml( quote.textual content )#
</blockquote>
<figcaption>
←
<a href="index.htm?quoteIndex=#encodeForUrl( quote.prevID )#">Prev quote</a>
—
<a href="index.htm?quoteIndex=#encodeForUrl( quote.nextID )#">Subsequent quote</a>
→
</figcaption>
</determine>
</cfoutput>
</cfmodule>
If I need to “incrementally apply” Turbo Drive to simply that widget, all I’ve to do is wrap the widget in a Turbo Body:
<!---
Even with Turbo Drive disabled by default, Turbo Frames nonetheless work. As such,
for navigation occasions that refresh the web page, we are able to very simply wrap these
navigation components in a Turbo Body that advances the historical past.
--->
<turbo-frame id="quote-frame" data-turbo-action="advance">
<determine>
<blockquote>
#encodeForHtml( quote.textual content )#
</blockquote>
<figcaption>
←
<a href="index.htm?quoteIndex=#encodeForUrl( quote.prevID )#">Prev quote</a>
—
<a href="index.htm?quoteIndex=#encodeForUrl( quote.nextID )#">Subsequent quote</a>
→
</figcaption>
</determine>
</turbo-frame>
Now, after I click on on the Prev / Subsequent hyperlinks, we are able to see that Turbo Drive is intercepting the navigation occasions:

As you may see, the Prev / Subsequent hyperlinks are being executed by way of the fetch()
API and the content material of the Turbo Body is being dynamically up to date by Turbo Drive.
Asynchronously loaded Turbo Frames additionally “simply work”. If I need to dynamically transclude some content material from one other ColdFusion template, all I’ve to do is create a <turbo-frame>
with a [src]
attribute:
<!---
Even when Turbo Drive is disable by default, lazy-loaded Turbo Frames nonetheless
work. Which suggests, we are able to VERY EASILY lazy-load content material into the present web page.
--->
<turbo-frame id="lazy-frame" src="https://www.bennadel.com/weblog/lazy.htm" loading="lazy">
<!--- Placeholder content material (or if script hasn't loaded). --->
<a href="https://www.bennadel.com/weblog/lazy.htm">Go to lazy content material</a> →
</turbo-frame>
When asynchronously loading a Turbo Body, the static content material of the body will probably be rendered whereas the distant content material is being fetched. This content material also can act as a “swish degradation”. So, if our JavaScript bundle fails to load, on the very least, the person will probably be introduced with a hyperlink to manually go to the content material that was going to be loaded asynchronously.
.cfm
File Extensions
Turbo Drive Will not Intercept I simply need to reiterate that within the present construct of Hotwire, solely “static” file extensions get intercepted. As such, when you level to a .cfm
web page, Hotwire will not intercept the navigation occasion or the shape submission. As such, it’s a must to use URL rewriting to level to .htm
URLs after which map these to ColdFusion behind the scenes.
Sooner or later, Hotwire might permit different file extensions to be allow-listed. However for now, that is an open situation for the framework
Incrementally Making use of Hotwire Stimulus
Hotwire Stimulus is the “JavaScript sprinkles” that provides interactivity to your static DOM content material. Stimulus works by instantiating Controllers (ie, JavaScript class cases) after which binds them to host components on the web page. Because the content material of the web page modifications (ideally by way of Hotwire Turbo), Stimulus takes care of the controller life-cycle occasions similar to connecting after which disconnecting the controller to and from the DOM, respectively.
By itself, the Stimulus library does not do something. It solely snaps into motion when a [data-controller]
attribute is detected on the DOM. As such, it needs to be simpler to incrementally apply Stimulus (when in comparison with incrementally making use of Turbo Drive).
The complexity of incremental adoption actually begins to indicate up whenever you mix Hotwire Turbo Drive and Stimulus. As a result of Turbo Drive takes transient pages and creates a lengthy operating course of, the general life-cycle of the web page modifications. You possibly can not rely on the web page unloading to clean-up your JavaScript. Nor are you able to (essentially) rely on web page loading to initialize your JavaScript.
To be sincere, the greatest path ahead right here continues to be a bit fuzzy for me. The method that I feel I will use is to create an “App Controller”, connect it to the <physique>
tag, after which use its join()
life-cycle hook to initialize the remainder of my “old skool” JavaScript bindings.
<physique data-controller="app">
This Stimulus controller needs to be instantiated and sure to the DOM no matter how my ColdFusion web page hundreds, whether or not or not it’s as a full-page load or a Turbo Drive enhanced web page load. Then, in my JavaScript bundle, I can use this class to wire-up the remainder of the not-yet-updated JavaScript.
To discover this, I’ve included a “counter” widget in my ColdFusion web page:
<!---
This represents a "pre-Stimulus" widget. It will likely be bootstrapped by a page-
degree Stimulus controller till it may be migrated to make use of Stimulus by itself.
--->
<p class="old-school">
<button>
Clicked <span>0</span> Instances
</button>
</p>
If you click on the <button>
, all this does is increment the worth within the <span>
. I will wire this up in my root Stimulus controller:
// Import core modules.
import { Software } from "@hotwired/stimulus";
import { Controller } from "@hotwired/stimulus";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
class AppController extends Controller {
/**
* I get referred to as every time the controller occasion is sure to a number aspect.
*/
join() {
console.log( "App linked!" );
// Wire-up all of the options that haven't but been ported to their very own Stimulus
// controllers. Remember the fact that these strategies will probably be referred to as on each web page load.
setupOldSchoolThing();
}
}
window.Stimulus = Software.begin();
// When not utilizing the Ruby On Rails asset pipeline / construct system, Stimulus does not know
// the best way to map controller lessons to data-controller attributes. As such, we've got to
// explicitly register the Controllers on Stimulus startup.
Stimulus.register( "app", AppController );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
operate setupOldSchoolThing() {
// This setup technique will probably be referred to as on each web page load. As such, it's going to solely be
// relevant a few of the time. If this web page does not include an old-school widget,
// exit out.
if ( ! doc.querySelector( ".old-school" ) ) {
console.warn( "No old-school widget detected." );
return;
}
var dom = Object.create( null );
dom.container = doc.querySelector( ".old-school" );
dom.button = dom.container.querySelector( "button" );
dom.counter = dom.button.querySelector( "span" );
// Parse preliminary counter from the DOM state.
var clickCount = +dom.counter.textContent;
dom.button.addEventListener(
"click on",
operate handleClick() {
dom.counter.textContent = ++clickCount;
}
);
}
As you may see, my AppController
‘s join()
technique turns round and calls setupOldSchoolThing()
to wire-up non-Stimulus performance. And, once we load the ColdFusion web page, we are able to see that the AppController
logs the join()
hook and that our button works:

As you may see, my “old skool” JavaScript was efficiently wired-up by my middleman Stimulus controller.
As I proceed to improve my software, porting extra of the interactivity over to Stimulus, I can then take away these initialization calls from the AppController
.
Idempotent Outdated College JavaScript Initialization
As a result of the life-cycle of the web page begins to vary as Hotwire Turbo Drive is launched, we are able to not assume that JavaScript will solely be invoked as soon as. As a substitute, we’ve got to start out ensuring that our initialization code is idempotent, which simply implies that it is secure to name a number of instances.
On this case, I’ve two makes an attempt at making the code extra idempotent:
-
If there isn’t any aspect on the web page that matches the CSS selector,
.old-school
, I simply short-circuit out of the setup. This manner, as ourAppController
is linked to each web page the person visits, we are able to safely skip initialization of code that does not apply to the present web page. -
I am utilizing the DOM because the supply of the of reality for my preliminary counter worth. This manner, as pages are pulled out of the browser’s historical past, and Stimulus re-initializes the widgets, our counter can pick-up proper the place it left off as a substitute of skipping again to zero.
It is onerous to speak about idempotentcy from a common standpoint since all widgets are distinctive and particular and can probably have their very own constraints. A very powerful factor it is advisable do is simply be sure that your event-handlers are solely sure as soon as.
ASIDE: The binding of occasion handlers is probably going going to be trickiest half to handle. You could want to start out tapping within the
disconnect()
life-cycle technique of theAppController
as a method to teardown event-handlers. Nevertheless, the browser will routinely take away occasion handlers when the related DOM nodes are destroyed. So, you would possibly get fortunate in a variety of circumstances.
I will Report Again
Hopefully, this plan is fruitful. I might like to start out changing my ColdFusion weblog over to utilizing Hotwire within the subsequent few weeks. In fact, the conversion is extra than simply incrementally making use of the assorted Hotwire applied sciences, it’s going to additionally require re-thinking a few of the Controller layer structure. I will report again as I (nearly actually) run into hurdles.
UPDATE: 2023-03-17 – Including the Libraries
This morning, I took the primary steps in direction of integrating Hotwire into this ColdFusion weblog. All I did was set up the @hotwired/turbo
and @hotwired/stimulus
modules and disable Turbo Drive (as I outlined earlier on this publish). My uncompressed JavaScript bundle shot up from ~ 20KB to ~ 200KB (an order-of-magnitude enhance). However, it’s being served up by way of the Cloudflare CDN (Content material Supply Community); and, the utilized GZip compression reduces the transferred bundle dimension to 49Kb.
That is extra JavaScript than I might wish to have for a “weblog”; however, I’ve to do not forget that the JavaScript loading is all deferred and non-blocking. And, after I take a look at my Chrome Lighthouse rating after the change, to date I’ve not been penalized by the bigger bundle:

I do know that the Lighthouse rating is considerably superficial. However, a minimum of it offers me a baseline in opposition to which I can measure some incremental modifications over time.
Wish to use code from this publish?
Try the license.