Selecting default items using ColdFusion 8's AJAX Controls

So here is an interesting question. Take a look at Ben's post on related selects in ColdFusion 8 (and don't forget it is even easier now). Pretty simple, right? Well how do you set a control to use Ajax and have a default selected item? Turns out this isn't so easy.

The first thing I tried was selected=, figuring that would be the easiest solution, but unfortunately it didn't work.

I then tried ajaxOnLoad, thinking maybe I could set the default myself. But ajaxOnLoad() is fired when the page is complete, but before those Ajax calls to populate the drop downs are done.

I then tried onLoad in the cfform tag. This didn't work at all. So I threw the problem over to Todd Sharp, and together we were able to come up with a solution. It ain't pretty, but it works. My hope is that folks can look at this and suggest something nicer. I'm actually a bit surprised this isn't supported out of the box. It seems like I use forms half the time to edit content, not create content, so being able to set defaults is a must.

Anyway - the code:

<cfajaxproxy bind="javascript:test({mediaid},2)">
<head>
<script>
var imdone = false;
function test(x,val) {
if(!imdone) {
var dd = document.getElementById('mediaid');
for(var i = 0; i < dd.length; i++){
if(dd.options[i].value == val){
dd.selectedIndex = i;

}
}
imdone = true;
}
}
</script>
</head>

<cfform >
<table>
<tr>
<td>Select Media Type:</td>
<td><cfselect name="mediaid" id="mediaid"
bind="cfc:art.getMedia()"
bindonload="true" value="mediaid" display="mediatype" /></td>
</tr>
<tr>
<td>Select Art:</td>
<td><cfselect name="artid"
bind="cfc:art.getArt({mediaid})" value="artid" display="artname" /></td>
</tr>
</table>
</cfform>

So first off - note the use of cfajaxproxy. It is bound to the first drop down. When the value changes, and this occurs on initial load, code is run to set the default. In this case note the hard coded value of 2. This would be #form.selected# or whatever. Also note the use of a variable to remember that the default value has been selected. The cfajaxproxy will always run on change, so we want to be sure it is run only once.

Thoughts? This code only supports one selected item, and only supports defaulting the left control, but obviously it could be extended to handle both. Again though it is a bit disappointing that essentially one line of code:

cfselect name="mediaid" id="mediaid" bind="cfc:art.getMedia()" bindonload="true" value="mediaid" display="mediatype" />

had to be extended by about 10 lines of JavaScript. To be fair, my beloved Spry doesn't make this much easier. You can use spry:if type conditionals so it is a bit slimmer. Maybe someone can speak to how other frameworks like Prototype does it?

Edit: A followup post by Todd: Selecting Multiple Default Items With ColdFusion 8 Ajax Controls

Comments

I've since whipped up a script that would handle multiple selected items by passing an array of them in the ajaxproxy. This uses the mysteriously undocumented ColdFusion.JSON.decode function to handle the array. Obviously you'd also change the size on the select and add multiple=true...

<cfajaxproxy bind="javascript:test({mediaid},'[2,3,4]' )">

<script>
var imdone = false;
function test(x,val) {
   if(!imdone) {
   var dd = document.getElementById('mediaid');
   valArr = ColdFusion.JSON.decode(val);
   for(var i = 0; i < dd.length; i++){
      //loop over the array of selectedItems
      for(var j = 0; j < valArr.length; j++){
         if(dd.options[i].value == valArr[j]){
            dd.options[i].selected = true;
         }
      }
   }
   imdone = true;
   }
}

</script>
# Posted By todd sharp | 8/7/07 2:12 PM
Also worth nothing - if the first select accepts multiple then the bind will receive a comma seperated list of selected items (2,3,4 in this case) - so your CFC will have to accomodate...
# Posted By todd sharp | 8/7/07 2:14 PM
I haven't tried it because I'm lazy but can you just set the default value in the form scope <cfset form.mediaid = 1 /> and then use <cfform>'s preserveData="true" attribute?
# Posted By Sean Coyne | 8/7/07 2:24 PM
Tried it - didn't work. Good one there though. If that would have worked....
# Posted By Raymond Camden | 8/7/07 2:37 PM
Blogged a complete example of supporting multiple selected items (including handling them on the cfc side of things).

