Using jQuery To Pass Arrays To Remote ColdFusion Components
Yesterday, I was reading an interesting post over on Ray Camden's blog about passing arrays in a jQuery AJAX request to a remote method on a ColdFusion component. Passing complex objects like structs and arrays to a ColdFusion method is trivial when done on the ColdFusion server; but, when this type of method invocation is performed over HTTP, complex objects create for a rather complex situation. As Ray pointed out on his blog, when you try to post an array of values in an AJAX request, jQuery serializes the array using an explicit object notation.
Assuming that I am submitting a Javascript array of girl names, my HTTP params might look like this:
girls[]: Tricia
girls[]: Vicky
girls[]: Erika
I don't fully understand all of the notation that is being used in object serialization; if you want to learn more about it, Ben Alman has a great post about how the $.param() method has been updated in jQuery 1.4 to accommodate more explicit object serialization. That said, for this exploration, we'll stick with a one dimensional array of simple values.
To look at how these values are passed, and how ColdFusion can use them, I've created a Math ColdFusion component:
Math.cfc
<cfcomponent
output="false"
hint="I am a test component with remote-access methods.">
<cffunction
name="sum"
access="remote"
returntype="numeric"
returnformat="json"
output="false"
hint="I sum all the numbers in the given array.">
<!--- Define arguments. --->
<cfargument
name="values"
type="array"
required="true"
hint="I am array of numeric values."
/>
<!--- Return array sum. --->
<cfreturn arraySum( arguments.values ) />
</cffunction>
</cfcomponent>
As you can see, this ColdFusion component has a single remote-access method that takes an array of numeric values and returns their sum. This method is expecting a valid ColdFusion array as its only argument; which means, we have to figure out how to convert the above object notation into a native ColdFusion data type.
Before we see the mechanism behind the data conversion, let's get some context and take a look at the jQuery code that will end up consuming our remote ColdFusion component method:
<!--- Create the Math object to get a static sum (for testing). --->
<cfset math = createObject( "component", "Math" ) />
<!--- Create an array of numbers to sum. --->
<cfset values = [ 1, 2, 3 ] />
<!DOCTYPE HTML>
<html>
<head>
<title>Using jQuery To Pass Arrays To Remote ColdFusion Components</title>
<script type="text/javascript" src="../jquery-1.4.2.js"></script>
<script type="text/javascript">
// jQuery.ajaxSettings.traditional = true;
// When the DOM is ready, initialize the scripts.
$(function( $ ){
// Sum an array of values remotely.
$.ajax({
type: "get",
url: "./Math.cfc",
data: {
method: "sum",
values: [ 1, 2, 3 ],
},
dataType: "json",
success: function( sum ){
// Store the respone in the DOM.
$( "#sum" ).text( sum );
}
});
});
</script>
</head>
<body>
<h1>
Using jQuery To Pass Arrays To Remote ColdFusion Components
</h1>
<p>
<cfoutput>
Static Sum: #math.sum( values )#
</cfoutput>
</p>
<p>
AJAX Sum:
<span id="sum"></span>
</p>
</body>
</html>
As you can see, our AJAX method sets the "values" argument to a Javascript array of numeric values:
values: [ 1, 2, 3 ]
It is this Javascript array that we must convert to a ColdFusion array in order for our remote method to be invoked without error. Assuming for a moment that this data type happens magically, when we run the above code, we get the following output:
Static Sum: 6
AJAX Sum: 6
As you can see, jQuery was successfully able to sum the given values using the remote ColdFusion method invocation.
Both jQuery and the remote ColdFusion method were written as if this nothing special were going on. So, how is the Javascript array being converted into a ColdFusion array? The magic happens in our ColdFusion Application Framework. In the ColdFusion application framework, we have the application event, onRequestStart(). This event doesn't just give us a place to initialize some variables - as is often how we use it - this event gives us almost total control over how our request is processed. In this case, the onRequestStart() event handler is a perfect place to sanitize any complex data coming from a jQuery request.
Application.cfc
<cfcomponent
output="false"
hint="I define the application settings and event handlers.">
<!--- Define the application. --->
<cfset this.name = hash( getCurrentTemplatePath() ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) />
<!--- Define request settings. --->
<cfsetting
requesttimeout="10"
showdebugoutput="false"
/>
<cffunction
name="onRequestStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the request.">
<!--- Define the local scope. --->
<cfset var local = {} />
<!---
Loop over the URL scope looking for serialized values
(passed by jQuery array serialization). These will come
through as something like:
values[] : 1,2,3
We need to conver them back to true ColdFusion array.
--->
<cfloop
item="local.key"
collection="#url#">
<!--- Check for array notation. --->
<cfif find( "[]", local.key )>
<!---
Get the true value of the url key (without the
embedded brackets).
--->
<cfset local.cleanKey = replace(
local.key,
"[]",
"",
"one"
) />
<!---
Convert value to a true ColdFusion array. Since
the compound field is posted as a comma-delimted
list, then we can use list-to-array conversion.
--->
<cfset url[ local.cleanKey ] = listToArray(
url[ local.key ]
) />
<!---
Now that we have cleaned the key, we can deleted
the compound key from the URL scope.
--->
<cfset structDelete( url, local.key ) />
</cfif>
</cfloop>
<!--- Return true so the rest of the page can process. --->
<cfreturn true />
</cffunction>
</cfcomponent>
When jQuery posts the Javascript array, it serializes it using the explicit object notation:
girls[]: Tricia
girls[]: Vicky
girls[]: Erika
The ColdFusion application server then takes that series of values and concatenates them into a single comma-delimited list:
girls[]=Trica,Vicky,Erika
This is what our ColdFusion application then sees in the URL struct. It is this URL struct that is then eventually used to invoke the remote ColdFusion method. Our onRequestStart() event handler, however, gives us a chance to alter the URL scope after it has been initialized by the ColdFusion server, but before it is used to invoke the remote method. As such, the onRequestStart() event handler gives us an opportunity to convert that comma-delimited list into a native ColdFusion array to be used as one of the method arguments.
As you can see in the Application.cfc above, that is exactly what we're doing. We're taking the current URL entry:
url[ "girls[]" ] = "Tricia,Vicky,Erika"
... and we're converting it to a native ColdFusion array:
url[ "girls" ] = [ "Tricia", "Vicky", "Erika" ]
When the ColdFusion application server then invokes the remote method, the value contained with the URL struct matches the native array data type require by our method argument.
I've never used jQuery to pass complex objects to ColdFusion before, so I'm not sure how I actually feel about it. But, feelings aside, if you want to pass complex values, the ColdFusion application framework provides an elegant way to allow this type of complex data conversion to happen seamlessly.
Want to use code from this post? Check out the license.
Reader Comments
This is a "perfect world" scenario and array girl names is easy. What if you were dealing with "boys" and you had to deal with a Sr., Jr. or any value that has a comma (ie, titles or products)? Using ListtoArray() will result in incorrect values.
I've had to resort to converting data to JSON when sending arrays or structures back to the server using jquery-json and using deserializeJSON():
http://code.google.com/p/jquery-json/
http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_43.html
@James,
That's a good point; when you have commas in the complex values, it get's hairy on the server side. I suppose what works in other languages is that they concatenate to lists, but rather arrays. Lists are so easy to work with. I guess arrays are as well.
JSON seems like a good approach to solving your problem. I'm gonna play around with some more server-side stuff.
I have to ditto James here Ben - you linked to my article, but I don't think you read it closely - as that (data with commas) was the main issue I was solving. ;)
That being said, I do like the code you used - for cases where you might not have control over the form of the data (like perhaps a remote PHP POST), that could come in handy.
I wonder if you can use getHTTPRequestData to get the stuff pure, so if you had
names[]="Ray,Camden"
names[]="Foo"
you could handle it gracefully.
Ok, I reserve the write to talk about this on my blog too. ;) Imagine the following form:
<form action="test4a.cfm" method="post">
<input type="text" name="name" value="Camden,Raymond">
<input type="text" name="name" value="Smith,John">
<input type="submit">
</form>
As you can see, I've got two name values, both with embedded commas. In the form scope, you get form.name=Camden,Raymond,Smith,John, which is useless. However, getHTTPRequestData clearly shows nicely encoded content:
name=Camden%2CRaymond&name=Smith%2CJohn
So you can then do this (and this can be made more generic):
<cfset req = getHTTPRequestData()>
<cfset data = []>
<cfif len(req.content)>
<cfloop index="item" list="#req.content#" delimiters="&">
<cfset arrayAppend(data, urlDecode(listLast(item, "=")))>
</cfloop>
</cfif>
<cfdump var="#data#" label="Data">
@Raymond,
Sorry - I did not intend to draw a comparison between our two approaches. I just meant that your post got me thinking about the passing complex data; it was something that I have never tried to do with a jQuery post before and when I saw your screen shot of the HTTP params, it just got my mind working.
I'm working on my SOTR topic on the ColdFusion Application Framework and I my mind just took off. That said, yes, I totally did miss the part about the comma - oops :)
Work harder Ben - it's mistakes like that that made those Mars orbitals crash. ;)
@Ben,
I give you complete blog dibs... I need more caffeine :D
Hi Ben,
I am a huge fan of jQuery's AJAX methods, thanks a lot for this posting.
Just as an aside, up until now, I have been using JSMX - http://www.lalabird.com/ - for my ajax calls to Coldfusion objects. I discovered that it automatically serialises form objects, one-dimensional javascript arrays, and javascript objects into objects that can be consumed in Coldfusion.
Any any array or struct being returned from your cfc is also serialized into a javascript array, which is very cool. I have found it very useful for most of my AJAX requirements.
Hey Ben,
I had a need for this type of functionality and did a bit more fiddling. A potential solution is actually really simple (then again, isn't everything once you figure out how to do it!):
http://www.grantshepert.com/post.cfm/posting-json-to-coldfusion-via-jquery
@Steve,
That looks very interesting. I'll have to take a closer look at that.
@Grant,
Seems like solid stuff. JSON is such a great data packaging tool.
Ben,
I took a different approach to this. I'm encoding each variable as a JSON string using the json2.js library before using getJSON. I then use OnCFCRequest (CF9, but could use onRequestStart in older versions) to loop through each variable in the Arguments scope and see if it is JSON encoded. If it is, I deserialize it and save it back to the scope before passing it off to the original CFC and method requested.
I did it this way for 3 reasons:
1) I wanted to be able to use the same exact methods in each CFC whether they are called from coldfusion or Jquery.
2) I used the onCFCRequest (after seeing this and Ray's blog posts) because I also wanted to keep from changing the argument types to any, which I would have had to do to filter the argument scope from within the method.
3) I only had to write the filter code once, as opposed to include it in each method, be it as code or as a function.
It seems to be working for just about any data type I throw at it with little performance loss.
I haven't posted code, but can if you're interested in taking a look.
Regards,
Sean Ford
@Sean,
I'm sorry, but I love the fact that the ColdFusion application framework allows you to do stuff like this. It's just such a powerful framework.
I appreciate you wanting to post code; unfortunately, code does not get formatted well in the comments. Maybe post it to something like JSBin and then post the link here if you like??