I've Never Had A Good Story For View-Rendering Helpers In ColdFusion
As I've been digging into the Hotwire framework, I've been looking at a lot of Ruby on Rails code (which is where Hotwire originated). Coming from a ColdFusion background, grokking Rails code is somewhat challenging because it's (seemingly) like 99% "view helpers" and syntactic sugar. CFML, on the other hand - with its majestic tag support and encodeFor*
functions - feels more accessible. But, the other day in a Stimulus controller demo, I had to write this in a CFML template:
<button
data-action="parent##logAction"
data-parent-child-id-param="#encodeForHtmlAttribute( item.id )#"
data-parent-child-data-param="#encodeForHtmlAttribute( serializeJson( item ) )#">
Trigger Parent Action
</button>
It was the attribute with the embedded serializeJson()
call that just rubbed me the wrong way. Don't get me wrong - I love me an encodeForHtmlAttribute()
call (security for the win!); but, it's just so verbose. And, with the serializeJson()
call on top of it, it's too much words!
I started to think about "view helpers". And how, after 20+ years of writing CFML, I've never really had a good story for view-related utilities.
I know that some ColdFusion frameworks have ways to "magically" make some User Defined Functions (UDFs) available to the view rendering templates. For example, Framework One (FW/1) evaluates templates in the context of your Application.cfc
component. As such, any methods in said component are available in your view templates.
But, this feels a bit messy to me - having two different concerns (application events and configuration + view rendering) being handled by the same component.
I haven't used ColdBox, but I've read that if you have *Helper.cfm
templates in special places, ColdBox will make them available for the relevant view rendering templates.
But, this also feels a bit too "magical" to me. Early in my career, I spent a lot of time trying to figure out how to hack UDFs to make them globally available; only to, later in my career, come to realize how magical it was to have explicit code that was easy to understand.
A couple of years ago, I looked at collecting HTML class names using a helper function. I wonder if I could take that concept, wrap it in a ColdFusion component, and just extend it to make it more robust. With a ColdFusion component as the packaging, I could cache it and then just alias it (for brevity) where ever it was need.
Something like this - and for this exploration, I'm just thinking about attributes:
<cfscript>
// In a production app, this ColdFusion component would be cached in the application
// scope, or wired into a dependency-injection mechanism.
application.viewHelper = new ViewHelper();
// Then, I would likely alias it for the sake of brevity wherever it was needed.
view = application.viewHelper;
</cfscript>
<cfoutput>
<!--- Serializing complex values (as JSON) for use in an attribute. --->
<form>
<input
type="hidden"
name="state"
#view.attr( "value", { id: 1, name: "Ben" } )#
/>
</form>
<!--- Optionally including [selected] attribute based on condition. --->
<form>
<select>
<cfloop index="i" from="1" to="10">
<option
#view.attr( "value", i )#
#view.selectedAttr( i == 5 )#>
#i#
</option>
</cfloop>
</select>
</form>
<!--- Optionally including [disabled] attribute based on condition. --->
<form>
<button #view.disabledAttr( false )#>
I am enabled
</button>
<button #view.disabledAttr( true )#>
I am disabled
</button>
</form>
<!--- Optionally including [checked] attribute based on condition. --->
<form>
<cfloop index="i" from="1" to="5">
<input
type="radio"
name="r"
value="1"
#view.checkedAttr( i == 2 )#
/>
</cfloop>
</form>
<!--- Coalescing [class] attribute based on conditions. --->
<p #view.classAttr([
"block": true,
"block--modifier": true,
"block__element": false
])#>
I have CSS classes.
</p>
<!--- Coalescing [style] attribute. --->
<p #view.styleAttr([
"color": "red",
"font-style": "italic"
])#>
I have styles.
</p>
<!--- I render multiple "data-" attributes with encoded values. --->
<p #view.dataAttrs([
"action": "parent##logAction",
"parent-child-id-param": 1,
"parent-child-data-param": { id: 1, name: "Ben Nadel" }
])#>
I have "data-"" attributes.
</p>
</cfoutput>
One of the main things that the ViewHelper.cfc
ColdFusion component does is encapsulate all the escaping of attribute values. But then, for boolean attributes like checked
, disabled
, and selected
, it will also encapsulate logic around whether or not the relevant attribute is rendered at all (based on conditions).
Here's the output that we get:
I'm not 100% sold on this approach - I'm just thinking out loud here. And, I'm not saying that I would use this for every attribute that I output - only where it makes sense and reduces complexity and verbosity.
Here's the ColdFusion component that powers the above CFML template:
component
output = false
hint = "I provide utility methods for rendering HTML in a view template."
{
/**
* I output the given attribute with proper attribute encoding. If the value is a
* complex data structure, it is serialized as JSON.
*/
public string function attr(
required string name,
any value = ""
) {
if ( isSimpleValue( value ) ) {
return( "#name#=""#encodeForHtmlAttribute( value )#""" );
} else {
return( "#name#=""#encodeForHtmlAttribute( serializeJson( value ) )#""" );
}
}
/**
* I output the given boolean attribute if the given condition is Truthy.
*/
public string function booleanAttr(
required string name,
any condition = false
) {
return( isTruthy( condition ) ? name : "" );
}
/**
* I output the "checked" boolean attribute if the given condition is Truthy.
*/
public string function checkedAttr( any condition = false ) {
return( booleanAttr( "checked", condition ) );
}
/**
* I output the "class" attribute, including every key in the given collection whose
* corresponding value is Truthy.
*/
public string function classAttr( required struct values ) {
var classNames = filterStructOnTruthyValues( values )
.keyArray()
.toList( " " )
;
if ( classNames.len() ) {
return( "class=""#classNames#""" );
}
return( "" );
}
/**
* I output a "data-*" attribute for each key-value pair in the given collection.
*/
public string function dataAttrs( required struct values ) {
var pairs = values.keyArray()
.map(
( key ) => {
return( attr( "data-#key#", values[ key ] ) );
}
)
.toList( " " )
;
return( pairs );
}
/**
* I output the "disabled" boolean attribute if the given condition is Truthy.
*/
public string function disabledAttr( any condition = false ) {
return( booleanAttr( "disabled", condition ) );
}
/**
* I output the "selected" boolean attribute if the given condition is Truthy.
*/
public string function selectedAttr( any condition = false ) {
return( booleanAttr( "selected", condition ) );
}
/**
* I output the "style" attribute using the given key-value CSS properties.
*/
public string function styleAttr( required struct values ) {
var pairs = [];
for ( var key in values ) {
pairs.append( "#key#: #values[ key ]# ;" );
}
var styles = pairs.toList( " " );
if ( styles.len() ) {
return( "style=""#styles#""" );
}
return( "" );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I filter the given collection down to the keys with Turthy values.
*/
private struct function filterStructOnTruthyValues( required struct collection ) {
var filteredCollection = collection.filter(
( key, value ) => {
// CAUTION: The filter() method will include null keys in the iteration.
// As such, we can't use the isTruthy() method on its own or we'll run
// into null reference errors.
return( ! isNull( value ) && isTruthy( value ) );
}
);
return( filteredCollection );
}
/**
* I determine if the given value is a Falsy.
*/
private boolean function isFalsy( any value = false ) {
if ( ! isSimpleValue( value ) ) {
return( false );
}
if ( isBoolean( value ) || isNumeric( value ) ) {
return( ! value );
}
return( value == "" );
}
/**
* I determine if the given value is a Truthy (which is any value that isn't Falsy).
*/
private boolean function isTruthy( any value = false ) {
return( ! isFalsy( value ) );
}
}
This was just a thought experiment. I'm curios to hear how other ColdFusion developers are handling this kind of thing - common view-rendering patterns.
Want to use code from this post? Check out the license.
Reader Comments
This would clean up the view code nicely. My colleagues tend to do messy things like wrap verbose CFIF tags around selected, checked, and disabled conditions...which looks really scary within the HTML tag. I thought I was doing better by relocating this logic above as in...
<cfset disabledClause = user.type != 'admin' ? 'disabled' : ''>
Note: The condition statement can be very lengthy in our cases.
Then I use the conditional variable in the proper place...
<button #disabledClause#>Delete</button>
but I can see many advantage to your approach. It feels very similar to several React CSS libraries I've seen.
@Chris,
I've totally done that (reducing a complex condition into a variable, and then using that variable for the dynamic attribute). In fact, I just did that the other day. Clearly great minds think alike! 😉
Re: React functions, my
classAttr()
method here is more-or-less a direct rip-off of thecx()
package concept that is all over React applications. I think the React one uses an Array; or, is a variadic function (can take N-arguments); so, mine is less robust. But yeah, I copied the idea.Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →