Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Dan Card
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Dan Card

Setting Up My ColdFusion + Hotwire Demos Playground

By
Published in , Comments (6)

A month ago, I started building a ColdFusion and Hotwire application as a learning experience. Only, once I finished the basic ColdFusion CRUD (Create, Read, Update, Delete) features, I didn't really know how to go about applying the Hotwire functionality. I realized that I bit off more than I could chew; and, I needed to go back and start learning some of the Hotwire basics before I could build an app using the "Hotwire way". As such, I've started a new ColdFusion and Hotwire Demos project, where I intended to explore stand-alone aspects of the Hotwire framework.

View this code in my ColdFusion Hotwire Demos project on GitHub.

Nothing in this repository is intended to be "production ready"! This repository is not a shining example of Docker containerization, ColdFusion code organization, Parcel.js bundling, or component modularization. My intent here is only to get enough stuff working such that I can learn more about how Hotwire and ColdFusion / Lucee CFML might interact.

The context that I am creating here is a CommandBox-driven Docker container that also installs Node.js 19.x. Here's my Dockerfile for the build:

FROM ortussolutions/commandbox

RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash - \
	&& apt-get install -y \
		build-essential \
		nodejs \

In this case, nodejs installs the node version that is provided by nodesource.com call. And, the build-essential package provides libraries like make and cc that are needed by the Parcel.js bundler (and its dependencies).

The base image for CommandBox doesn't install any particular ColdFusion engine by default - I have to tell it which engine I want using environment variables in my docker-compose.yaml file. In this case, I'm using Lucee CFML 5.3.10.

version: "2.4"

services:

  lucee:
    build:
      context: "./docker/"
      dockerfile: "Dockerfile"
    ports:
      - "80:8080"
      - "8080:8080"
    volumes:
      - "./demos:/app"
    environment:
      BOX_SERVER_APP_CFENGINE: "lucee@5.3.10+97"
      BOX_SERVER_PROFILE: "development"
      cfconfig_adminPassword: "password"
      LUCEE_CASCADE_TO_RESULTSET: "false"
      LUCEE_LISTENER_TYPE: "modern"
      LUCEE_PRESERVE_CASE: "true"

This gives me a basic ColdFusion server; but, as I mentioned in a previous article, Hotwire Turbo Drive doesn't work with .cfm file extensions. This is because ColdFusion can serve up anything (it's hella powerful!); and, Turbo Drive needs assurances that HTML is going to be served. As such, Turbo Drive will only intercept navigation actions that involve .htm / .html file extensions.

To get my ColdFusion server to play nicely with Turbo Drive, I'm going to be routing all my ColdFusion links through a non-existing template, hotwire.cfm. Then, I'm going to be using the cgi.path_info property to define the actual URL.

So, for example, if I want to navigate to index.cfm, I'm going to define my link as:

./hotwire.cfm/index.htm

I need to use .htm as the file extension in the URL so that Turbo Drive intercepts the navigation event. Of course, what I really want to do is execute index.cfm - not index.htm. For this, I am using the ColdFusion application framework's onRequest() event handler in order to override the script execution:

component
	output = false
	hint = "I define the application settings and event handlers."
	{

	// Define the application settings.
	this.name = "HelloWorld";
	this.applicationTimeout = createTimeSpan( 0, 1, 0, 0 );
	this.sessionManagement = false;
	this.setClientCookies = false;

	// ---
	// LIFE-CYCLE METHODS.
	// ---

	/**
	* I process the requested script.
	*/
	public void function onRequest( required string scriptName ) {

		// The root-absolute path to this demo app (used in the page module).
		request.appPath = "/hello-world/app";

		// Basecamp's Hotwire Turbo Drive will only work with static ".htm" or ".html"
		// file extensions (at the time of this writing). As such, in order to get Turbo
		// Drive to play nicely with ColdFusion's ".cfm" file extensions, we're going to
		// route all requests through the index file and then dynamically execute the
		// corresponding ColdFusion template.
		// --
		// CAUTION: In a production application, blindly invoking a CFML file based on a
		// user-provided value (path-info) can be dangerous. I'm only doing this as part
		// of a simplified demo.
		if ( cgi.path_info.len() ) {

			var turboScriptName = cgi.path_info
				// Replace the ".htm" file-extension with ".cfm".
				.reReplaceNoCase( "\.html?$", ".cfm" )
				// Strip off the leading slash.
				.right( -1 )
			;

			include "./#turboScriptName#"; 

		} else {

			include scriptName;

		}

	}

}

