Creating Custom Turbo Stream Actions In Hotwire And Lucee CFML
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> →
</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:
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
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.
@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.
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 thetarget
attribute (ID-based selector).this.targetElements
- for use with thetargets
attribute (CSS-based selector).I haven't tried this myself, but I wanted to post that link here for future reference.
@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.
@Peter,
My pleasure. 💪
@All,
So this custom
action="visit"
Turbo Stream directive just came in handy when transcluding a ColdFusion form into another page:www.bennadel.com/blog/4418-transcluding-a-form-into-a-turbo-frame-using-hotwire-and-lucee-cfml.htm
Basically, instead of doing a traditional
location()
redirect after a form submission, I issue two<turbo-stream action="visit">
directives to refresh two different<turbo-frame>
elements. So cool! 😎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 →