Skip to main content
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Joe Casper and Chris Bickford
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Joe Casper ( @joe_casper ) Chris Bickford ( @chris_bickford )

What If ColdFusion Looked Like jQuery

By on
Tags:

I am a Huge fan of jQuery. I love the method chaining and the ability to apply methods to each element in a jQuery collection object. I was wondering if I would want ColdFusion to be able to do that. When I started to experiment, I realized that ColdFusion has two limitations that stop it from being like jQuery:

  1. The biggest limitation is simply the ability to create on-the-fly, nameless functions. It can be done, but it's hard and not very elegant. These anonymous functions are half of what make jQuery (and Javascript in general) so amazingly powerful.

  2. ColdFusion does not have a very solid structure like the browser's Document Object Model (DOM) over which actions can be taken. This is not a limitation of ColdFusion itself, but merely a fact of the sever-side world.

But, I figured there could be jQuery-like structures for ColdFusion. These turned out to be more like data type wrappers, but still, I thought they were very interesting. Here is a demo:

<!---
	Create a strcut with value keys. This is to
	demonstrate that our data wrapper allows the
	adding of STRUCTs to a simple list.
--->
<cfset objValues = StructNew() />
<cfset objValues[ "testa" ] = "likes" />
<cfset objValues[ "testb" ] = "Nancy." />


<!---
	Create an array of values. This is to
	demonstrate that our data wrapper allows the
	adding of ARRAYs to a simple list.
--->
<cfset arrValues = ListToArray( "Nancy,is" ) />


<!---
	Create a jQuery-like wrapper for a ColdFusion
	list value. I am using the $ signe just to drive
	home that this is modelled on jQuery.
--->
<cfset $List = CreateObject( "component", "$List" ).Init() />


<!---
	Now that we have our jQuery-like data wrapper,
	let's start manipulating the list using chained
	methods and an Each method call.
--->
#$List.Prepend( "Ben" )
.Append( "naughty" )
.InsertAt( 2, objValues )
.InsertAt( 4, arrValues )
.Each( EachHandler )
.ToString()#


<!--- This defines the Each Handler for the calls above. --->
<cffunction
	name="EachHandler"
	access="public"
	returntype="string"
	output="false"
	hint="Handles manipulation of each list item. Must return the item.">

	<!--- Define arguments. --->
	<cfargument
		name="Value"
		type="string"
		required="true"
		/>

	<!--- Return cammel-case value. --->
	<cfreturn REReplace(
		ARGUMENTS.Value,
		"^([a-z])",
		"\U\1",
		"ONE"
		) />
</cffunction>

The 6 chained method calls on $List result in the following output:

Ben,Likes,Nancy.,Nancy,Is,Naughty

This demonstrates an Each method that takes a list value and returns an updated value. However, just as in jQuery / Javascript, there is no requirement for the Each method to actually update the list. In this example, we can use the Each method as an iterator that populates a global, REQUEST-scoped value array:

<!--- Create out global values array. --->
<cfset REQUEST.Values = ArrayNew( 1 ) />

<!---
	Build the global array using the Each
	method of the list data wrapper.
--->
<cfset $List.Each( EachHandler2 ) />


<!--- Dump out the global array. --->
<cfdump
	var="#REQUEST.Values#"
	label="Global Value Array"
	/>


<!--- This defines the Each method handler for use above. --->
<cffunction
	name="EachHandler2"
	access="public"
	returntype="void"
	output="false"
	hint="Takes each value and adds it to a REQUEST-scope array.">

	<!--- Define arguments. --->
	<cfargument
		name="Value"
		type="string"
		required="true"
		/>

	<!--- Append this value to our global array. --->
	<cfset ArrayAppend( REQUEST.Values, ARGUMENTS.Value ) />

	<!--- Return void. --->
	<cfreturn />
</cffunction>

CFDumping out the REQUEST.Values array, we get:

ColdFusion As jQuery Demo

I like the idea. I think it has some merit, I am just not sure that ColdFusion is the proper place to apply this type of methodology. Does anyone have any thoughts on this type of thing?

Here is the code that created the $List.cfc ColdFusion component / jQuery-like data wrapper:

