Skip to main content
Ben Nadel at Angular 2 Master Class (New York, NY) with: Todd Tyler
Ben Nadel at Angular 2 Master Class (New York, NY) with: Todd Tyler

Creating Custom Turbo Stream Actions In Hotwire And Lucee CFML

By
Published in , Comments (7)

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="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="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="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="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.

Reader Comments

13 Comments

Ben,

I'm enjoying your series on Hotwire. Keep the content coming.

I'm hoping it culminates with an implementation of Hotwire in CFML. I'd love you use mostly CFML to express my apps, or at least a lite implementation.

I mentioned in a previous comment. It would be cool if we could get the whole stack running just in cfm and not require a node server. Maybe that necessitate a slightly different implementation, but that could account for your .htm redirect concept.

15,902 Comments

@Peter,

That's the goal - to move move as much of the processing and logic into CFML as possible. At runtime, you don't need a node.js server, all the pages are being served by ColdFusion. But, I do need Node to build / bundle the JS and CSS files. At this point, I am not sure there's any way around that. I mean, you could write vanilla JS and CSS; but, it likely gets more complicated that way. Or, maybe "complicated" isn't the right word. But, once the files are compiled, then it's basically all CFML.

15,902 Comments

Note to self: I was just reading over on this post by Marco Roth, Turbo 7.2: A guide to Custom Turbo Stream Actions, and it looks like the action callback method might be invoked with some helper properties:

  • this.targetElement - for use with the target attribute (ID-based selector).
  • this.targetElements - for use with the targets attribute (CSS-based selector).

I haven't tried this myself, but I wanted to post that link here for future reference.

13 Comments

@Ben,

Thanks for clearing that up. I wasn't thinking that the the css/js was only a build step, but I recall that now from DHH's initial Hotwire demo.

15,902 Comments

Here's a follow-up post that looks at some additional custom actions for Turbo Streams:

www.bennadel.com/blog/4450-selecting-portions-of-a-turbo-stream-template-with-custom-actions.htm

In this case, I'm re-creating the native actions, but allowing each <turbo-stream> element to have a [selector] attribute that extracts only a portion of the <template> element to apply to the live DOM. I think this will make it much easier to update forms (for example) with an error message.

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