Raymond Camden's Blog Rss

Ask a Jedi: ColdFusion Ajax example of retrieving fields of data

23

Posted in jQuery, ColdFusion | Posted on 10-18-2009 | 4,234 views

I have to apologize for the title of this blog entry. I can't honestly think of a nicer way to say it. But enough of that, let me just get to the question.

I have searched all over the web and read countless articles and documentation, tried different examples but i just cant seem to figure out what i first thought was a relatively simple request and solution, maybe it is but i cant seem to figure it out, anyway enough of my waffling here's the scenario:

What i have is a very straight forward form with say 5 different text inputs. What i would like to do is on the first input box, which say is an invoice number, is for the user to enter the invoice number and then hit a button next to the input box and with the power of Coldfusion's built in AJAX stuff go to a cfc which has a query in it which searches for the invoice number, if one is found return the invoice number plus its details back to the form via AJAX, so all of the 5 text boxes would have returned data in them and the form isnt refreshed.

As with most things in ColdFusion, there are a couple ways of doing this. Let's start with one example and then I'll show a way to simplify it. First though, we need a simple form. I'll use the cfartgallery sample database for my example.

view plain print about
1<cfform>
2id: <cfinput type="text" name="artid" id="artid"><br/>
3name: <cfinput type="text" name="artname" id="artname"><br/>
4description: <cftextarea name="description" id="description"></cftextarea><br/>
5price: <cfinput type="text" name="price" id="price"><br/>
6</cfform>

I've got a form with 4 inputs. The first one is our primary ID field. When the user enters data there, we then want to load the 3 other fields with the corresponding data. One way to do that would be with binding.

view plain print about
1<cfform>
2id: <cfinput type="text" name="artid" id="artid"><br/>
3name: <cfinput type="text" name="artname" id="artname" bind="cfc:test.getName({artid@keyup})" readonly="true"><br/>
4description: <cftextarea name="description" id="description" bind="cfc:test.getDescription({artid@keyup})" readonly="true"></cftextarea><br/>
5price: <cfinput type="text" name="price" id="price" bind="cfc:test.getPrice({artid@keyup})" readonly="true"><br/>
6</cfform>

I've modified the 3 form fields now so that each of them is bound to the ID field. (I also added a readonly flag just to make it clear to the user that these fields are bound to the back end data.) For each field we run a differnent method: getName, getDescription, and getPrice. These are all bound to one CFC:

view plain print about
1<cfcomponent>
2
3<cffunction name="getData" access="remote">
4    <cfargument name="artid" required="true">
5    <cfset var q = "">
6    
7    <cfquery name="q" datasource="cfartgallery">
8    select    *
9    from    art
10    where    artid = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.artid#">
11    </cfquery>
12    
13    <cfreturn q>
14</cffunction>
15
16<cffunction name="getDescription" access="remote">
17    <cfargument name="id" type="any">
18    <cfif not isNumeric(arguments.id) or arguments.id lte 0>
19        <cfreturn "">
20    </cfif>
21    <cfreturn getData(arguments.id).description>
22</cffunction>
23
24<cffunction name="getName" access="remote">
25    <cfargument name="id" type="any">
26    <cfif not isNumeric(arguments.id) or arguments.id lte 0>
27        <cfreturn "">
28    </cfif>
29    <cfreturn getData(arguments.id).artname>
30</cffunction>
31
32<cffunction name="getPrice" access="remote" returntype="string">
33    <cfargument name="id" type="any">
34    <cfif not isNumeric(arguments.id) or arguments.id lte 0>
35        <cfreturn "">
36    </cfif>
37    <cfreturn getData(arguments.id).price>
38</cffunction>
39
40</cfcomponent>

As you can see, the 3 methods all make use of a central getData() method. That method runs the query and then the individual queries all just return one field. (ColdFusion 9 users - please read my PS at the end.) This works, but as you can guess, each time we type into the ID field we perform 3 Ajax requests. This isn't horrible - and the data being returned is rather small, but multiple network requests for the same row of data could probably be done better. Let's look at a modified version.

