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.
consistently in coldfusion loops, would be so much nicer if it could be built in originally
ReplyDeleteThis 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.
ReplyDeleteI also like your Groovy-esque use of the implied "it" iteration arguments. Very cool!