Notice here that if the cgi.path_info value is populated, I replace the .htm file extension with a .cfm file extension and then CFInclude the calculated template path. What this means is that my routing to:

./hotwire.cfm/index.htm

... gets translated into this line of ColdFusion code in my onRequest() event handler:

include "./index.cfm";

Again, I want to reiterate that this is not intended to be production-ready code. In fact, dynamically executing CFML templates based on user-provided paths is definitely unsafe. This is just enough for me to get Lucee CFML and Hotwire playing nicely together for exploratory purposes.

In order to try and hide this HTML-CFML bait-and-switch from the code in my ColdFusion templates, I'm using the <base> tag to define the hotwire.cfm proxy in my layout (truncated version of page.cfm):

<cfif ( thistag.executionMode == "end" )>

	<cfsavecontent variable="thistag.generatedContent">
		
		<!doctype html>
		<html lang="en">
		<head>
			<cfoutput>
				<base href="#request.appPath#/hotwire.cfm/" />
			</cfoutput>

			<!---
				CAUTION: Since I'm setting the base-href to route through a ColdFusion
				file, our static assets have to use root-absolute paths so that they
				bypass the base tag settings.
			--->
			<cfoutput>
				<script src="#request.appPath#/dist/main.js" defer async></script>
				<link rel="stylesheet" type="text/css" href="#request.appPath#/dist/main.css"></link>
			</cfoutput>
		</head>
		<body>

			<cfoutput>
				#thistag.generatedContent#
			</cfoutput>

		</body>
		</html>

	</cfsavecontent>

</cfif>

Thanks to my <base> tag:

<base href="#request.appPath#/hotwire.cfm/" />

... when I have a content link like this:

<a href="some-page.htm">Goto Some page</a>

... it will be evaluated by the browser as:

/hello-world/app/hotwire.cfm/some-page.htm

... which my Application.cfc onRequest() event handler will then execute as:

include "./some-page.cfm";

It's frustrating that I have to jump through these hoops in order to get ColdFusion and Turbo Drive to work together. But, the alternative would be to build out much more robust routing logic on the ColdFusion side; and, that's completely tangential to the goal of this repository. As such, I'm opting into some silly code in order to minimize the amount of boiler plate logic that I have to put in place.

With this page.cfm template above, I can then build relatively simple ColdFusion pages by wrapping content in a CFModule tag that executes page.cfm as a custom tag. For example, here's my hello world root index page:

<cfmodule template="./tags/page.cfm">

	<h1>
		Hello World
	</h1>

	<p>
		This is the root page in my CFML+Hotwire exploration.
	</p>

	<p>
		<a href="sub/index.htm">Try going to a sub folder</a> &rarr;
	</p>

</cfmodule>

And, here's my sub-folder index page:

<cfmodule template="../tags/page.cfm">

	<h1>
		Sub Folder
	</h1>

	<p>
		Folders are a fun, if you can get into it.
	</p>

	<p>
		<a href="index.htm">Back to the root page</a> ^
	</p>

</cfmodule>

Note that my link from the sub-folder back to the root-folder is index.htm and not ../index.htm. This is because all relative paths are appended to the <base [href]>, not to the current folder.

Once I had my basic ColdFusion application in place, I then went about installing Hotwire Turbo, Hotwire Stimulus, and Parcel:

{
	"name": "hello-world",
	"scripts": {
		"js-build": "parcel build          ./src/js/main.js --dist-dir ./app/dist/",
		"js-watch": "parcel watch --no-hmr ./src/js/main.js --dist-dir ./app/dist/",
		"less-build": "parcel build        ./src/less/main.less --dist-dir ./app/dist/",
		"less-watch": "parcel watch        ./src/less/main.less --dist-dir ./app/dist/"
	},
	"author": "Ben Nadel",
	"license": "ISC",
	"dependencies": {
		"@hotwired/stimulus": "3.2.1",
		"@hotwired/turbo": "7.2.4",
		"@parcel/transformer-less": "2.8.3",
		"parcel": "2.8.3"
	}
}

The npm scripts are intended to be executed from within the running Docker container (which is the whole point of containerization). So, in order to compile my JavaScript, I first "bash into" the running container:

docker-compose run lucee bash

Then, change to the desired directory:

cd hello-world/

And then, run my npm scripts inside the container:

npm run js-watch

