Collecting HTML Class Name Attributes In Template Rendering In Lucee CFML 5.3.7.47
The other day, I was updating a CFML template to include some conditional CSS class names in an ordered list. And, by the time I was done, the CFML looked a hot mess with several ternary operators all being interpolated into one class=""
attribute. And, as I sat there, wallowing in the shame of such ugly looking code, it hit me like a bolt of lightening: Angular already solved this problem so elegantly with the NgClass
directive that applies dynamic class names based on a set of conditionals. The same exactly thing should be quite doable in Lucee CFML 5.3.7.47.
To give you a sense of the shame that I was feeling, I ended up with an LI
element that looked something like this:
<cfloop index="local.scheduleDay" array="#rc.scheduleDays#">
<li class="m-schedule__item #( scheduleDay.isActiveDeployment ? 'm-schedule__item--deployment' : '' )# #( scheduleDay.isToday ? 'm-schedule__item--today' : '' )# #( scheduleDay.isFuture ? 'm-schedule__item--future' : '' )# #( scheduleDay.isBlocked ? 'm-schedule__item--blocked' : '' )#">
<!--- truncated --->
</li>
</cfloop>
As you can see, the class
attribute on my list-item has one base class name and then a number of other class names that are applied conditionally. Well, I say "as you can see", but the reality is, this is terribly unreadable!
Now, on the Angular side of the world, they solved this with problem a decade ago with the NgClass
directive which accepts (among other things) an Object in which the keys represent class names and the values determine which class names are included. So, the above CFML mess could be represented in an Angular app as follows:
<li [ngClass]="{
'm-schedule__item': true,
'm-schedule__item--deployment': scheduleDay.isActiveDeployment,
'm-schedule__item--today': scheduleDay.isToday,
'm-schedule__item--future': scheduleDay.isFuture,
'm-schedule__item--blocked': scheduleDay.isBlocked
}">
<!-- truncated -->
</li>
This is 1000-times more readable than the soup of ternaries that I have in my ColdFusion. And, it's totally something that we can do in ColdFusion as well. All we have to do is create a function that takes a Struct and returns a list of keys whose values are "truthy". Here's what that might look like in Lucee - I'm calling the function encodeClassAttribute()
:
<cfscript>
contacts = [
{ id: 1, name: "Joe", isBFF: true, isFriend: true },
{ id: 2, name: "Sam", isBFF: false, isFriend: true },
{ id: 3, name: "Kit", isBFF: false, isFriend: false },
{ id: 4, name: "Ryan", isBFF: false, isFriend: false },
{ id: 5, name: "Alex", isBFF: true, isFriend: true },
{ id: 6, name: "Jordan", isBFF: false, isFriend: true }
];
```
<cfoutput>
<ul>
<cfloop value="contact" array="#contacts#">
<li class="#encodeClassAttribute([
contact: true,
'contact--friend': contact.isFriend,
'contact--bff': contact.isBFF
])#">
#encodeForHtml( contact.name )#
</li>
</cfloop>
</ul>
</cfoutput>
```
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I collect the list of CSS class-names to apply to a given context. The list will
* include every key whose value is a Truthy.
*
* @conditions I am the key/value pairs being inspected.
*/
public string function encodeClassAttribute( required struct conditions ) {
var classNames = [];
loop
key = "local.className"
value = "local.condition"
struct = conditions
{
if ( isTruthy( condition ?: false ) ) {
classNames.append( className );
}
}
return( classNames.toList( " " ) );
}
/**
* I determine if the given value is a Truthy.
*
* @value I am the value being inspected.
*/
public boolean function isTruthy( any value ) {
return( ! isFalsy( value ) );
}
/**
* I determine if the given value is a Falsy.
*
* @value I am the value being inspected.
*/
public boolean function isFalsy( any value ) {
if ( isNull( value ) ) {
return( true );
}
if ( ! isSimpleValue( value ) ) {
return( false );
}
if ( isBoolean( value ) || isNumeric( value ) ) {
return( ! value );
}
return( value == "" );
}
</cfscript>
As you can see, we loop over the key-values pairs in the passed-in Struct; and, we collect every key whose value passes the isTruthy()
call. This gives us a collection of class names which we then serialize as a space-delimited list. And, when we run this ColdFusion code, we get the following HTML:
As you can see, each LI
element has a class
attribute that contains only the class names with the relevant truthy values.
This approach feels so clean to me, I'm frankly shocked that it's taken me close to a decade of using Angular before I realized that the same approach can be applied in ColdFusion template rendering. The trick will be figuring out how to take this function and make it generally available to my Lucee CFML templates.
Want to use code from this post? Check out the license.
Reader Comments
I took this concept for another spin, wondering if I could make a "View helper" ColdFusion component:
www.bennadel.com/blog/4428-ive-never-had-a-good-story-for-view-rendering-helpers-in-coldfusion.htm