<cfcomponent
	output="false"
	hint="Creates functionality around a list.">


	<!---
		Create an instance struct to hold
		instance specific data.
	--->
	<cfset VARIABLES.Instance = StructNew() />

	<!--- Store the list internally as an array. --->
	<cfset VARIABLES.Instance.Values = ArrayNew( 1 ) />

	<!--- Set the default delimiter. --->
	<cfset VARIABLES.Instance.Delimiter = "," />


	<!--- This is a hack to get better naming for functions. --->
	<cfset THIS.ToString = THIS.$ToString />



	<cffunction
		name="Init"
		access="public"
		returntype="any"
		output="false"
		hint="Returns an initialized list object.">

		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />

		<!--- Set up the default value array. --->
		<cfset VARIABLES.Instance.Values = ArrayNew( 1 ) />

		<!--- Set up the default delimiter. --->
		<cfset VARIABLES.Instance.Delimiter = "," />


		<!---
			Check to see if a second argument, the delimiter,
			has been passed in.
		--->
		<cfif (
			StructKeyExists( ARGUMENTS, "2" ) AND
			Len( ARGUMENTS[ 2 ] )
			)>

			<!--- Store the passed in delimiter. --->
			<cfset VARIABLES.Instance.Delimiter = ARGUMENTS[ 2 ] />

		</cfif>


		<!--- Check to see if we had a values set passed in. --->
		<cfif StructKeyExists( ARGUMENTS, "1" )>

			<!--- Append the values to our list. --->
			<cfset THIS.Append(
				Values = ARGUMENTS[ 1 ],
				Delimiters = VARIABLES.Instance.Delimiter
				) />

		</cfif>


		<!--- Return This reference. --->
		<cfreturn THIS />
	</cffunction>


	<cffunction
		name="Append"
		access="public"
		returntype="any"
		output="false"
		hint="Appends list items to the list.">

		<!--- Define arguments. --->
		<cfargument
			name="Values"
			type="any"
			required="true"
			hint="These are the values that you are appending. Can be list, array, struct."
			/>

		<cfargument
			name="Delimiters"
			type="string"
			required="false"
			default="#VARIABLES.Instance.Delimiter#"
			hint="The characters use for list delimiters."
			/>


		<!---
			Append the new values to the end of our
			ineternal value array.
		--->
		<cfset VARIABLES.Instance.Values.AddAll(
			THIS.ToValueArray(
				ArgumentCollection = ARGUMENTS
				)
			) />

		<!--- Return This reference. --->
		<cfreturn THIS />
	</cffunction>


	<cffunction
		name="Delete"
		access="public"
		returntype="any"
		output="false"
		hint="This takes values (list, array, struct) and deletes them from the list.">

		<!--- Define arguments. --->
		<cfargument
			name="Values"
			type="any"
			required="true"
			hint="These are the values that you are deleting. Can be list, array, struct."
			/>

		<cfargument
			name="Delimiters"
			type="string"
			required="false"
			default="#VARIABLES.Instance.Delimiter#"
			hint="The characters use for list delimiters."
			/>


		<!---
			Create an array of the values and then remove
			them all from our internal list.
		--->
		<cfset VARIABLES.Instance.Values.RemoveAll(
			THIS.ToValueArray(
				ArgumentCollection = ARGUMENTS
				)
			) />

		<!--- Return This reference. --->
		<cfreturn THIS />
	</cffunction>


	<cffunction
		name="Each"
		access="public"
		returntype="any"
		output="false"
		hint="Applies the given function to each list item.">

		<!--- Define arguments. --->
		<cfargument
			name="Method"
			type="any"
			required="true"
			/>

		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />

		<!--- Loop over list items to apply method. --->
		<cfloop
			index="LOCAL.Index"
			from="1"
			to="#VARIABLES.Instance.Values.Size()#"
			step="1">

			<!--- Get an updated value. --->
			<cfset LOCAL.Value = ARGUMENTS.Method(
				VARIABLES.Instance.Values[ LOCAL.Index ]
				) />

			<!---
				Theoretically, each of the methods *should*
				return a value. However, this function might
				NOT be returning a value. Therefore, check to
				see if value exists locally before saving it
				back into our list.
			--->
			<cfif StructKeyExists( LOCAL, "Value" )>

				<!--- We have a valid value. --->
				<cfset VARIABLES.Instance.Values[ LOCAL.Index ] = LOCAL.Value />

			</cfif>

		</cfloop>

		<!--- Return This reference. --->
		<cfreturn THIS />
	</cffunction>


	<cffunction
		name="InsertAt"
		access="public"
		returntype="any"
		output="false"
		hint="Inserts the given values at the given index.">

		<!--- Define arguments. --->
		<cfargument
			name="Index"
			type="numeric"
			required="true"
			hint="The index at which to insert the values. LTE 1 will prepend."
			/>

		<cfargument
			name="Values"
			type="any"
			required="true"
			hint="These are the values that you are inserting. Can be list, array, struct."
			/>

		<cfargument
			name="Delimiters"
			type="string"
			required="false"
			default="#VARIABLES.Instance.Delimiter#"
			hint="The characters use for list delimiters."
			/>


		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />


		<!--- Create a local values array. --->
		<cfset LOCAL.Values = THIS.ToValueArray(
			ArgumentCollection = ARGUMENTS
			) />


		<!--- Check the index to see if we are prepending. --->
		<cfif (ARGUMENTS.Index LTE 1)>

			<!--- Return prepending result. --->
			<cfreturn THIS.Prepend(
				ArgumentCollection = ARGUMENTS
				) />

		<cfelseif (ARGUMENTS.Index GT VARIABLES.Instance.Values.Size())>

			<!--- Return appending result. --->
			<cfreturn THIS.Append(
				ArgumentCollection = ARGUMENTS
				) />

		<cfelse>

			<!---
				We actually need to insert the values. In order
				to do that, lets get local copies of the array
				to mess with.
			--->
			<cfset LOCAL.PreValues = VARIABLES.Instance.Values />
			<cfset LOCAL.PostValues = VARIABLES.Instance.Values />


			<!--- Trim the pre-value list. --->
			<cfloop
				index="LOCAL.Index"
				from="#VARIABLES.Instance.Values.Size()#"
				to="#ARGUMENTS.Index#"
				step="-1">

				<!--- Delete the post non-pre values. --->
				<cfset ArrayDeleteAt(
					LOCAL.PreValues,
					LOCAL.Index
					) />

			</cfloop>


			<!--- Trim the post-value list. --->
			<cfloop
				index="LOCAL.Index"
				from="#(ARGUMENTS.Index - 1)#"
				to="1"
				step="-1">

				<!--- Delete the pre non-post values. --->
				<cfset ArrayDeleteAt(
					LOCAL.PostValues,
					LOCAL.Index
					) />

			</cfloop>


			<!--- Add the new value to the pre values. --->
			<cfset LOCAL.PreValues.AddAll(
				LOCAL.Values
				) />

			<!--- Add the post values to that. --->
			<cfset LOCAL.PreValues.AddAll(
				LOCAL.PostValues
				) />

			<!--- Store the new list back into the instance. --->
			<cfset VARIABLES.Instance.Values = LOCAL.PreValues />

			<!--- Return This reference. --->
			<cfreturn THIS />

		</cfif>
	</cffunction>


	<cffunction
		name="Prepend"
		access="public"
		returntype="any"
		output="false"
		hint="Prepends list items to the list.">

		<!--- Define arguments. --->
		<cfargument
			name="Values"
			type="any"
			required="true"
			hint="These are the values that you are prepending. Can be list, array, struct."
			/>

		<cfargument
			name="Delimiters"
			type="string"
			required="false"
			default="#VARIABLES.Instance.Delimiter#"
			hint="The characters use for list delimiters."
			/>


		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />


		<!--- Create a local values array. --->
		<cfset LOCAL.Values = THIS.ToValueArray(
			ArgumentCollection = ARGUMENTS
			) />

		<!---
			At this point, we should have an array of values
			that we need to prepend. To accomplish this, we
			are going to append the exiting list to this new
			list and then save the resultant back into the
			list object.
		--->
		<cfset LOCAL.Values.AddAll( VARIABLES.Instance.Values ) />

		<!--- Store the new list back into our list. --->
		<cfset VARIABLES.Instance.Values = LOCAL.Values />

		<!--- Return This reference. --->
		<cfreturn THIS />
	</cffunction>


	<cffunction
		name="$ToString"
		access="public"
		returntype="string"
		output="false"
		hint="Returns a string representation of the list.">

		<!--- Join all values. --->
		<cfreturn ArrayToList(
			VARIABLES.Instance.Values,
			VARIABLES.Instance.Delimiter
			) />
	</cffunction>


	<cffunction
		name="ToValueArray"
		access="public"
		returntype="array"
		output="false"
		hint="Takes some sort of value (list, array, struct), and returns an array of values.">

		<!--- Define arguments. --->
		<cfargument
			name="Values"
			type="any"
			required="true"
			hint="These are the values that are being converted to an array. Can be list, array, struct."
			/>

		<cfargument
			name="Delimiters"
			type="string"
			required="false"
			default="#VARIABLES.Instance.Delimiter#"
			hint="The characters use for list delimiters."
			/>


		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />


		<!---
			Check to see which type of data is being prepended
			to the list. We can take a string, an array,
			or a struct (from which we will take the values).
		--->
		<cfif IsSimpleValue( ARGUMENTS.Values )>

			<!--- Create a local copy of the new values. --->
			<cfset LOCAL.Values = ListToArray(
				ARGUMENTS.Values,
				ARGUMENTS.Delimiters
				) />

		<cfelseif IsArray( ARGUMENTS.Values )>

			<!---
				The passed in value is an array. Just store it
				locally for now.
			--->
			<cfset LOCAL.Values = ARGUMENTS.Values />

		<cfelseif IsStruct( ARGUMENTS.Values )>

			<!--- Create a local values array. --->
			<cfset LOCAL.Values = ArrayNew( 1 ) />

			<!---
				The passed in value is a struct. We need
				to get the values out of it. Loop over the
				struct to get all the simple values for
				our internal array.
			--->
			<cfloop
				item="LOCAL.Key"
				collection="#ARGUMENTS.Values#">

				<!---
					Check to see if this value is simple.
					We only want simple values since our
					eventual list can only be a string.
				--->
				<cfif IsSimpleValue( ARGUMENTS.Values[ LOCAL.Key ] )>

					<!--- Add to array. --->
					<cfset ArrayAppend(
						LOCAL.Values,
						ARGUMENTS.Values[ LOCAL.Key ]
						) />

				</cfif>

			</cfloop>

		<cfelse>

			<!---
				The values passed in did not match an valid data
				type. Just create an empty array to pass back.
			--->
			<cfset LOCAL.Values = ArrayNew( 1 ) />

		</cfif>


		<!--- Return the values array. --->
		<cfreturn LOCAL.Values />
	</cffunction>

