Mike asks:
I'm trying to do someting i think may be simple, but its method is eluding me. I want to get to grips with using the cfajaxproxy tag to allow me to have a cfselect that when selecting values will auto populate some information into other fields, having pulled the info from the db.
I've blogged about cfajaxproxy before and it's truly one of the most amazing tags ever added to the language. I quickly built up an example of what I think Mike was talking about. I first started with related selects that spoke to the cfartgallery demo datasource. I started with this since Forta had already written the code. ;)
Here is what I began with:
<cfform name="main">
<b>Media:</b> <cfselect bind="cfc:art.getMedia()" bindonload="true" value="mediaid" display="mediatype" name="media" />
<b>Art:</b> <cfselect bind="cfc:art.getArt({media})" bindonload="true" value="artid" display="artname" name="art" />
Nothing too complex here. You select media and you get art populated in the second drop down. Now this is where I want to demonstrate the use of cfajaxproxy. I added a few form fields and other content:
<p>
<b>Description:</b> <cftextarea name="desc" id="desc" /><br>
<b>Price:</b> <cfinput name="price" id="price" /><br>
<span id="img"></span>
</cfform>
I have 3 areas now. The first two, description and price, I want to populate with the description and price of the art. The span, img, will be populated with the art image.
The first thing I want to do is bind to the art drop down. This will let me say, "When the art drop down changes, do something."
<cfajaxproxy bind="cfc:art.getArtDetail({art.value})" onSuccess="showDetail">
In this example, I've bound to the Art drop down, and I've called a CFC (the same CFC as before) to get more information about the art piece. I've then told the tag to run showDetail when done. That JavaScript function is rather trivial:
<script>
function showDetail(r) {
document.getElementById("desc").value = r.DESCRIPTION;
document.getElementById("price").value = r.PRICE;
var newbod = "<img src='http://localhost/cfdocs/images/artgallery/" + r.LARGEIMAGE + "'>";
document.getElementById("img").innerHTML = newbod;
}
</script>
Since my CFC method returns a struct, I can treat it in JavaScript pretty much the same way I treat it in ColdFusion.
Here is the complete CFC code:
<cfcomponent output="false">
<cfset variables.dsn="cfartgallery">
<!--- Get array of media types --->
<cffunction name="getMedia" access="remote" returnType="query">
<!--- Define variables --->
<cfset var data="">
<!--- Get data --->
<cfquery name="data" datasource="#variables.dsn#">
SELECT mediaid, mediatype
FROM media
ORDER BY mediatype
</cfquery>
<!--- And return it --->
<cfreturn data>
</cffunction>
<!--- Get art by media type --->
<cffunction name="getArt" access="remote" returnType="query">
<cfargument name="mediaid" type="numeric" required="true">
<!--- Define variables --->
<cfset var data="">
<!--- Get data --->
<cfquery name="data" datasource="#variables.dsn#">
SELECT artid, artname
FROM art
WHERE mediaid = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.mediaid#">
ORDER BY artname
</cfquery>
<!--- And return it --->
<cfreturn data>
</cffunction>
<cffunction name="getArtDetail" access="remote" returnType="struct">
<cfargument name="artid" type="numeric" required="true">
<cfset var data="">
<cfset var c = "">
<cfset var s = structNew()>
<!--- Get data --->
<cfquery name="data" datasource="#variables.dsn#">
SELECT *
FROM art
WHERE artid = <cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.artid#">
</cfquery>
<cfloop list="#data.columnlist#" index="c">
<cfset s[c] = data[c][1]>
</cfloop>
<!--- And return it --->
<cfreturn s>
</cffunction>
</cfcomponent>
There isn't anything really complex here. Now here is the complete test document I used:
<cfajaxproxy bind="cfc:art.getArtDetail({art.value})" onSuccess="showDetail">
<script>
function showDetail(r) {
document.getElementById("desc").value = r.DESCRIPTION;
document.getElementById("price").value = r.PRICE;
var newbod = "<img src='http://localhost/cfdocs/images/artgallery/" + r.LARGEIMAGE + "'>";
document.getElementById("img").innerHTML = newbod;
}
</script>
<cfform name="main">
<b>Media:</b> <cfselect bind="cfc:art.getMedia()" bindonload="true" value="mediaid" display="mediatype" name="media" />
<b>Art:</b> <cfselect bind="cfc:art.getArt({media})" bindonload="true" value="artid" display="artname" name="art" />
<p>
<b>Description:</b> <cftextarea name="desc" id="desc" /><br>
<b>Price:</b> <cfinput name="price" id="price" /><br>
<span id="img"></span>
</cfform>
Pay special attention to how simple the code is here. The only JavaScript isn't really that complex.
I hope this example helps.
Archived Comments
Ray,
I can follow the code for the most part, but what I'm not understanding is the instantiation of the JavaScript function. Specifically this line:
function showDetail(r){
What does the "r" refer to and how does the JavaScript function know when to fire? Should <cfajaxproxy> have an id or name equal to r?
r is the result from the server. It can be anything. In my case, r is a struct (look at the CF code).
I'm with Andrew. Where is 'r' defined? The CFC that is bound by the cfajaxproxy tag (getArtDetail) returns a struct 's' but is not set to a variable that I can see. What am I missing?
Could you just call it 'bozo' if you wanted to instead of 'r' as long as the 'onSuccess' value matches the function name?
Yes, you can call it whatever. This is no different than a UDF in CF. Imagine this simple UDF:
<cfscript>
function hi() { var msg = "Hi world"; return msg; }
</cfscript>
<cfset result = hi()>
In my udf that variable is called msg, but all I really return is the data.
Ray,
Let's say you had a cfc similar to yours execpt that there was also an init() method through which you passed the dsn and placed it in variables scope, which the other methods would reference as needed. Pretty standard fare, right?
But I can't seem to get this to work using cfajaxproxy. can you initialize a cfc into memory and then bind to it? I know there's an easy answer to this and I'm going to feel really dumb...
Heh, I blogged on this about 8 days ago. :)
http://www.coldfusionjedi.c...
Perfect - thank you, Ray.
When I tried using the <cfselect>s as in your example, I got the error:
The value of the QUERY attribute is invalid. The 'Query' attribute must be defined if the 'Value', 'Display', or 'Group' attributes are defined.
The <cfselect>s have to be pared down to include only the bind, bindonload and name parameters in order to work properly. Or am I missing something?
Odd, it works just fine locally. You are using CF8, right?
Yep, and as soon as I remove the "value" and "display" parameters it works fine.
All I can suggest is checking to ensure you don't have an older CF8. Make sure you have final.
Ray,
You were right...you don't have to define r to use it in the function. When I got home I read the section in CFWACK VOl II, then I tried your code and it worked, except for the fact that not all media are represented in the art table. For those media that do not have art associated with it, I get an error. Other than that, I now understand how the code works and look forward to the opportunity to use it in my apps when it's appropriate.
Awesome. Straightforward example that shows how powerful ajaxproxy is. Thanks.
Hey Gang,
I'm trying to bind a select box to two different buttons but so that the values in the select box will update anytime either of the two buttons are pressed. The only way I could get this to work was to pass useless variables to my cfc so I could use the @click handler. Is there another way to do this without have to pass unneeded variables?
<cfform name="phoneRequestForm">
<input name="addPhoneButton" type="button" value="Add Phone" onclick="insertPhone();" />
<input name="deletePhoneButton" type="button" value="Delete Phone" onclick="deletePhone();" />
<cfselect id="AvailableIDs" name="AvailableIDs" bind="cfc:phoneRequestMain.SelectAvailablePhones({addPhoneButton@click},{deletePhoneButton@click})" bindonload="true" value="PhoneID" display="PhoneID" />
</cfform>
Ray - Love the blog... it has been a great help to me.
So wait - are you saying you want the button click to call a CFC, but you don't want to pass any data? I'd probably just use onClick, and call a JS function that makes use of AjaxProxy to call then CFC.
Right, my other js functions (insertPhone,deletePhone) uses AjaxProxy to update the database and I want to reflect those changes in my select that is bound to a cfc that just returns a list of phones. I could update the select in the callbackhandler for insertPhone and deletePhone (by making another ajax call to my SelectAvailablePhones cfc) but I thought binding would be neater. It seems like the purpose of binding is attach an element to changing data but maybe in this case it is more for binding to other changing elements.
I get an errors when i try to run this example:
Error invoking CFC /art.cfc: The MEDIAID argument passed to the getArt funtion is not of type numeric
Error invoking CFC /art.cfc: The ARTID argument passed to the getArt funtion is not of type numeric
Can you show me this anywhere online Howard? I know it works for most folks.
Howard, I too get the same errors. I tried Forta's example and got the same errors.
Error invoking CFC /art.cfc: The MEDIAID argument passed to the getArt funtion is not of type numeric
Error invoking CFC /art.cfc: The ARTID argument passed to the getArt funtion is not of type numeric
I get the same error bit I have no problem with Fortas example where he converts the query result to an array.
I'm running this on my local machine so I can't show anybody but I'm using Rays' code exactly.
If you can find a way to get it online I'd appreciate it. Otherwise I'm not sure what to recommend.
I'm at a complete loss. I've stripped this code down as far as it can possibly go, and there is nothing I can do to get the onSuccess function to fire. This is the HTML in its entirety:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1...">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
<script>
function noteRefresh(theResult) {
alert('noteRefresh');
}
</script>
</head>
<body>
<cfform name="theForm">
<cfinput src="../img/pin_green.gif" value="7" type="image" id="pin7" name="pin7" width="16" height="16" alt="Subject 7">
</cfform>
<cfajaxproxy bind="cfc:cfc.getNote({pin7.value@mouseover})" onSuccess="noteRefresh" />
</body>
</html>
The cfc, which does nothing but return a string returns its string, according to the ?CFDEBUG debugger. Yet the onSuccess does not fire. FireBug breakpoints to not trip, nothing happens. I've moved the tags around, changed everything it's possible to change and still nothing. I even put var i; i = 1; in the function so that it didn't invoke any UI, but still nothing.
Do you haven /any/ idea why?
The cfc in its entirety is:
<cfcomponent>
<cffunction name="getNote" access="remote" >
<cfargument name="id" type="numeric" required="yes" />
<cfreturn "A string returned from cfc">
</cffunction>
</cfcomponent>
Holy crap! I think I found it!!!
The directory containing the cfc has an Application.cfm that has a <cfajaximport> and <htmlhead> tag in it. If I remove those two from Application.cfm, it works!
So there must be some unwritten rule about not putting those in the application.cfm?
Well, I'd definitely not put the cfhtmlhead in app.cfm as it would impact all requests. ANd cfajaximport really should be on the page it is needed on.
Ray, in response to Richard Baldwin's question, you said,
"Are you saying you want the button click to call a CFC, but you don't want to pass any data? I'd probably just use onClick, and call a JS function that makes use of AjaxProxy to call then CFC."
Would you mind showing a small example of this? All I am doing is having a text field display thevalue returned from my cfc function when a button is clicked - so am passing the button@click value, but it seems strange to have to have a function argument that accepts this value if it isn't being used in the function.
<cfinput type="text" name="next_id" bind="cfc:mycfc.getNextID({get_next_id_button@click})" />
<cffunction name="getNextID" access="remote" returntype="numeric">
<cfargument name="useless_cfajax_argument" required="no" />
...
<cfreturn nextID>
</cffunction>
Great blog, Ray.
Sorry - what do you need an example of? It sounds like you get it - but you find it odd that you don't care about the value - but since this is a button, I think that's normal. We just care that the button was clicked.
THANK YOU RAY!
I've been poking around the internets, trying to find a simple way to populate multiple form fields from a single function in a CFC (i.e., I don't want to bind each of the CFINPUT fields and have 10 queries running against the DB)... and you did it for me!
I'm querying an employee record based on autosuggested e-mail in the first function and then using your CFAJAXPROXY to call the second function which populates the rest of the fields on the form.
Works perfectly and saves me much work and hair pulling!
Thanks Ray!
Exactly the kind of example that I was looking for - short, sweet, and to the point.
Ray, this is great. I can everything to work, except the values passed to the cftext and cfinput fields. Instead, it shows "undefined." And ideas?
No idea. I'd suggest looking w/ Firebug to see what data was returned.
I just want to add that one cannot use this comment tag when using cfajaxproxy bind:
"<!-- some comment -->"
I had to install firebug to find that out. I wasted a few hours beforehand. Strange that the returned data included those comments. I didn't read the entire cfajaxproxy doc, must have been documented..
Thats an HTML comment. Of course it is returned. If you want a CF comment, you need to use 3 dashes.
That's correct. I just didn't realized it was returning those comments, too. I ran firebug - lo and behold, there they are.
OK Ray, it's me again. I ran this and I an getting the correct values returned from my cfc but it isn't displaying correctly within my form. I ran with cfdebug and Firebug and still see the values and data. Any thoughts/ideas?
Ray, guess what? I'm stupid. :)
Javascript is case sensitive I was doing r.subject but the cfc was returning SUBJECT. Do you see a difference? Duh! So sorry to bug you.
No worries.
I got error: Error invoking CFC/art.cfc: Error Executing database Query.
Please help me remove the popup error.
Every time I reload the page I got the above mentioned error.
You have an error in art.cfc. You need to look at your exception log to see more details about the error. Or use a tool like Firebug to see the result in detail.
Ray,
I need to do exactly what you are doing so I was very excited when I found this blog entry. I've closely adapted your code, but I get an error on the return from the from the cfc. Here is the error message:
error:bind: Bind failed for select box LocationID, bind value is not a 2D array or valid serialized query
info:LogReader: LogReader initialized
info:global: Logger initialized
Thanks in advance,
:-}
Len
Here is the call:
<cfselect bind="../cfc/cfc:hierchy.getLocs()" bindonload="true" name="Loc" size="1" value="LocationID" display="LocationName"/>
Here is the code in the CFC:
<cffunction access="remote" name="getLocs" returntype="string">
<cfquery name="getLocQry" datasource="PHREDsevenInternalSQL">
SELECT LocationName,Bu_Grp_ID
FROM Locations
Where IsActive=#TruVal#
</cfquery>
<cfreturn getLocQry>
</cffunction>
Your cffunction has returntype=string, but it's a query.
Ray,
Thanks for getting back to me. I had already found that and fixed it but it didn't change the error.
:-}
Len
Use Firebug, or Chrome Dev tools, to see the JSON response. Cut and paste it into here - or if it is too big, use Pastebin.