Creating Custom Turbo Stream Actions In Hotwire And Lucee CFML

Cyberdime
Published: February 24, 2023

The Hotwire Turbo framework uses <turbo-stream> elements to apply targeted DOM (Document Object Model) manipulations to the current page. These Turbo Stream elements can be rendered in response to a Form POST; or, as we saw yesterday, they can be returned inline in any page response. The default Turbo Stream actions are all DOM-related. However, we can define our own custom actions as well. To explore this, I’m going to create a custom Turbo Stream action that invokes Turbo.visit().

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

To define a custom Turbo Stream action, we have to monkey patch an action handler onto the StreamActions object. The name of the method maps to the action attribute on the <turbo-stream> element. In this case, we’re going to call the action, visit:

// Import core modules.
import * as Turbo from "@hotwired/turbo";
import { StreamActions } from "@hotwired/turbo";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
/**
* I support Turbo.visit() stream actions.
*/
StreamActions.visit = function() {
	var url = this.dataset.url;
	var action = ( this.dataset.action || "advance" );
	var frame = ( this.dataset.frame || undefined );
	Turbo.visit(
		url,
		{
			action: action,
			frame: frame
		}
	);
}

When Turbo Drive invokes the .visit() method, it is binding the <turbo-stream> element as the this context. As such, in order to access additional data about the action, we can query the DOM node using this.getAttribute() or, in my case, this.dataset to access the data-* attributes.

ASIDE: There are no rules on how you define data and how you name your attributes. My decision to use data-url, for example, was completely arbitrary.

With this .visit() handler defined, our ColdFusion server can then respond with <turbo-stream> elements of type action="visit":

<turbo-stream
	action="visit"
	url="https://www.bennadel.com/blog/index.htm">
	<!--- No content is relevant for this action. --->
</turbo-stream>

To see this in action, I’ve created a rather trite ColdFusion application with two pages: a main page; and, another page which does nothing but redirect the user back to the main page using our new Turbo Stream action.

Here’s the main page:

<cfmodule template="./tags/page.cfm">
	<cfoutput>
		<h2>
			Welcome to My Site
		</h2>
		<p>
			<a href="https://www.bennadel.com/blog/bounce.htm">Go to Bouncer</a>
		</p>
	</cfoutput>
</cfmodule>

This provides a link to bounce.htm, which does nothing by redirect back to the main page:

<cfmodule template="./tags/page.cfm">
	<cfoutput>
		<h2>
			Bouncing Back to Home
		</h2>
		<p>
			<!--- Required for restoration visits - see note below. --->
			<a href="https://www.bennadel.com/blog/index.htm">Go back to home</a> &rarr;
		</p>
		<!---
			CAUTION: If the user returns to this page through a restoration visit (ie,
			hitting the browser's BACK BUTTON), this Turbo-Stream element will no longer
			be here since it is removed during the stream evaluation. As such, it is
			important to provide the manual link above.
			--
			Also, by adding [data-action="replace"], we can override the current history
			entry, somewhat preventing the back button problem.
		--->
		<turbo-stream
			action="visit"
			data-url="https://www.bennadel.com/blog/index.htm">
		</turbo-stream>
	</cfoutput>
</cfmodule>

As you can see, I’ve included an inline <turbo-stream [action="visit"]> element. After the page renders (briefly), Turbo Drive will kick-in, gather up all the Turbo Stream elements, and then redirect the user back to the main page.

NOTE: For maximal safety, I’ve also included a manual “Back to home” link for restoration visits (when page is pulled from cache); and, for when the user hits this page directly without loading JavaScript. I’m always trying to keep my eye on progressive enhancement, never assuming that Hotwire is even loaded.

Now, if we load this ColdFusion page and try to access the bounce page, we get the following output:

A user navigates to the bounce page repeatedly, only to be immediately redirected back to the main page.

As you can see, the bounce.cfm page, almost immediately redirects the user back to the main page thanks to our custom Turbo Stream.

In this demo, we use the browser’s back button to render the cached version of bounce.cfm, which renders without any inline Turbo Stream elements. This is where our static anchor link comes into play. We can also, somewhat, get around this issue by including data-action="replace" in our <turbo-stream> element. This would replace the current window.history item instead of pushing a new item onto the stack.

Turbo ships with a handful of DOM manipulation stream actions that will likely give you most of what you need. But, it’s great to see that we can fill out that last mile of functionality with custom Turbo Stream actions. For more information, try looking at TurboPower, a 3rd-party collection of custom stream actions.

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

Source: www.bennadel.com