http://cfsilence.com/blog/client/index.cfm/2007/8/...
# Posted By todd sharp | 8/7/07 2:40 PM
You read my mind! I knew I should have worked on something else! I spent 2 hours trying to get this to work yesterday before giving up and resigning myself to emailing you about it. I just hadn't gotten around to emailing you yet :)
# Posted By Will | 8/7/07 2:55 PM
I think the real question is how to they do it with Ext JS Library, as I believe that is Adobe used to model theirs after.

The docs are here: http://extjs.com/deploy/ext/docs/ for anyone to go through. I looked at them briefly but saw nothing off the top of my head. Will take another look after the kids go down for the night.
# Posted By Mike Benner | 8/7/07 5:25 PM
Thankfully SOMEBODY is talking about this!!! I just spent the last hour and a half scouring the web for some sort of solution to this. Not having this out of the box is shameful on Adobe's part. I surely hope they come up with a clean solution for this as well. Cheers to you for making it work for now.
# Posted By Eric | 9/13/07 3:17 PM
Eric - to be fair - I found the same problem in Spry. No simply way to do defaults. Can others on this entry comment on other Ajax frameworks?
# Posted By Raymond Camden | 9/13/07 3:21 PM
This works for defaulting the first select... How can the 2nd select be defaulted as well?
I tried updating the javascript by adding the same logic to the 2nd select but it doesn't work.

Thanks.
# Posted By fred | 10/21/07 2:08 PM
I don't have an easy solution for this in CF8 Ajax.
# Posted By Raymond Camden | 10/21/07 2:15 PM
workaround: I'm able to default the second value with the script below. It basically works depending on the time it takes for the CFC query to return. This sets the second select to its corresponding value after 1 second of page loading. If for whatever reason the query takes longer, this doesn't work. No guarantees. In my case 'model' is the name of my 2nd select.

<pre>
<script>
   function initSelect(){
    var dd2 = document.getElementById('Model');

    for(var i = 0; i < dd2.length; i++){
    //loop over the array of selectedItems
    if(dd2.options[i].value == '<cfoutput>#form.defaultvalue#</cfoutput>'){
    dd2.options[i].selected = true;
           }
       } ;
    }

   setTimeout("initSelect()",1000)

</script>
</pre>
# Posted By Fred | 10/21/07 2:55 PM
I think I've found a simpler solution to this.

I've created a .js file that "overrides" a function from coldfusion's cfajax.js library.

The new version of the function allows you to specify which options are selected as part of the array you return from your "bound" .cfc function.

See my site for details.
http://www.realitystorm.com/experiments/coldfusion...
# Posted By Steve Savage | 4/2/08 4:48 PM
A useful trick is to tell the calling CFC what the selected value and making sure that it's #1:

<cfcomponent>

<cffunction name="get" access="remote">
<cfargument name="TABLE_NAME" default="">
<cfargument name="VARIABLE_NAME" default="">

<!--- this is kind of lame, but cold fusion will not set the selected variable!!!!! --->
<cfargument name="SELECTED" default="" type="String">
         <cfset answer=ArrayNew(2)>
         
         <!--- this is to set the "selected" item first; so that they're um.. selected. --->
         <cfset iter = 2>
         <cfset answer[1][1]=selected>
         <cfset answer[1][2]=selected>
         <cfloop index="i" from="1" to="#data.RecordCount#">
            <cfif ucase(trim(data.value[i])) neq ucase(trim(selected))>
               <cfset answer[iter][1]=data.value[i]>
               <cfset answer[iter][2]=data.display[i]>
               <cfset iter = iter + 1>
            <cfelse>
            
               <!--- ooo! we have the label of the first selected value! --->
                <cfset answer[1][2]=data.display[i]>
                
            </cfif>
      </cfloop>
         