ASIDE: In this case, I disabled Hot Module Reloading (HMR) because I could not figure out how to get the WebSocket connection to work with the Docker container. It seemed no matter which ports I exposed, the network request (ws://localhost/) would fail. Build systems are not my strong-suit; and, manually refreshing the page is not very painful for me.

In order to make sure that Hotwire's Tubro Drive was wired-up, my main.js file imports the Turbo Drive library and binds to the load event:

// Import core modules.
import * as Turbo from "@hotwired/turbo";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

var turboEvents = [
	// "turbo:click",
	// "turbo:before-visit",
	// "turbo:visit",
	// "turbo:submit-start",
	// "turbo:before-fetch-request",
	// "turbo:before-fetch-response",
	// "turbo:submit-end",
	// "turbo:before-cache",
	// "turbo:before-render",
	// "turbo:before-stream-render",
	// "turbo:render",
	"turbo:load",
	// "turbo:before-frame-render",
	// "turbo:frame-render",
	// "turbo:frame-load",
	// "turbo:frame-missing",
	// "turbo:fetch-request-error"
];

for ( var eventType of turboEvents ) {

	document.documentElement.addEventListener(
		eventType,
		( event ) => {

			console.group( "Event:", event.type );
			console.log( event.detail );
			console.groupEnd();

		}
	);

}

Now, if I run my ColdFusion application, and navigate between the two pages, I get the following:

Notice that the ColdFusion application appears to be doing full-page refreshes of the content. However, we can tell by the Console that the page is not reloading (otherwise the console history would be cleared after each navigation). Instead, Hotwire Turbo is intercepting the click events, preventing the default browser behavior, and updating the content via fetch() requests.

In fact, if we jump over to the Network tab of the Chrome dev tools, we can see that fetch is how the requests are being made:

At this point, I now have a relatively simple (albeit non-production-ready) way to start a ColdFusion container that can compile Hotwire code. Now, I should be able to start exploring some of the many features of the Hotwire framework.

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

Reader Comments

15,880 Comments

@Matt,

Yes! Charlie Arehart has a GitHub repository called Awesome CF Compose , which demonstrates how to create a variety of ColdFusion containers with either Adobe ColdFusion or Lucee CFML.

Then, there's the CommandBox Docker Hub page, which provides a more hand-held way to spin up a ColdFusion container. Basically, you use their base image, and then just define the ColdFusion engine that you want to use via the ENV variables. In fact, I'm using the CommandBox approach for my playground. Here's my Dockerfile :

FROM ortussolutions/commandbox

... you can see I'm using the ortussolutions/commandbox base image (the rest of that file is just installed node.js). Then, in my docker-compose.yaml , I'm simply defining the BOX_SERVER_APP_CFENGINE and mounting my code volume:

version: "2.4"
services:
  lucee:
    environment:
      BOX_SERVER_APP_CFENGINE: "lucee@5.3.10+97"

Now, to be clear, I am not very good at Docker 😆 so, if you want to know how to do something special and custom, it'll be over my head. But, as far as getting a development environment working, this approach has been relatively easy.

15,880 Comments

@Matt,

My pleasure. To be clear, I personally find Docker to be complicated and confusing 😨 I've been using it for a few years now, and I barely have any idea what I'm doing. I was never a person who dealt much with servers directly, so Docker kind of wraps a black-box around another black-box for me. As such, I'm not sure how easy or hard other people find this kind of stuff.

That said, using Docker has allowed me to do a lot more experimentation that I would have been able to do using other means. 💪

13 Comments

Thanks for exploring this topic.

I love using CFMl for my projects. The spirt of Hotwire is to enable you to use CFML MORE in place where you would have to move application logic into a JS frame work.

Until this series, it seemed like it would be way too hard to use hotwire with CFML.

Ben - would it be possible to make Hotwire CFML native and remove its dependence on node?

15,880 Comments

@Peter,

I'm with you - when I can use more CFML in more places, I feel like I'm winning! 💪 🙌 🎉

In this case, the dependence on Node.js is simply for the building of the JavaScript and Less CSS files. You could, from what I understand, just include the Turbo JS file in your <head> element. I feel like the Hotwired site used to have an example of this; but, now when I go to the Turbo Installation Docs, it has no example and directs you to Skypack for more information.

But, I'm pretty sure you can take advantage of all the Turbo Drive / Turbo Frames / Turbo Streams functionality with a simple JavaScript file.

That said, if you need to compile any CSS or start to bundle JavaScript Controllers in Hotwire Stimulus, I'm pretty sure you'll need to have some sort of build script (which is often in Node.js, though other things like ESBuild and Vite are becoming hawt these days - though I have no experience with them).

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

Post a Comment

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