</cfcomponent>

Want to use code from this post? Check out the license.

Reader Comments

36 Comments

JQuery is fun, no doubt. Certainly returning the jQuery object allows some great chaining and very concise syntax.

Your experiment with CF in a jQuery style is interesting though to be true, I'd be happy if I could define arrays and structs using a native notation rather than a series of tags.

Ex.

aStruct{ key1: 'value1', key2: 'value2' }

anArray[ 'one', 'and', 'uh', 'two', 'and', 'uh']

42 Comments

"The 6 chained method calls on $List result in"
code that is impossible to read.

Sorry, but I just don't like the syntax, and can't see the benefit.

Yes, having a 'for each' loop construct would be nice, but no more than that.

198 Comments

@Dan:

Here are 2 functions that give you almost what you want:

<cffunction name="arrayCreate" returntype="array" output="false">
<cfset var aReturn = arrayNew(1) />
<cfset var i = 0 />
<cfloop index="i" from="1" to="#arrayLen(arguments)#">
<!---//
you might want to using duplicate(arguments[i])
if you plan on using complex array items
//--->
<cfset arrayAppend(aReturn, duplicate(arguments[i])) />
</cfloop>
<cfreturn aReturn />
</cffunction>

<cfdump var='#arrayCreate("one", "two", "three", "four")#'>
<cfdump var='#arrayCreate()#'>