<cfreturn answer>
</cffunction>
# Posted By PhilNg | 5/11/08 9:57 PM
Thanks for the help guys! This page has been my close friend for the past couple of hours as I tried to figure out how to make this all work. Hopefully Adobe will provide some more functionality soon.
# Posted By Mark Chripczuk | 7/15/08 7:02 PM
I've worked thru most of the workarounds I've found in this blog entry and on the original by Ben Forta. I finally settled on Steve Savage's solution and it's working very well for me. Once you have his javascript file in place and you wrap your head around the array you need to generate, which has 3 values per record - the id (option value), the display value, and a boolean that indicates if this record should be selected - it's actually very easy to implement.

Ray, if you see this comment, how can Steve's solution be passed on to Adobe for possible inclusion in the next update of CF8?
# Posted By Nando | 7/26/08 11:19 AM
Use this form:

www.adobe.com/go/wish
# Posted By Raymond Camden | 7/28/08 7:52 PM
Done!
# Posted By Nando | 7/29/08 7:15 AM
Hi Nando,

Can you please give us an example how did you work around with Steve Savage's solution? I'm not sure how to use his new function.

Thanks,
George
# Posted By George | 9/1/08 11:44 PM
For some reason IE doesn't work. FireFox is fine. It seems as IE executes the test function before the select is populated with options, so dd.length is 0. Ugh!

I am using the function just for a single cfselect which is using javascript bind.
# Posted By Giedrius | 9/18/08 11:50 AM
a twist on this problem is: how do you select an item in one combo box using the selected value in another combo box. say combo box #1 is a list criteria (fastest, economical, luxuriest, etc), combo box #2 is a list of car models. you don't want to filter combo box #2, you just want to select the default car model for that criteria (you might filter combo box #1 with a MFG combo box but we can skip that for now). How do you set the selected index of the model combo box based on the selection you make in the criteria combo box whenever the combo box is changed?
# Posted By Michael White | 9/25/08 3:26 PM
Use cfajaxproxy to run JS function foo() when the first drop down changes. In the JS function, use another ajaxproxy to call your cfc to get the right value for the second drop down based on the value in the first. Then simply loop over the options in drop down 2, and when you get the right value, you set it to selected. This is what I told you in email I think. :) Didn't it make sense?
# Posted By Raymond Camden | 9/26/08 6:43 AM
George, It was months ago but if you are still after some more info, I got the solution by Steve Savage working recently (thanks Steve!).

My example uses a product type/category/sub category hierarchy.

I added an argument to each of the functions in the cfc for the ID of the value I wanted to select:

<!--- Get array of product types --->
<cffunction name="getProductTypes" access="remote" returnType="array">
<cfargument name="typeID" type="numeric">
...
<!--- Get category by product type --->
<cffunction name="getCategories" access="remote" returnType="array">
<cfargument name="typeid" type="numeric" required="true">
<cfargument name="catID" type="numeric">
...
<!--- Get sub category by category --->
<cffunction name="getSubCategories" access="remote" returnType="array">
<cfargument name="categoryID" type="numeric" required="true">
<cfargument name="subCatID" type="numeric">
...
In each of the functions I added a cfif to the query to array loop to set the new array element to true for the passed ID:

<!--- Convert results to array --->
<cfloop index="i" from="1" to="#getTypes.RecordCount#">
<cfset result[i][1]=getTypes.typeID[i]>
<cfset result[i][2]=getTypes.type[i]>
<cfif getTypes.typeID[i] is arguments.typeID>
<cfset result[i][3]=true>
</cfif>
</cfloop>
...
(repeat for categories and sub categories)

Then on the display page I included Steve's magic JavaScript and added the arguments to each of the cfselect tags:

<cfselect name="typeID" bind="cfc:typeCatSubCat.getProductTypes(#typeID#)" bindonload="true" />
...
<cfselect name="catID" bind="cfc:typeCatSubCat.getCategories({typeID},#catID#)" />
...
<cfselect name="subCatID" bind="cfc:typeCatSubCat.getSubCategories({catID},#subCatID#)" />
# Posted By Chris | 12/23/08 6:27 PM