Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Brice Green
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Brice Green

A Simple Slide Show Using Hotwire And Lucee CFML

By
Published in , Comments (4)

Now that I have my ColdFusion and Hotwire playground up and running, I can start to explore the features of the Hotwire framework. And, one of the most attractive features is the ability to update a portion of the page using a full-page render. This works by scoping DOM (Document Object Model) changes to a given <turbo-frame> element. To see this in action, I wanted to create a simple slide show using Lucee CFML.

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

If you look at the header of my blog, I have a hero image with Prev/Next buttons. When you click one of those buttons, I make an API call back to the server that returns a JSON payload with information about the requested hero image. I then have to take that data and use it to update the hero image rendering.

Performing this update is actually quite a lot of work. I have to:

  • Make the API call.
  • Deserialize the JSON response.
  • Update the figure image being rendered.
  • Update the figure caption text (complete with links to hero participants).
  • Update the Prev/Next anchor links.

Updating [src] and [href] attributes is relatively easy; but, updating the figure caption text is a huge pain in the butt because I have to do it all in JavaScript with String concatenation. The advantage of using Hotwire's Turbo Drive is that I can move all of that complexity out of the JavaScript context and back onto the ColdFusion server, where rendering dynamic HTML templates is much easier.

The key here is to wrap the slide show portion of the page in its own <turbo-frame> element (pseudo-code example):

<turbo-frame id="slide-show">
	<!--- Render slide show content --->
	<a href="?slideIndex=#( slideIndex - 1 )#">Prev</a>
	<a href="?slideIndex=#( slideIndex + 1 )#">Next</a>
</turbo-frame>

When the user clicks on one of the Prev/Next buttons, Hotwire Turbo Drive will intercept the click event, cancel the default navigation, and make a fetch call instead. Once Turbo Drive receives the HTML response - of the full page load - it will then query the response HTML for the id="slide-show" frame and use it to swap-out the contents of the already rendered frame (leaving the rest of the DOM tree unaffected).

This allows us to dynamically render the slide show without any application JavaScript! All of the rendering is pushed to the ColdFusion server.

Here's the full demo from ColdFusion + Hotwire project. Instead of a Picture-based slide show, I'm creating a Quote-based slide show (to keep things simple). When the page first loads, a random quote is selected. However, if url.quoteIndex is provided, the specified quote is rendered:

<cfscript>

	param name="url.quoteIndex" type="numeric" default=0;

	// NOTE: In a production environment, this would be cached in the application scope.
	// But, to keep things simple, I'm just re-instantiating it on each request.
	quotes = new lib.Quotes();

	// Select the quote to render on the page.
	quote = ( url.quoteIndex )
		? quotes.getQuote( val( url.quoteIndex ) )
		: quotes.getRandomQuote()
	;

</cfscript>

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

		<h1>
			ColdFusion + Hotwire Slide Show Demo
		</h1>

		<!---
			While we want the slideshow to be in its own turbo-frame, so that it can
			operate without altering the rest of the page state, we still want the slide-
			show navigation buttons to change the URL so that a page refresh (or a copy-
			paste of the current URL) will result in the currently-rendered quote being
			re-rendered (as dictated by "url.quoteIndex"). As such, I'm including the
			[data-turbo-action] attribute value "advance". This will keep the DOM updates
			locked-down to the frame while replacing / updating the URL.
		--->
		<turbo-frame id="quote-frame" data-turbo-action="advance">

			<figure class="m1-quote">
				<blockquote class="m1-quote__quote">
					#encodeForHtml( quote.excerpt )#
				</blockquote>
				<figcaption class="m1-quote__caption">
					&mdash; #encodeForHtml( quote.author )#
				</figcaption>
			</figure>

			<!---
				Since these navigation links are inside a turbo-frame element, they will
				be used to replace the contents of THIS FRAME ONLY (id="quote-frame"),
				leaving the rest of the page content untouched.
				--
				NOTE: I originally tried to include the [data-turbo-preload] attribute to
				pre-fetch the Prev/Next pages; but, the preload feature does not work with
				turbo-frames at this time - https://github.com/hotwired/turbo/issues/857
			--->
			<div class="m2-controls">
				&larr;
				<a href="index.htm?quoteIndex=#encodeForUrl( quote.prevIndex )#">
					Prev quote
				</a>
				|
				<a href="index.htm?quoteIndex=#encodeForUrl( quote.nextIndex )#">
					Next quote
				</a>
				&rarr;
			</div>

		</turbo-frame>

		<p>
			<!--- This content will NOT BE ALTERED during slide show navigation. --->
			Page originally loaded with quote index (#encodeForHtml( url.quoteIndex )#).
		</p>

	</cfoutput>
</cfmodule>

As you can see, the <turbo-frame> contains the quote content as well as Prev/Next buttons. Outside of the frame, I have a paragraph that shows which quote index was initially provided on page load. As the user navigates between the quotes, what we should see is that content within the <turbo-frame> changes but everything else stays exactly the same:

A slide show of quotes that is updated using Next and Prev buttons.

How freaking cool is that! As you can see, nothing outside of the <turbo-frame> element has changed - only the contents within the frame get updated by Turbo Drive. And, to underscore the point, all of the rendering of the HTML is being performed by ColdFusion! No dealing with JSON, no concatenating strings and wiring up anchor tags - just full, server-side CFML template rendering.

ASIDE: Hotwire Turbo Drive uses the id attribute to match <turbo-frame> elements in the current DOM with <turbo-frame> elements provided in the fetch response.

You may also notice in the GIF that the URL is updating as I click on the Prev/Next buttons. By default, frame-based navigation events don't update the URL. However, since I'm including the data-turbo-action="advance" attribute on the <turbo-frame> element, Turbo Drive will push state onto the history as part of the navigation. This way, the URL state is updated to reflect the state of the page. And, if I hit the Next button a few times followed by a hard-refresh of the page, you'll see that the currently-selected quote is re-rendered:

Turbo Drive is updating the URL as the user clicks the Next button. This allows a hard-refresh of the page to re-render the currently-selected quote.

Seeing this in action - the moving of what would otherwise be complex JavaScript rendering back onto the server - this is really exciting. I still have trouble imagining a very complex application being built with Hotwire; but, this demo alone is enough to get me wanting to know more.

Pre-Caching Prev/Next Link Content

When I first put this demo together, I had added a pre-caching directive to the Prev/Next links:

<a href="..." data-turbo-preload>Next</a>

I had hoped that this would pre-fetch the Prev/Next URLs and then use them for instant rendering of subsequent navigation events. Unfortunately, at this time, the data-turbo-preload directive does not work in conjunction with a parent <turbo-frame>. I've opened a GitHub Issue about this; so, perhaps it will be addressed in the future.

My Quotes.cfc ColdFusion Component

For completeness, here's my Quotes.cfc ColdFusion component which provides random access and index-based access to a quote:

component
	output = false
	hint = "I provide an assortment of quotes from Sun Tzu's The Art of War."
	{

	/**
	* I initialize the service with the given quotes.
	*/
	public void function init() {

		variables.quotes = [
			{
				author: "Sun Tzu",
				excerpt: "Appear weak when you are strong, and strong when you are weak."
			},
			{
				author: "Sun Tzu",
				excerpt: "In the midst of chaos, there is also opportunity."
			},
			{
				author: "Sun Tzu",
				excerpt: "Move swift as the Wind and closely-formed as the Wood. Attack like the Fire and be still as the Mountain."
			},
			{
				author: "Sun Tzu",
				excerpt: "Treat your men as you would your own beloved sons. And they will follow you into the deepest valley."
			},
			{
				author: "Sun Tzu",
				excerpt: "So in war, the way is to avoid what is strong, and strike at what is weak."
			},
			{
				author: "Sun Tzu",
				excerpt: "One may know how to conquer without being able to do it."
			},
			{
				author: "Sun Tzu",
				excerpt: "If ignorant both of your enemy and yourself, you are certain to be in peril."
			},
			{
				author: "Sun Tzu",
				excerpt: "If he sends reinforcements everywhere, he will everywhere be weak."
			},
			{
				author: "Sun Tzu",
				excerpt: "Disorder came from order, fear came from courage, weakness came from strength."
			},
			{
				author: "Sun Tzu",
				excerpt: "Therefore, just as water retains no constant shape, so in warfare there are no constant conditions."
			},
			{
				author: "Sun Tzu",
				excerpt: "Plan for what it is difficult while it is easy, do what is great while it is small."
			},
			{
				author: "Sun Tzu",
				excerpt: "So long as victory can be attained, stupid haste is preferable to clever dilatoriness."
			},
			{
				author: "Sun Tzu",
				excerpt: "The principle on which to manage an army is to set up one standard of courage which all must reach."
			},
			{
				author: "Sun Tzu",
				excerpt: "It is best to keep one's own state intact; to crush the enemy's state is only second best."
			},
			{
				author: "Sun Tzu",
				excerpt: "Ground on which we can only be saved from destruction by fighting without delay, is desperate ground."
			}
		];
		variables.quoteCount = variables.quotes.len();

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I get the quote at the given index.
	*/
	public struct function getQuote( required numeric index ) {

		var quote = quotes[ index ].copy();

		// Inject the prev/next indices so that the UI can easily navigate through the
		// series of available quotes.
		quote.prevIndex = ( index == 1 )
			? quoteCount
			: ( index - 1 )
		;
		quote.nextIndex = ( index == quoteCount )
			? 1
			: ( index + 1 )
		;
		quote.index = index;

		return( quote );

	}


	/**
	* I get a random quote from the collection.
	*/
	public struct function getRandomQuote() {

		return( getQuote( randRange( 1, quoteCount, "sha1prng" ) ) );

	}

}

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

Reader Comments

15,880 Comments

@Joseph,

Thank you kindly! That means a lot. This Hotwire stuff is pretty cool, but really forces me to think differently (which is both frustrating and fun).

2 Comments

Hi Ben,

I've also been taking a similar approach to inject html into the dom. If you haven't already compared it, it would be worth checking out HTMX.. it's ridiculously simple to implement, requires almost no boilerplate and has become quite popular in the Django community.. It does take a little bit of rethinking as you've described.

I hope you find it as powerful as I have.

15,880 Comments

@Adam,

When I first heard about Hotwire, I actually heard about HTMX at around the same time; and, was trying to find some discussions about the pros/cons of each. From what I saw, it seems that HTMX is more "low level" and gives you more flexibility and Hotwire is a little more "high level" and wants you to work with a certain set of patterns. But, to be clear, I've not looked at the HTMX syntax or tried it out for myself - this was just me trying to get a sense of which path I should venture down.

Ultimately, I went with Hotwire more because I'm such a fan of the Basecamp / DHH mindset, and less about the technology itself. That said, I'm sure / hope that when I get my head wrapped around the Hotwire stuff, HTMX would also make more sense and be more about new syntax and less about a totally different approach.

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