Tuesday, January 12, 2010

Consistent Looping in ColdFusion

I've said this before, but I hate how inconsistent looping is in ColdFusion. And to make matters worse, ColdFusion 9 script syntax doesn't support looping over arrays using for...in statements. Why not?

I decided to try to ease my pain by creating a custom tag to loop over data in a consistent way. Here's what I came up with.

Usage

<cfimport prefix="for" taglib="/com/tags" />

<for:each key="key" in="#data#" value="value" index="i">
... stuff ...
</for:each>


Examples

<cfimport prefix="for" taglib="/com/tags" />

<cfoutput>
Array

<cfset people = [] />
<cfset people[1] = "LeBron James" />
<cfset people[2] = "Dwyane Wade" />
<cfset people[3] = "Kobe Bryant" />

<for:each in="#people#">
Hello, my name is #it#
</for:each>

Struct

<cfset people = {} />
<cfset people["LeBronJames"] = "LeBron James" />
<cfset people["DwyaneWade"] = "Dwyane Wade" />
<cfset people["KobeBryant"] = "Kobe Bryant" />

<for:each in="#people#" value="name">
Hello, my name is #name#
</for:each>

List

<cfset people = "LeBron James,Dwyane Wade,Kobe Bryant" />

<for:each in="#people#">
Hello, my name is #it#
</for:each>

Query

<cfset people = queryNew("firstName,lastName") />

<cfset queryAddRow(people) />
<cfset querySetCell(people, "firstName", "LeBron") />
<cfset querySetCell(people, "lastName", "James") />

<cfset queryAddRow(people) />
<cfset querySetCell(people, "firstName", "Dwyane") />
<cfset querySetCell(people, "lastName", "Wade") />

<cfset queryAddRow(people) />
<cfset querySetCell(people, "firstName", "Kobe") />
<cfset querySetCell(people, "lastName", "Bryant") />

<for:each in="#people#" value="person">
Hello, my name is #person.firstName# #person.lastName#
</for:each>

Nested Array

<cfset people = [] />
<cfset people[1] = {firstName="LeBron", lastName="James"} />
<cfset people[2] = {firstName="Dwyane", lastName="Wade"} />
<cfset people[3] = {firstName="Kobe", lastName="Bryant"} />

<for:each in="#people#" value="person">
Hello, my name is #person.firstName# #person.lastName#
</for:each>

Nested Struct

<cfset people = {} />

<cfset people["LeBronJames"] = {firstName="LeBron", lastName="James"} />
<cfset people["DwyaneWade"] = {firstName="Dwyane", lastName="Wade"} />
<cfset people["KobeBryant"] = {firstName="Kobe", lastName="Bryant"} />

<for:each in="#people#" value="person">
Hello, my name is #person.firstName# #person.lastName#
</for:each>
</cfoutput>


And here's the code
/com/tags/for/each.cfm

<cfif thisTag.executionMode eq "start">

<cfparam name="attributes.value" default="it" />
<cfparam name="attributes.in" default="" />
<cfparam name="attributes.start" default="1" />
<cfparam name="attributes.delimeter" default="," />

<cfset attributes.type = getType(attributes.in) />
<cfset attributes.length = getLength(attributes.in, attributes.type, attributes.delimeter) />

<cfif not structKeyExists(attributes, "end")>
<cfset attributes.end = attributes.length />
</cfif>

<cfif attributes.length>
<cfset processLoop(attributes) />
<cfelse>
<cfexit method="exittag" />
</cfif>

<cfset content = [] />

<cfelse>

<cfset arrayAppend(content, thisTag.generatedContent) />

<cfset thisTag.generatedContent = "" />

<cfset attributes.start++ />

<cfif attributes.start lte attributes.end>
<cfset processLoop(attributes) />
<cfexit method="loop" />
</cfif>

<cfoutput>
#arrayToList(content, "")#
</cfoutput>

</cfif>

<cffunction name="processLoop" access="private" output="false" returntype="void">
<cfargument name="attributes" required="true" type="struct" />