view plain print about
1<cfajaxproxy bind="cfc:test.getData({artid@keyup})" onsuccess="showData">
2
3<script>
4
5function showData(d) {
6    //convert into a struct
7    var data = {}
8    for(var i=0; i < d.COLUMNS.length; i++) {
9        data[d.COLUMNS[i]] = d.DATA[0][i]
10    }
11    document.getElementById('artname').value = data["ARTNAME"]
12    document.getElementById('description').value = data["DESCRIPTION"]
13    document.getElementById('price').value = data["PRICE"]
14    
15}
16</script>
17
18<cfform>
19id: <cfinput type="text" name="artid" id="artid"><br/>
20name: <cfinput type="text" name="artname" id="artname" readonly="true"><br/>
21description: <cftextarea name="description" id="description" readonly="true"></cftextarea><br/>
22price: <cfinput type="text" name="price" id="price" readonly="true"><br/>
23</cfform>

This code removes all the bindings from the fields. Instead we use cfajaxproxy to create a binding between the ID field, a CFC, and a JavaScript function. Whenever the ID field is changed, we run the getData method from the CFC and pass the results to our JavaScript function. Then the function just has to parse the result and set the form fields. Much easier, right? Now we have one Ajax request that fetches all the data. Technically we are passing back the same amount of data, but with only network request we will be less prone to suffer from congestion over the intertubes.

Oh, and because I couldn't help myself, I whipped up a quick jQuery version. I'll just paste the script blocks as that's the only thing that changed.

view plain print about
1<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
2<script>
3$(document).ready(function() {
4
5    $("#artid").keyup(function() {
6        var artid = $(this).val()
7        if(isNaN(artid)) return
8        
9        $.getJSON("test.cfc?method=getdata&artid=" + artid + "&returnformat=json", {}, function(d,status) {
10            var data = {}
11            for(var i=0; i < d.COLUMNS.length; i++) {
12                data[d.COLUMNS[i]] = d.DATA[0][i]
13            }
14            $("#artname").val(data["ARTNAME"])
15            $("#description").val(data["DESCRIPTION"])
16            $("#price").val(data["PRICE"])
17        })
18
19    })
20})
21</script>

Enjoy!

PS: There is a fairly serious bug with ColdFusion 9 and returning JSON from CFCs. Before I post details though I'm waiting for the guy who found it to let me know if he has blogged it already. For now though note that the returnType="string" on getPrice is required in ColdFusion 9.

Comments

[Add Comment] [Subscribe to Comments]

Hi Ray,
Many thanks for posting this example. I didn't expect to see an example posted so quickly.
This is really going to help me out and i appreciate your time taken to post these examples.
Also thank you for posting more than one variation, this also helps in understanding the best method to use.
From your example its seems so straight forward, i guess its true in what they say, knowledge is power :)
Many thanks again
Matt
Very cool Ray. I didn't know about the onsuccess attribute of cfajaxproxy.
Ray, here's an interesting twist. What if I would like to achieve the same thing but using a cfhttp to get my data instead or a query (data comes from a webservice)? The webservice contains data for st type, st name, city, and state... the xml coming back needs to be stripped, and I'm using cfhttp because the webservice is a .net data set that cfinvoke can not return properly... any ideas on how that maybe achieved?

Here's a sample code:    
<cfhttp method="get" url="webservice.asmx/method?string=#form.variable#"></cfhttp>
<cfset Result = xmlparse(cfhttp.FileContent)>
<cfset xmlRoot = Result>
<cfset nodes = xmlSearch(xmlRoot, "//tbCEP")>
<cfif arrayLen(nodes)>
<cfoutput>#nodes[1].logradouro#</cfoutput>
Results: <br />
<cfoutput>
stType = #nodes[1].sttype# <br/>
stName = #nodes[1].name# <br/>
</cfoutput>

this would then need to go back to the form page and populate a form. I'm using the cfoutput just forn illustration...
Any ideas? Sure. Use the same code. :) Seriously - it doesn't matter _where_ the data comes from. It matters how the data _looks_. Or the form of it. My client side code expected a query of data back. You can either convert the web service result into a query (using queryNew), or edit the client side code to work with the different form of data.
Hey Ray,

I did that. I used QueryNew and tested the code several times... it worked fine until the Ajax part... the fields are populating, but they are populating with "undefined"...

Here is a snippet of my form page: http://i3.photobucket.com/albums/y95/abitdodgy/For...

And here is a snippet of my component page:
http://i3.photobucket.com/albums/y95/abitdodgy/Com...

I'm really not sure why it's returning undefined... :/
It maybe worth mentioning that the code works only if a valid cep is insert... 04735005 works for example... if the cep is not valid the page errors out.. but replacing the ajax with this:

