Earlier today a follower on Twitter asked about doing related selects in jQuery. This is relatively trivial, but surprisingly most Google searches turned up plugins instead of tutorials. There's nothing wrong with that. In fact, if I needed to do this in a project I'd make use of the excellent Select box manipulation plugin for a few years now. It occurred to me though that it may be interesting to do a related select example "from scratch" so to speak. This is how I solved it - feel free to rewrite it better/faster/leaner.
First, I began with a form:
<form>
<label for="states">States</label>
<select name="states" id="states">
<option value="">Select a State</option>
<option value="1">Louisiana</option>
<option value="2">California</option>
<option value="3">Virginia</option>
<option value="4">Texas</option>
</select>
<label for="cities">Cities</label>
<select name="cities" id="cities">
<option value="">Select a City</option>
</select>
</form>
Here you can see my two selects. One labeled states and one for cities. Notice that cities is blank except for a top placeholder item. Now let's look at the JavaScript. The code isn't more than 20 lines so I've included the entire block here:
//first, detect when initial DD changes
$("#states").change(function() {
//get what they selected
var selected = $("option:selected",this).val();
//no matter what, clear the other DD
//Tip taken from: http://stackoverflow.com/questions/47824/using-core-jquery-how-do-you-remove-all-the-options-of-a-select-box-then-add-on
$("#cities").children().remove().end().append("<option value=\"\">Select a City</option>");
//now load in new options if I picked a state
if(selected == "") return;
$.getJSON("test.cfc?method=getcities&returnformat=json",{"stateid":selected}, function(res,code) {
var newoptions = "";
for(var i=0; i<res.length; i++) {
//In our result, ID is what we will use for the value, and NAME for the label
newoptions += "<option value=\"" + res[i].ID + "\">" + res[i].NAME + "</option>";
}
$("#cities").children().end().append(newoptions);
});
});
So we begin by simply listening to changes in the first drop down. We grab the selected value right away. Next we nuke all the options in the city drop down. For that action I used an excellent example from StackOverflow. You can see that I can move from the drop down to it's children, to removing them, to moving to the end and appending my placeholder. All in one line. I sometimes shy away from code like that as it feels like a bit too much going on at once, but in this case I think it's completely appropriate.
Next we see if the user actually picked something. If they didn't, we leave. If they did, we do a quick call to our back end to load the data. Now for this example, the code isn't too important, but for completeness sake I'll post it at the bottom. Basically you can imagine a database call. It's going to return an array of results where each array index contains an ID and NAME value. From that I can create a string, and like the earlier statement used to add a placeholder, I can append it to the drop down.
And that's it. As I said, there's plugins out there to make this even easier, but it certainly isn't too difficult this way either. Here is the complete HTML page.
<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>
$(document).ready(function() {
//first, detect when initial DD changes
$("#states").change(function() {
//get what they selected
var selected = $("option:selected",this).val();
//no matter what, clear the other DD
//Tip taken from: http://stackoverflow.com/questions/47824/using-core-jquery-how-do-you-remove-all-the-options-of-a-select-box-then-add-on
$("#cities").children().remove().end().append("<option value=\"\">Select a City</option>");
//now load in new options if I picked a state
if(selected == "") return;
$.getJSON("test.cfc?method=getcities&returnformat=json",{"stateid":selected}, function(res,code) {
var newoptions = "";
for(var i=0; i<res.length; i++) {
//In our result, ID is what we will use for the value, and NAME for the label
newoptions += "<option value=\"" + res[i].ID + "\">" + res[i].NAME + "</option>";
}
$("#cities").children().end().append(newoptions);
});
});
})
</script>
</head>
<body>
<form>
<label for="states">States</label>
<select name="states" id="states">
<option value="">Select a State</option>
<option value="1">Louisiana</option>
<option value="2">California</option>
<option value="3">Virginia</option>
<option value="4">Texas</option>
</select>
<label for="cities">Cities</label>
<select name="cities" id="cities">
<option value="">Select a City</option>
</select>
</form>
</body>
</html>
And the CFC:
component {
remote function getCities(required numeric stateid) {
var result = [];
//This would be a db call
for(var i=1; i<arguments.stateid*2; i++) {
var city = {};
city.name = "Some City #i# for State #arguments.stateid#";
city.id = i;
arrayAppend(result, city);
}
return result;
}
}
Archived Comments
Is it really better to append the HTML as a string instead of adding options to the select control?
Why? The browser ends up turning it into real DOM items once inserted - so the job is going to be done - so why not just let the browser handle it when I'm done?
Nothing "wrong" with it -- for some reason it has always felt cleaner to me to avoid putting the literal HTML in javascript whenever possible. Just wondering if it's more than a style thing and there is actually some good reason to do it your way that I'm missing.
To me it was just simpler. I'm trying to move to jQuery Templates for larger stuff, but this seemed like it would be overkill for templates.
Thanks Ray. Similar to the email I sent last week with the bound selects with cfselect, once the form is submitted, if the user returns to the form from the results page via the back button on the brower or javascript, would the selects retain the values the user selected or is the cities reset and waiting for states to be changed?
Ah, my favorite non-trivial Ajax issue. :) Let's do this in a follow up, ok? Maybe later today before I head off for the weekend.
Long story short though - you have to add JS logic to handle:
a) I had a default for the left drop down.
b) Set it automatically
c) Ensure my event handler fires
Not too difficult at all really - just delicate.
Exactly. Thanks. I did a little test with this example and it did retain the select option and user selected values.
THANK YOU!!!! Sometimes you search for hours and get nowhere. I have been trying to do this for weeks off and on and kept coming up with road blocks (mostly b/c I am really not all that great at debugging js) But I never thought to delete the contents of the select and fill it back in again. I kept thinking I had to have the values bound somehow. Replace the actual HTML - DUH!
Thanks SO much Ray!
Chris
Glad to help.
I use that select box plugin extensively, however, it doesn't seem to work properly (or at least in the same manner)under jQuery 1.6, and I wonder if this is being updated anymore.
Let me retract that statement, at least temporarily...some of the pages that use that plugin under 1.6 don't work in the same manner, but the fault might lie with a different plugin and the effects are cascading down, causing issues with the select box.
OK, here's the issue and the resolution. It's in the jQuery 1.6 release notes...
After populating the select box, I want to select the first option in the list. In earlier versions of jQuery, this worked:
$("#mySelect option:first").attr("selected", "selected");
it now seems necessary to use "prop" instead of "attr"
$("#mySelect option:first").prop("selected", "selected");
Man, that's going to break a lot of code...
I was just saying on Twitter a few days ago that that attr/prop changes were a bit confusing. My understanding (and take with grain of salt), is that ATTR always returns the original. So given
<input name="ray" value="1">
The attr for value is always 1. If I change to N later on, attr is still 1 cuz that's the value as set in the DOM originally. But prop will give the _current_ value.
I keep getting a return result of null when trying this out. I wonder what I am doing incorrect here...
My cfc reads like this .. <cffunction name="getReps" access="public" returntype="array" output="no">
<cfargument name="client_ID" required="yes">
<cfset var result = arrayNew(1) />
<cfset var rep = structNew() />
<cfset var clientReps = application.clientService.getRepsByClientID(arguments.client_ID)/>
<cfloop query="clientReps">
<cfset rep.name = '#rep_FirstName# #rep_LastName# (#rep_JobTitle#)' />
<cfset rep.id = rep_ID />
<cfset arrayAppend(result,rep) />
</cfloop>
<cfreturn result />
</cffunction>
My json call reads as such
$.getJSON("org/ews/selectFields.cfc?method=getReps&returnformat=json",{"client_ID":selected}, function(res,code) {
alert(res); return;
});
Now I know the query that is run in the function does in fact return no less than one row. Any ideas why I keep getting "res is null" in my error console?
God Bless,
Chris
What does Firebug show you? Or Chrome dev tools?
Oh - I see it. Your access="public" - it should be access="remote".
But yeah - Firebug woulda shown that error to you.
Well I changed it to remote and still get null as the return result. I have put the function on the main page and dumped the result on the page using a specific client id - just to confirm that there is for sure a record returned - and there is.
As for firebug - it keeps sending back res is null. It doesn't say anything about my function being remote or public etc.
On another note: In your code you used function(res,code) - I assume the res = results or the return value, what is the 'code' for? I don't see it anywhere else in your script.
code is the HTTP status code.
So - can I see your code online? I'd like to test it myself.
I just emailed you the info - thanks Ray!
I've moved to using templates for all situations where I'll need to loop and output data returned by an AJAX method. Select boxes and tables are cake with templates and I have very clean JavaScript code...
<script id="clientSelectorTemplate" type="x-jquery-tmpl">
<option value="${ClientID}">${ClientName}</option>
</script>
function getAppUserClients() {
var providerCode = '<%=Session["appUserProviderCode"] %>';
$.ajax({
type: "POST",
contentType: "application/json",
url: "Services/AppUser.asmx/GetAppUserClientsByProviderCode",
dataType: "json",
data: "{'providerCode' : '" + providerCode + "'}",
success: function (msg) {
getAppUserClientsCallback(msg);
},
error: errorHandler
});
}
function getAppUserClientsCallback(result) {
var clientList = result.d;
$("#clientSelectorTemplate").tmpl(clientList).appendTo("#clientSelector");
setActiveClientID();
}
HTML markup...
<select id="clientSelector" onchange="setActiveClientID()">
</select>
Yes, that was .NET crap referenced in my code - it was definitely not by choice. You really take for granted how easy it is to build web apps with CF until you don't have it at your disposal. I can also tell you that ColdFusion's developer community seems to have much more helpful blogs/online resources for CF developers than the ASP.NET community. I honestly don't understand why so many people are in love with ASP.NET for developing web apps. I really think that CF is better suited for rapid web app development.
Andy, please tell me you aren't surprised by the fact that the ColdFusion Community rocks? ;)
Back in the day (say around versions 5 and MX) when I started learning web development I quickly settled on CF as my platform of choice. Like most of the guys who read and contribute to this and other sites, I learned CF development from the CFWACK books and getting help from more experienced guys who ran CF blogs. There is a wide array of developers in the CF world who post for readers of ALL skill levels and everyone seems pretty quick to help someone out no matter how trivial or complex their issue may be. The ASP.NET community just isn't like that. It seems to be heavily fragmented.
Yes, I can now build a fully functional and complex AJAX app using ASP.NET and jQuery/jQuery UI - but I feel as though it was very much a school of hard knocks experience (over the course of the last 3 months).
Bottom line - in my opinion the CF Community is hands down the best community in terms of educating and helping their own folks.
This is now OT but as the admin I say it's ok. ;)
When you say the ASP.NET community is fragmented, what do you mean? Like what types of groups I mean.
I feel like you have developers who post extremely complicated solutions vs. those who post stuff that is crafted from the desk of Mr. Obvious.
The server platform itself (your version of IIS + your .NET framework version + other server variables) can be a nightmare to debug as well. I never faced these types of configuration challenges with ColdFusion so I was quickly aggravated by say trying to create web services to interact with a data access layer and getting it to all return objects in a format that my UI code could work with. While I'm convinced that you can do virtually anything you want in .NET or Java I just feel like CF let me focus on my ideas rather than building reusable framework items for running a query, etc. To be blunt, all of that shit and more just exists out of the box with ColdFusion. Search, PDF, charting and graphing, grids, layouts - it all just works and it works rather easily and intuitively.
I don't want to go on a rant and I'm happy to have acquired a new skill I'm just stating that I don't see what's so great about ASP.NET as a web development platform.
Interesting. Well thanks man for the opinion.
Thanks for the help Ray - my application.cfc had a cfabort in it and of course calling the file via ajax was making an hhtp request and the cfabort came into play.
I think that I must not be understanding the array at this point though. It would seem that when I append data to my array - it simply duplicates the last record returned.
I have a query that returns two distinctly different records, and when I append the records to an array - only the last record of the query gets inserted....
<cfloop query="clientReps" >
<cfoutput>
<cfset rep.name = '#rep_FirstName# #rep_LastName# (#rep_JobTitle#)' />
<cfset rep.id = rep_ID />
#arrayAppend(result,rep)#<br />
</cfoutput>
</cfloop>
<cfdump var="#clientReps#" label="Original Query">
<cfdump var="#result#" label="resulting array">
That returns the following results..
http://awesomescreenshot.co...
Sorry to be ignorant but I ave tried everything that I can think of from cfloop to cfoutput to changing the array depth (which of course breaks :) )
God Bless,
Chris
Try adding <cfset rep = structNew()> (or {}) after your cfloop.
Very interesting - reset the struct each time. It worked - thanks!!! Now a overly long project finally has an end in site.
Ray,
I was wondering why you are using the end() function twice in your code. It almost seems like you are using it to move to the 'end' of the select object. But I don't think that's not how the end() function is used. See http://api.jquery.com/end/ Am I missing something here?
For the first time I used end, it came directly from the Stack Overflow post.
For the second one - I believe you are right - I bet $(selector).children().append(..) would be sufficient.
Ouch - nope - doesn't work at all. Makes one big option with all the labels.
What about removing the children() function all together? You should be able to call append() directly on the $(selector) object.
Interesting - that worked perfectly. (Again, I'm just modifying the second instance.) I have to admit - out of all the form controls the Select one is the most awkward at times. ;) Thanks.
Ok, you're right. You need end() on the first call because you are filtering with children(). The end will remove that filter and put you back at $("#cities").
The second end() removes the children() filter as well. Its just filtering on children then removing the filter. So you should be able to remove both .children() and .end() from the $(selector) call.
Ray as usual, thank you for this little demo. Helped me alot with getting a TON of data from the backend to my drop downs.
I was sending over the whole HTML including <select> and <option> tags. That was a lot of overhead to pass along via ajax. Now, I'm just passing over the data and rendering the HTML in JS then plopping in the DOM. FAST!
One of the biggest "awakenings" I had in the past few years was learning that Ajax!=Fast. I had updated Lighthouse Pro to be Ajax-based and - due to a flaw in my understanding - ran into performance issues. (Mainly because I was sending huge XML packets back and forth.) Ajax _can_ obviously create sites that are much more performant, but you have to think about what you're doing. That's obvious - but easily forgotten to folks getting into Ajax.
Much like Chris in comments 14 & 16, i'm having an issue with res (I've also tried using data). I am getting an error saying data.length is undefined. Firebug is not returning any error messages. Any suggestions?
//jQuery
$.getJSON('cfc/metadata.cfc', {
method: 'getSelectedIndustriesFromPlans',
GMSType: 'Plan',
selectedGMSFK: value,
returnFormat: 'json'
}, function(data,code) {
alert(data.length);
}
});
//my.cfc
<cffunction name="getSelectedIndustriesFromPlans" output="false" returntype="query" access="remote" hint="Returns the selected SubIndustries based off of the selected plan">
<cfargument name="selectedPlanFKList" type="string" required="yes">
<cfquery name="getSelectedWWMIndustry" datasource="#THIS.metadata#">
SELECT IndustryID AS optionvalue, Industry AS optiontext
FROM IndustryTable
WHERE IndustryID IN (#selectedPlanFKList#)
</cfquery>
<cfreturn getSelectedWWMIndustry>
</cffunction>
//returned json
{"COLUMNS":["OPTIONVALUE","OPTIONTEXT"],"DATA":[[1150,"Banking - General"],[1102.0,"Credit Unions"],[2400.0,"Defense"],[1950.0,"Education - General"],[2350.0,"Energy and Utilities - General"],[1103.0,"Lenders"],[2100.0,"Oil and Gas"]]}
Thank you in advanced,
Jennifer
data - in your JavaScript, refers to the entire object. To check the length you would use
data.DATA
Can this be used with a CFC returning a query and not an array? I'm having with the last bit that writes new options to the child select.
My cfc returns a string as in: {"COLUMNS":["MODEL"],"DATA":[["1000 Sport"],["1098, S, R"],["600 Sport"],["620 Sport"],["748 (all types)"],["749 (all types)"],["750 Super"],["800 Sport & Sport"],[848.0],["851 Super"],["888, (all types) "],["900 SuperSport"],[916.0],[996.0],["998, S, R"]]}
Everything works great, except I'm having trouble with the last bit that sets var newoptions.
var newoptions = "";
for ( {newoptions += "<option value=\"" + res.model + "\">" + res.model + "</option>";
}
$("#models").children().end().append(newoptions);
Your "for" loop isn't valid JavaScript. Your loop should begin something like so:
for(var i=0; i<something; i++)
Another thought. If you extend this out to three selects related, how do you listen for changes to cities (to change DD streets)?
$("#cities").change(function()
Will loading the data to cities when a state is selected trigger a second call for more data for streets?
Your example is right.
And as to your second question - I don't know. Test it and see. ;) If not, don't forget you can fake an event with jQuery's trigger function.
Thanks Ray. I've got the first two DDs working, and when I add a third, the listener for the second DD works as expected. However, my second call for data to populate the third DD gives an error: "selected is undefined" at this line:
$.getJSON("getfitmentdata09.cfc?method=getyears&returnformat=json", {
"make": selected,
"models": selectedmodels
},
Here's the complete script:
<script>
$(document).ready(function () {
//first, detect when initial DropDown changes
$("#make").change(function () {
//get what they selected
var selected = $("option:selected", this).val();
//no matter what, clear the other DD
$("#models").children().remove().end().append("<option value=\"\">Select a Stinkin Model</option>");
//now load in new options if I picked a brand
if (selected == "") return;
$.getJSON("getfitmentdata09.cfc?method=getmotomodels&returnformat=json", {
"make": selected
}, function (res, code) {
var newoptionsyears = "";
for (var i = 0; i < res.DATA.length; i++) {
newoptionsyears += "<option value=\"" + res.DATA[i] + "\">" + res.DATA[i] + "</option>";
}
$("#models").children().end().append(newoptionsyears);
});
});
//detect when second DropDown changes
$("#models").change(function () {
//get what they selected
var selectedmodels = $("option:selected", this).val();
//no matter what, clear the other DD
$("#years").children().remove().end().append("<option value=\"\">Select a Stinkin Year</option>");
//now load in new options if I picked a brand
if (selectedmodels == "") return;
$.getJSON("getfitmentdata09.cfc?method=getyears&returnformat=json", {
"make": selected,
"models": selectedmodels
}, function (res, code) {
var newoptions = "";
for (var i = 0; i < res.DATA.length; i++) {
newoptions += "<option value=\"" + res.DATA[i] + "\">" + res.DATA[i] + "</option>";
}
$("#years").children().end().append(newoptions);
});
});
});
</script>
This does not make sense to me. "selected" is defined earlier??
Could you post it up on pastebin? Kinda hard to read.
Btw my query is returning me query records and how ur code will work here, i am missing something
my JSON
{"COLUMNS":["REASONID","REASONDETAIL","FREQUENCY"],"DATA":[[1,"Post1",1],[2,"Post3 ",1],[3,"Post4",1]]}
I'm not sure I get what your asking me. Do you understand that the JavaScript loops over results from the CFC, and if your data is different, all you have to do is modify the JavaScript?
Hello Ray, would this work with multiple selected items? Let's say i have a dropdown with departments, which i can select only one, then a dropdown set to allow multiple selections and that one would be populating a third one based on the multiple selections from the second one.
I've tried with an old post from Ben Forta's bound cfselect, but couldn't get it to work (i've sent you an email tday about it).
Thank you!
This would not work with 3 or more selects, but you could extend it to do so. You would simply add code for the 2nd select that has a listener list the 1st one.
Hey Ray, I just saw your comment now, thank you and I apologize for the delay answering you!
No worries.
Hello,
Have you got this as a downloadable zip (the demo) so I can have a look at all of the structure?
Thanks
Jan
Sorry no - but you can view source. The CFC code is above, very short, and can be copied.
Hi Raymond,
I couldn't get the CFC code to work - I posted it in a .cfc doc and it wasn't looking right - I am fairly new to CFC's and do I just directly paste the code above and save it as test.cfc?
Jan
Here is a good way to start with Object Oriented Programming for ColdFusion Jan: http://objectorientedcoldfu...
Yep, you just save it as soandso.cfc.
I mean a good PLACE to start, rather than way.
When running this it is only giving me the last value of my query in the array. Any thoughts on why this is happening?
<cfset result = arrayNew(1) />
<cfset ArrayMedSchools = structNew() />
<cfloop query="GetMedSchool" >
<cfoutput>
<cfset ArrayMedSchools.name = Institution />
<cfset ArrayMedSchools.id = InstitutionID />
<cfset arrayAppend(result,ArrayMedSChools) />
</cfoutput>
</cfloop>
<cfset ArrayMedSchools = structNew()/>
<cfdump var="#GetMedSchool#" label="Original Query">
<cfdump var="#result#" label="resulting array">
Tom, your comment has nothing to do with this particular post as far as I can see. For general CF questions, please use Stack Overflow.