<cfif structKeyExists(attributes, "index")>
<cfset caller[attributes.index] = attributes.start />
</cfif>

<cfif structKeyExists(attributes, "key")>
<cfset caller[attributes.key] = getKey(attributes.in, attributes.type, attributes.delimeter, attributes.start) />
</cfif>

<cfif structKeyExists(attributes, "value")>
<cfset caller[attributes.value] = getValue(attributes.in, attributes.type, attributes.delimeter, attributes.start) />
</cfif>

</cffunction>

<cffunction name="getType" access="private" output="false" returntype="string">
<cfargument name="data" required="true" type="any" />

<cfif isArray(arguments.data)>
<cfreturn "array" />
<cfelseif isStruct(arguments.data)>
<cfreturn "struct" />
<cfelseif isQuery(arguments.data)>
<cfreturn "query" />
<cfelse>
<cfreturn "string" />
</cfif>

</cffunction>

<cffunction name="getLength" access="private" output="false" returntype="numeric">
<cfargument name="data" required="true" type="any" />
<cfargument name="type" required="true" type="string" />
<cfargument name="delimeter" required="true" type="string" />

<cfswitch expression="#arguments.type#">

<cfcase value="array">
<cfreturn arrayLen(arguments.data) />
</cfcase>

<cfcase value="struct">
<cfreturn structCount(arguments.data) />
</cfcase>

<cfcase value="query">
<cfreturn arguments.data.recordCount />
</cfcase>

<cfcase value="string">
<cfreturn listLen(arguments.data, arguments.delimeter) />
</cfcase>

</cfswitch>

</cffunction>

<cffunction name="getKey" access="private" output="false" returntype="string">
<cfargument name="data" required="true" type="any" />
<cfargument name="type" required="true" type="string" />
<cfargument name="delimeter" required="true" type="string" />
<cfargument name="index" required="true" type="numeric" />

<cfset var result = "" />

<cfset var i = "" />

<cfswitch expression="#arguments.type#">

<cfcase value="array">
<cfset result = arguments.index />
</cfcase>

<cfcase value="struct">
<cfset result = listGetAt(listSort(structKeyList(arguments.data), "text"), arguments.index) />
</cfcase>

<cfcase value="query">
<cfset result = arguments.index />
</cfcase>

<cfcase value="string">
<cfset result = listGetAt(arguments.data, arguments.index, arguments.delimeter) />
</cfcase>

</cfswitch>

<cfreturn result />

</cffunction>

<cffunction name="getValue" access="private" output="false" returntype="any">
<cfargument name="data" required="true" type="any" />
<cfargument name="type" required="true" type="string" />
<cfargument name="delimeter" required="true" type="string" />
<cfargument name="index" required="true" type="numeric" />

<cfset var result = "" />
<cfset var i = "" />

<cfswitch expression="#arguments.type#">

<cfcase value="array">
<cfset result = arguments.data[arguments.index] />
</cfcase>

<cfcase value="struct">
<cfset result = arguments.data[listGetAt(listSort(structKeyList(arguments.data), "text"), arguments.index)] />
</cfcase>

<cfcase value="query">
<cfset result = {} />
<cfloop list="#arguments.data.columnList#" index="i">
<cfset result[i] = arguments.data[i][arguments.index] />
</cfloop>
</cfcase>

<cfcase value="string">
<cfset result = listGetAt(arguments.data, arguments.index, arguments.delimeter) />
</cfcase>

</cfswitch>

<cfreturn result />

</cffunction>


It's been quite a while since I've used custom tags, so the code could probably be better, but it seems to do the job.

2 comments:

  1. consistently in coldfusion loops, would be so much nicer if it could be built in originally

    ReplyDelete
  2. This is really nice! I've played around with custom-tag based iterators before, but just the way you're doing it, for:each, feels really good.

    I also like your Groovy-esque use of the implied "it" iteration arguments. Very cool!

    ReplyDelete