<cfif IsDefined("Form.GetInfo")>
   <cfinvoke component="CheckCEP" method="ReturnAddress" returnvariable="q_AddressData">
   <cfinvokeargument name="strcep" value="#form.strcep#">
   </cfinvoke>
<cfdump var="#q_AddressData#">
</cfif>
And using a correct CEP actually returns a good result!
What does Firebug show you?
I'm a bit stumped to be honest, because my Firebug knowledge does not go farther than CSS... embarrassed to say this, I don't know what to look for, but I found this while tinkering in the Scripts tag:
({COLUMNS: ["STTYPE", "STNAME", "AREA", "CITY", "STATE"], DATA: [["Rua", "Sao Benedito", "Santo Amaro", "Sao Paulo", "SP"]]});
2 /* !eval(new String("("+json+");)) */

The values you see under, well, they are correct. If I query 04735005, I do get the data shown above from the webservice... however, it's coming back to the fields as undefined for some reason! :/
Ok, so this means the data comes to the client ok. The bug then is in how you use the data to write it back out on the page. So let's debug. You are aware of how to use console.log(), right? I will assume so.

First, notice my main loop. Let's see if it is doing anything.Before

for(var i=0; i < d.COLUMNS.length; i++) {

add

console.log("The length of cols is "+d.COLUMNS.length);
Solved. It works perfectly. The when changing the fieldnames in the js, I did not capitalize them! Many thanks!
I did the whole example step by step and im getting the next error

"Error invoking CFC test.cfc : Internal Server Error [enable debugging by adding 'cfdebug' to your URL parameters to see more information]"

actually im getting into CF coz i always was a php fan .. and i dont have a clue of what im doing wrong so if you can help me .. thnx a lot :D

c yas
Former (or considering to be former) PHP folks are ALWAYS welcome here. ;)

Ok, so the best thing to do is use Firebug, or Chrome Dev tools, to inspect what was returned. Are you familiar with those tools?
yup Rai im familiar with those tools and this is what i got

http://imageshack.us/photo/my-images/823/unledgwi....
Use the network tool to view the CFC request and examine the response.
I don't think that's right. Those look like the main CFM requests. In the network tab, click on XHR, and you should see the CFC request there.

Btw, if this is online, I can do it for you.
ya're right i was wrong
here there're the screens (the correct ones)

http://imageshack.us/photo/my-images/98/26786048.p...
http://imageshack.us/photo/my-images/269/89995251....
http://imageshack.us/photo/my-images/705/40229919....

and no .. it's not online coz my boss says that Ajax leave a hole in the server security and im not allow to put ajax content online :/
If you read the result there it's telling you to enable robust exception info. This is done in the CF Admin. Once you do, there is no need to post screen shots. You are in the right area. Just look at the detailed error. It's going to be raw html, but you should be able to read it.

Or - I think you can also right click in Chrome/Firebug and open the request in a new tab.

Either way - enable robust exception info and it will tell you the line # of your error and what it is.

Also, your boss is wrong. Completely. A AJAX request is no more or less secure than a non-AJAX request.
Ok thanks for your help,

now :P

what is "arguments" in the code?
i see that you pass it in the getData function query and then you use it in the other function, but i dont see where it come from.
Inside a CFC method, the Arguments scope represents everything passed in. You don't have to use arguments.x to refer to the X argument, but it helps differentiate it in code against locally created variables, or global variables in the Variables scope.
waahoo!!

i just wanted to ask coz i removed the "arguments" from the test.cfc and now it works well, as long as my "artid" field is not null, when it's empty i get the same error that i told ya .. so is there any way to make it i dont know ... with a button or something ? :)
Well, if arguments.id didn't work, it implies your front end code isn't passing it along when it should be. You want to keep that in there for sure.

I'd restore the CFC code to match mine. Run it again and tell us what the error is exactly. If it complains that arguments artid doesn't exist, then you need to use Firebug/CHrome again to look at the XHR but this time focus on the Request, not the Response. If you don't see artid in there something is amiss. Ensure you didn't typo when copying the client portion of the code.

Again - if I could see this in action, it would be a lot quicker to debug. Maybe ask your boss if you can put it up someplace temporary and then email me directly.
aahhh cool, thanks, you help me a lot!

[Add Comment] [Subscribe to Comments]