<cffunction name="structCreate" returntype="struct" output="false">
<cfset var stReturn = structNew() />
<cfset var sKey = 0 />
<cfloop item="sKey" collection="#arguments#">
<cfset stReturn[sKey] = duplicate(arguments[sKey]) />
</cfloop>
<cfreturn stReturn />
</cffunction>

<cfdump var='#structCreate(key1="value1", key2="value2")#'>
<cfdump var='#structCreate()#'>

24 Comments

@Dan,

I'm with you. I hate typing in TONS of tags to programmatically build a structure or array.

You could easily define you're arrays and structures in JSON format and avoid all the tags by using the JSON encode/decode library for CF to convert the JSON notation to a native CF data structure.

I've thought about doing this for a project I'm working on. I haven't done it yet, but there's on reason it shouldn't work.

CFJSON
http://www.epiphantastic.com/cfjson/

15,688 Comments

@Tom,

I agree with you. At first the jQuery method chaining is awesome. But then, I quickly realize that I am not sure how to best format it.... and to me, when I don't know how to best format the code itself, that is red flag that it needs to be much more simple.

In jQuery, I tend to create the jQuery object and keep a reference to it. Then, I just make multiple calls on it:

o = $( "..." );
o.attr();
o.attr();
o.each();

I find not only is this easier to read, it actually pays off in the end because in the Each() methods, I now have a reference to stuff that might be needed later.

@Rob,

:) The mad scientist is always hard at work.

@Dan G.,

Good stuff. I would like to experiment with that.

@Kurt,

I am with Dan on this one. Plus, by using a string, you are forced to only use data structures that can fit into a string value.

15,688 Comments

Mostly, I just want the EACH() ability. But since ColdFusion is compiled, I guess this is not possible - seems to only be possible in interpreted scripting languages.

24 Comments

@Dan and @Ben,

My point was simply that sometimes there's a need to build a data structure within CF for whatever reason and that as long as all you're storing at the very end nodes of the data structure is simple data such as strings, JSON is a very clean and simple way to do it. It requires MUCH less typing than a bunch of CFSETs and is MUCH easier to understand.

Yes, string parsing is slow, but I'm guessing that most places this technique would be used is to convert a JSON string that never changes into a native CF value. In this case you could simply cache the data structure after it was initially parsed and decoded.

Sorry I wasn't clearer earlier.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel