Dynamically Updating Views With Turbo Streams Using Hotwire And Lucee CFML

Cyberdime
Published: February 3, 2023

As I demonstrated in my earlier post, Turbo Frames can be used to swap portions of a view using the response from a GET page request. Hotwire takes that concept a step further with Turbo Streams. In response to a POST form submission, a series of <turbo-stream> elements can define multiple, independent mutations that Hotwire will perform on the currently rendered view. I wanted to explore the Turbo Streams mechanics in Lucee CFML.

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

When Turbo Drive intercepts a native form submission, it prevents the default behavior and then makes the request using the fetch() API. When it does this, it injects text/vnd.turbo-stream.html into the Accept HTTP request header. This header value signals to the ColdFusion server that the response can be composed of <turbo-stream> elements instead of the traditional HTML (error response) or a Location HTTP response header (success response).

A Turbo Stream response needs to be returned with the content type, text/vnd.turbo-stream.html, and contain zero or more <turbo-stream> elements. Each <turbo-stream> element defines a single operation that Turbo Drive has to perform on the currently rendered view. The default Turbo Stream operations are:

  • append
  • prepend
  • replace
  • update
  • remove
  • before
  • after

That said, you can also define custom Turbo Stream actions for your application – but, I’m getting way ahead of myself. In this post, I just want to look at the very basics of the Turbo Stream workflow.

And, to do that, I’m going to create a simple ColdFusion application that renders a dynamic list of Counters. New counters can be added to the list. And, existing counters can be independently incremented or removed from the list. The state for the counters is persisted in a small ColdFusion component:

THREAD SAFETY: In the following ColdFusion code, you’ll see that I am using the ++ operator on a cached value. The ++ operator is not thread safe. In a production application, I would create thread safety by using an AtomicInteger instead. However, for this simple demo, I’m not worrying about thread safety.

component
	output = false
	hint = "I provide a collection of incrementing counters. These are NOT intended to be thread-safe, and are just for a demo."
	{
	/**
	* I initialize an empty collection of counters.
	*/
	public void function init() {
		variables.counters = [:];
	}
	// ---
	// PUBLIC METHODS.
	// ---
	/**
	* I add a new counter. The new counter is returned.
	*/
	public struct function addCounter() {
		var id = createUuid();
		var counter = counters[ id ] = {
			id: id,
			value: 0
		};
		return( counter.copy() );
	}

	/**
	* I return the collection of counters.
	*/
	public array function getAll() {
		var asArray = counters.keyArray().map(
			( id ) => {
				return( counters[ id ].copy() );
			}
		);
		return( asArray );
	}

	/**
	* I increment the given counter. The existing counter is returned.
	*/
	public struct function incrementCounter( required string id ) {
		var counter = counters[ id ];
		counter.value++;
		return( counter.copy() );
	}

	/**
	* I remove the given counter. The removed counter is returned.
	*/
	public struct function removeCounter( required string id ) {
		var counter = counters[ id ];
		counters.delete( id );
		return( counter );
	}
}

As you can see, each counter has a UUID-based id and a value. The id becomes important because <turbo-stream> elements are (usually) applied to the existing DOM by way of an id.

Check out the license.

Source: www.bennadel.com