Client-Side Drilldowns Made Easy
Last September I shared a post on an alternative to Ajax for client-side interactivity leveraging Coldfusion's WDDX. I'd like to take it a step further now and share an approach (and corresponding code) I often use in my Model-Glue apps when needing to create tiered or drilldown-type select lists withOUT having to make numerous calls to the server. The gist of this methodology is the same as in my previous post:
Here is the working sample. Go ahead, play with it!
The main difference in this example is the fact that I'm not utilizing WDDX, but rather JSON to make the data Javascript friendly.
Let me show you what the queries look like for each tier of this particular drilldown:
An important item of note here is that the data set for a given tier must include the parent ID of the previous tier in order to perform the filtering operations you'll see soon.
Okay, so how to get the queries into a format Javascript can manipulate. The route I chose to go was to convert the queries to JSON via one of two methods, depending on what version of CF you're using. Since not everyone is using version 8 yet, I made this example compatible with anything 6 or higher (versions previous to 8 are dependent on outside conversion; I chose to use JSON.CFC. Version 8 can utilize the built in function "serializeJSON"). Here's the statement where the queries are transformed (using the commonly known custom tag "QuerySim" to create the data for this example):
You'll notice in the section where I rely on JSON.CFC, I am doing a replace on the lower case word "data" to make it upper case. This is to make the JSON string produced consistent with the one produced utilizing serializeJSON. Since Javascript is case sensitive, case consistency in the JSON is required if you wish to utilize only one javascript function to perform the select list population. You'll also notice the use of the optional secondary parameter in the serializeJSON function call. This is needed in order to produce a JSON string that can be accessed by Javascript exactly the same as the JSON.CFC string.
MG NOTE: Regarding the fact that I utilize this approach in Model-Glue apps...I have my controller return an already formatted JSON string to my view rather than return a query and then perform the transformation there. Many of my controller methods have an optional "returnJSON" argument that I use when I need a JSON string back rather than a query.
Okay, data sets are available to Javascript. Now to write a few Javascript functions that can spin through that data and populate the appropriate select list. Here are the functions needed to perform the necessary tasks:
Of note is the fact that we are working with three select lists. The first select list is always populated at load time and needs no Javascript intervention at all. Select lists "level_2" and "level_3", however, are being completely manipulated by the JS calls by one generically written function.
Last but not least, the HTML with the Javascript calls embedded in the select lists' onChange event:
Voila! That's it! instant, client-side drilldown with only a single call to the server!
Hope it saves someone a little time. ;)
Doug out.
- Retrieve all needed data sets for populating the drilldowns;
- Convert those CF queries to a form that Javascript can manipulate;
- Write the necessary functions to populate the dropdowns based on previous options selected.
Here is the working sample. Go ahead, play with it!
The main difference in this example is the fact that I'm not utilizing WDDX, but rather JSON to make the data Javascript friendly.
Let me show you what the queries look like for each tier of this particular drilldown:
| Tier 1 | Tier 2 | Tier 3 |
|---|---|---|
![]() |
![]() |
![]() |
An important item of note here is that the data set for a given tier must include the parent ID of the previous tier in order to perform the filtering operations you'll see soon.
Okay, so how to get the queries into a format Javascript can manipulate. The route I chose to go was to convert the queries to JSON via one of two methods, depending on what version of CF you're using. Since not everyone is using version 8 yet, I made this example compatible with anything 6 or higher (versions previous to 8 are dependent on outside conversion; I chose to use JSON.CFC. Version 8 can utilize the built in function "serializeJSON"). Here's the statement where the queries are transformed (using the commonly known custom tag "QuerySim" to create the data for this example):
<cf_querysim>
level_1
level_1_id,name
1|Colors
2|Shapes
3|Foods
</cf_querysim>
<cf_querysim>
level_2
level_2_id,level_1_id,name
1|1|Red
2|1|Blue
3|1|Yellow
4|2|Triangle
5|2|Square
6|2|Circle
7|3|Bread
8|3|Meat
9|3|Fruit
</cf_querysim>
<cf_querysim>
level_3
level_3_id,level_2_id,name
1|1|Fuschia
2|1|Brick Red
3|1|InfraRed
4|2|Teal
5|2|Cyan
6|2|Navy Blue
7|3|Light Yellow
8|3|Dark Yellow
9|4|Isosceles
10|4|Equilateral
11|4|Right Triangle
12|5|Rectangle
13|5|Parallelogram
14|6|Ellipse
15|6|Oval
16|7|Matzah
17|7|Hot Cross Buns
18|7|Brioche
19|8|Steak
20|8|Fajitas
21|8|Hamburger
22|9|Kiwi
23|9|Grapes
24|9|Oranges
</cf_querysim>
<!--- convert our query data to JSON strings...mind the second parameter to the serializeJSON function... --->
<cfif listfirst(SERVER.ColdFusion.ProductVersion) gt 7>
<cfset level_2_json = serializeJSON(level_2, true) />
<cfset level_3_json = serializeJSON(level_3, true) />
<cfelse><!--- we're on less than version 8. use json.cfc --->
<cfset objJSON = createobject("component","json") />
<cfset level_2_json = replace(objJSON.encode(level_2),"data","DATA","all") />
<cfset level_3_json = replace(objJSON.encode(level_3),"data","DATA","all") />
</cfif>
<script>
//set our json data into Javascript objects
var lev2data = <cfoutput>#level_2_json#</cfoutput>;
var lev3data = <cfoutput>#level_3_json#</cfoutput>;
</script>
level_1
level_1_id,name
1|Colors
2|Shapes
3|Foods
</cf_querysim>
<cf_querysim>
level_2
level_2_id,level_1_id,name
1|1|Red
2|1|Blue
3|1|Yellow
4|2|Triangle
5|2|Square
6|2|Circle
7|3|Bread
8|3|Meat
9|3|Fruit
</cf_querysim>
<cf_querysim>
level_3
level_3_id,level_2_id,name
1|1|Fuschia
2|1|Brick Red
3|1|InfraRed
4|2|Teal
5|2|Cyan
6|2|Navy Blue
7|3|Light Yellow
8|3|Dark Yellow
9|4|Isosceles
10|4|Equilateral
11|4|Right Triangle
12|5|Rectangle
13|5|Parallelogram
14|6|Ellipse
15|6|Oval
16|7|Matzah
17|7|Hot Cross Buns
18|7|Brioche
19|8|Steak
20|8|Fajitas
21|8|Hamburger
22|9|Kiwi
23|9|Grapes
24|9|Oranges
</cf_querysim>
<!--- convert our query data to JSON strings...mind the second parameter to the serializeJSON function... --->
<cfif listfirst(SERVER.ColdFusion.ProductVersion) gt 7>
<cfset level_2_json = serializeJSON(level_2, true) />
<cfset level_3_json = serializeJSON(level_3, true) />
<cfelse><!--- we're on less than version 8. use json.cfc --->
<cfset objJSON = createobject("component","json") />
<cfset level_2_json = replace(objJSON.encode(level_2),"data","DATA","all") />
<cfset level_3_json = replace(objJSON.encode(level_3),"data","DATA","all") />
</cfif>
<script>
//set our json data into Javascript objects
var lev2data = <cfoutput>#level_2_json#</cfoutput>;
var lev3data = <cfoutput>#level_3_json#</cfoutput>;
</script>
You'll notice in the section where I rely on JSON.CFC, I am doing a replace on the lower case word "data" to make it upper case. This is to make the JSON string produced consistent with the one produced utilizing serializeJSON. Since Javascript is case sensitive, case consistency in the JSON is required if you wish to utilize only one javascript function to perform the select list population. You'll also notice the use of the optional secondary parameter in the serializeJSON function call. This is needed in order to produce a JSON string that can be accessed by Javascript exactly the same as the JSON.CFC string.
MG NOTE: Regarding the fact that I utilize this approach in Model-Glue apps...I have my controller return an already formatted JSON string to my view rather than return a query and then perform the transformation there. Many of my controller methods have an optional "returnJSON" argument that I use when I need a JSON string back rather than a query.
Okay, data sets are available to Javascript. Now to write a few Javascript functions that can spin through that data and populate the appropriate select list. Here are the functions needed to perform the necessary tasks:
<script>
//function to repopulate targeted select list
function repopulate(targetObjID,targetDataSet,selectedIDVal, idColName, valColName, optionValColName){
/*
parameters:
targetObjID - the ID of the select list we want to populate;
targetDataSet - the actual Javascript data object we created previously;
selectedIDVal - the name of the column in this data set that contains the parent record ID
idColName - the name of the column that contains THIS tier's own record ID
valColName - the name of the column that contains the data we want to display as the option text in the dropdown;
optionValColName - the name of the column that contains the value we want to use as the new options's VALUE value;
*/
//loop over the data object. for every object with a keyname of idval, add it to the dropdown
var objTarget = document.getElementById(targetObjID);
ResetObject(objTarget);
for(i=0;i<targetDataSet.DATA[idColName].length;i++){
if(targetDataSet.DATA[idColName][i] == selectedIDVal){
objTarget[objTarget.options.length] = new Option(targetDataSet.DATA[valColName][i].substring(0,45),targetDataSet.DATA[optionValColName][i],false,false);
}
}
}
//function to clear a dropdown
function ResetObject(objTarget){
objTarget.options.length=0;
objTarget.options[0] = new Option("---------------","",false,false);
return;
}
function resetAll(objIDList){//empty out all of the dropdowns specified in objIDList
var idlist = objIDList.split(",");
for (i=0;i<idlist.length;i++){
ResetObject(document.getElementById(idlist[i]));
}
return;
}
</script>
//function to repopulate targeted select list
function repopulate(targetObjID,targetDataSet,selectedIDVal, idColName, valColName, optionValColName){
/*
parameters:
targetObjID - the ID of the select list we want to populate;
targetDataSet - the actual Javascript data object we created previously;
selectedIDVal - the name of the column in this data set that contains the parent record ID
idColName - the name of the column that contains THIS tier's own record ID
valColName - the name of the column that contains the data we want to display as the option text in the dropdown;
optionValColName - the name of the column that contains the value we want to use as the new options's VALUE value;
*/
//loop over the data object. for every object with a keyname of idval, add it to the dropdown
var objTarget = document.getElementById(targetObjID);
ResetObject(objTarget);
for(i=0;i<targetDataSet.DATA[idColName].length;i++){
if(targetDataSet.DATA[idColName][i] == selectedIDVal){
objTarget[objTarget.options.length] = new Option(targetDataSet.DATA[valColName][i].substring(0,45),targetDataSet.DATA[optionValColName][i],false,false);
}
}
}
//function to clear a dropdown
function ResetObject(objTarget){
objTarget.options.length=0;
objTarget.options[0] = new Option("---------------","",false,false);
return;
}
function resetAll(objIDList){//empty out all of the dropdowns specified in objIDList
var idlist = objIDList.split(",");
for (i=0;i<idlist.length;i++){
ResetObject(document.getElementById(idlist[i]));
}
return;
}
</script>
Of note is the fact that we are working with three select lists. The first select list is always populated at load time and needs no Javascript intervention at all. Select lists "level_2" and "level_3", however, are being completely manipulated by the JS calls by one generically written function.
Last but not least, the HTML with the Javascript calls embedded in the select lists' onChange event:
<body>
<h2>Client-Side Drilldown Example</h2>
Level 1: <select name="level_1" id="level_1" onChange="resetAll('level_2,level_3');repopulate('level_2',lev2data,this.options[this.selectedIndex].value,'level_1_id','name','level_2_id');">
<option value=""><cfoutput>#repeatstring("-",15)#</cfoutput></option>
<cfoutput query="level_1">
<option value="#level_1_id#">#name#</option>
</cfoutput>
</select>
<hr width="25%" align="left">
Level 2:
<select name="level_2" id="level_2" onChange="resetAll('level_3');repopulate('level_3',lev3data,this.options[this.selectedIndex].value,'level_2_id','name','level_3_id');">
<option value=""><cfoutput>#repeatstring("-",15)#</cfoutput></option>
</select>
<input type="button" value="show ID" onClick="alert(document.getElementById('level_2').options[document.getElementById('level_2').selectedIndex].value);">
<hr width="25%" align="left">
Level 3:
<select name="level_3" id="level_3" >
<option value=""><cfoutput>#repeatstring("-",15)#</cfoutput></option>
</select>
<input type="button" value="show ID" onClick="alert(document.getElementById('level_3').options[document.getElementById('level_3').selectedIndex].value);">
</body>
<h2>Client-Side Drilldown Example</h2>
Level 1: <select name="level_1" id="level_1" onChange="resetAll('level_2,level_3');repopulate('level_2',lev2data,this.options[this.selectedIndex].value,'level_1_id','name','level_2_id');">
<option value=""><cfoutput>#repeatstring("-",15)#</cfoutput></option>
<cfoutput query="level_1">
<option value="#level_1_id#">#name#</option>
</cfoutput>
</select>
<hr width="25%" align="left">
Level 2:
<select name="level_2" id="level_2" onChange="resetAll('level_3');repopulate('level_3',lev3data,this.options[this.selectedIndex].value,'level_2_id','name','level_3_id');">
<option value=""><cfoutput>#repeatstring("-",15)#</cfoutput></option>
</select>
<input type="button" value="show ID" onClick="alert(document.getElementById('level_2').options[document.getElementById('level_2').selectedIndex].value);">
<hr width="25%" align="left">
Level 3:
<select name="level_3" id="level_3" >
<option value=""><cfoutput>#repeatstring("-",15)#</cfoutput></option>
</select>
<input type="button" value="show ID" onClick="alert(document.getElementById('level_3').options[document.getElementById('level_3').selectedIndex].value);">
</body>
Voila! That's it! instant, client-side drilldown with only a single call to the server!
Hope it saves someone a little time. ;)
Doug out.
Subscription Options
You are not logged in, so your subscription status for this entry is unknown. You can login or register here.
No comments found.




