Created
April 18, 2023 11:49
-
-
Save bennadel/55e1539ed20ec18c0e0c6c5f1997a5ac to your computer and use it in GitHub Desktop.
Selecting Portions Of A Turbo Stream Template With Custom Actions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<cfmodule template="./tags/page.cfm"> | |
<cfoutput> | |
<div id="demo-container"> | |
<p class="original"> | |
To be changed via Turbo Streams. | |
</p> | |
</div> | |
<!--- | |
Each of the following forms is going to POST to an end-point that returns a | |
Turbo Stream element with a CUSTOM ACTION. These custom actions are named for | |
the native Turbo Stream actions along with the suffix "With" (for example, | |
"replace" becomes "repalceWith"). The custom actions allow for an additional | |
attribute, "selector", which determines which elements within the TEMPLATE | |
will be used in the DOM manipulation. For the sake of the demo, I'm providing | |
these attributes via hidden inputs: | |
---> | |
<form method="post" action="stream.htm"> | |
<input type="hidden" name="action" value="prependWith" /> | |
<input type="hidden" name="selector" value=".prepend-with" /> | |
<button type="submit"> | |
Prepend With | |
</button> | |
</form> | |
<form method="post" action="stream.htm"> | |
<input type="hidden" name="action" value="appendWith" /> | |
<input type="hidden" name="selector" value=".append-with" /> | |
<button type="submit"> | |
Append With | |
</button> | |
</form> | |
<form method="post" action="stream.htm"> | |
<input type="hidden" name="action" value="updateWith" /> | |
<input type="hidden" name="selector" value=".update-with" /> | |
<button type="submit"> | |
Update With | |
</button> | |
</form> | |
<form method="post" action="stream.htm"> | |
<input type="hidden" name="action" value="beforeWith" /> | |
<input type="hidden" name="selector" value=".before-with" /> | |
<button type="submit"> | |
Before With | |
</button> | |
</form> | |
<form method="post" action="stream.htm"> | |
<input type="hidden" name="action" value="afterWith" /> | |
<input type="hidden" name="selector" value=".after-with" /> | |
<button type="submit"> | |
After With | |
</button> | |
</form> | |
<form method="post" action="stream.htm"> | |
<input type="hidden" name="action" value="replaceWith" /> | |
<input type="hidden" name="selector" value=".replace-with" /> | |
<button type="submit"> | |
Replace With | |
</button> | |
</form> | |
</cfoutput> | |
</cfmodule> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import core modules. | |
import * as Turbo from "@hotwired/turbo"; | |
import { StreamActions } from "@hotwired/turbo"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
/** | |
* I get the DocumentFragment to use as the Turbo Stream payload for the given Turbo Stream | |
* element. If no selector is provided, the original template is returned. If a selector is | |
* provided, a new fragment will be generated using the selector and returned. | |
*/ | |
function getFragmentUsingSelector( turboStreamElement ) { | |
var originalFragment = turboStreamElement.templateContent; | |
var selector = turboStreamElement.getAttribute( "selector" ); | |
// If no selector is provided, use the entire template - this is the same behavior as | |
// the relevant native action. | |
if ( ! selector ) { | |
return( originalFragment ); | |
} | |
// Locate the desired sub-nodes within the template. In the vast majority of cases, | |
// this will likely be a SINGLE root node. But, I'm using querySelectorAll() in order | |
// to make the Stream action a bit more flexible. | |
var nodes = originalFragment.querySelectorAll( selector ); | |
// Construct a new document fragment using the selected sub-nodes. | |
var fragment = document.createDocumentFragment(); | |
fragment.append( ...nodes ); | |
return( fragment ); | |
} | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
// In the following custom Turbo Stream actions, I basically went into the Turbo source | |
// code for StreamActions: | |
// -- | |
// https://github.com/hotwired/turbo/blob/main/src/core/streams/stream_actions.ts | |
// -- | |
// ... copied the logic, and replaced the raw .templateContent references with a call to | |
// extract the sub-tree of the template using the SELECTOR (and the Function above). | |
StreamActions.replaceWith = function() { | |
this.targetElements.forEach( | |
( targetElement ) => { | |
targetElement.replaceWith( getFragmentUsingSelector( this ) ); | |
} | |
); | |
} | |
StreamActions.updateWith = function() { | |
this.targetElements.forEach( | |
( targetElement ) => { | |
targetElement.innerHTML = ""; | |
targetElement.append( getFragmentUsingSelector( this ) ); | |
} | |
); | |
} | |
StreamActions.afterWith = function() { | |
this.targetElements.forEach( | |
( targetElement ) => { | |
targetElement.parentElement?.insertBefore( | |
getFragmentUsingSelector( this ), | |
targetElement.nextSibling | |
); | |
} | |
); | |
} | |
StreamActions.appendWith = function() { | |
this.removeDuplicateTargetChildren(); | |
this.targetElements.forEach( | |
( targetElement ) => { | |
targetElement.append( getFragmentUsingSelector( this ) ); | |
} | |
); | |
} | |
StreamActions.beforeWith = function() { | |
this.targetElements.forEach( | |
( targetElement ) => { | |
targetElement.parentElement?.insertBefore( | |
getFragmentUsingSelector( this ), | |
targetElement | |
); | |
} | |
); | |
} | |
StreamActions.prependWith = function() { | |
this.removeDuplicateTargetChildren(); | |
this.targetElements.forEach( | |
( targetElement ) => { | |
targetElement.prepend( getFragmentUsingSelector( this ) ); | |
} | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* I get the DocumentFragment to use as the Turbo Stream payload for the given Turbo Stream | |
* element. If no selector is provided, the original template is returned. If a selector is | |
* provided, a new fragment will be generated using the selector and returned. | |
*/ | |
function getFragmentUsingSelector( turboStreamElement ) { | |
var originalFragment = turboStreamElement.templateContent; | |
var selector = turboStreamElement.getAttribute( "selector" ); | |
// If no selector is provided, use the entire template - this is the same behavior as | |
// the relevant native action. | |
if ( ! selector ) { | |
return( originalFragment ); | |
} | |
// Locate the desired sub-nodes within the template. In the vast majority of cases, | |
// this will likely be a SINGLE root node. But, I'm using querySelectorAll() in order | |
// to make the Stream action a bit more flexible. | |
var nodes = originalFragment.querySelectorAll( selector ); | |
// Construct a new document fragment using the selected sub-nodes. | |
var fragment = document.createDocumentFragment(); | |
fragment.append( ...nodes ); | |
return( fragment ); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<cfscript> | |
param name="request.context.action" type="string"; | |
param name="request.context.selector" type="string"; | |
</cfscript> | |
<cfcontent type="text/vnd.turbo-stream.html; charset=utf-8" /> | |
<cfoutput> | |
<!--- | |
Note that this CUSTOM Turbo-Stream action is accepting a **SELECTOR** attribute. | |
The SELECTOR attribute allows us to target portions of the TEMPLATE element such | |
that we can more flexibly reuse existing interfaces (by including them, but | |
selecting only a portion of the interface as the Turbo-Stream payload). | |
---> | |
<turbo-stream | |
action="#request.context.action#" | |
selector="#request.context.selector#" | |
target="demo-container"> | |
<template> | |
<p class="new replace-with"> | |
Selected via 'replaceWith' | |
</p> | |
<p class="new update-with"> | |
Selected via 'updateWith' | |
</p> | |
<p class="new prepend-with"> | |
Selected via 'prependWith' | |
</p> | |
<p class="new append-with"> | |
Selected via 'appendWith' | |
</p> | |
<p class="new before-with"> | |
Selected via 'beforeWith' | |
</p> | |
<p class="new after-with"> | |
Selected via 'afterWith' | |
</p> | |
</template> | |
</turbo-stream> | |
</cfoutput> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment