Spring @MVC 3 cascading selects using jQuery
I’d be rich if I had a dollar for each time I heard someone ask “How do I dynamically populate a drop down menu based on the current selection in another drop down?” Actually that is probably the poster child usage of AJAX. You can certainly use raw AJAX to accomplish this (just ask the Google). I want to modify the question to “How do I do this using Spring @MVC 3?” So, this post will present one way of doing this using the very popular Javascript library jQuery to accomplish the AJAX part.
One of the great thing about the Spring Framework’s @MVC web framework is that Spring mostly stays out of the way of the underlying view technology. If you’ve used Struts 1 you are familiar with the tag soup with which you pollute your JSPs. There are only a handful of spring specific tags which focus on Spring specific functionality. That means Spring does nothing to help you link one HTML select element to another. Or put in a better way, Spring does not get in your way.
Let’s look at some initial code in the JSP where we have two selects. We want the city select to be populated when we choose the usStates select.
In this fragment we create a form with two select elements using Spring MVC’s tags. This is where some confusion sets in. The form:select tag does not enable our desired cascading behavior. When rendered (inspect the running page with Firebug or look at the source in the browser) you will see a plain old HTML select element.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<c:url value="/signup" var="signupUrl" /> <form:form id="signup" action="${signupUrl}" method="post" modelAttribute="signupForm"> <fieldset> <form:select id="usStates" path="usState"> </form:select> <form:select id="city" path="city"> <form:option value="">City</form:option> </form:select> </fieldset> <p> <button type="submit">Sign Up</button> </p> </form:form> |
The Spring MVC controller RegistrationContoller will respond to our requests. It is autowired (I’m using the JEE @Inject annotation instead) with my GeoServer which will return the states and cities. First we want it to create a form backing object when the controller receives an HTTP GET request at the /register URL, i.e. when your browser visits http://localhost:port/context/register
This method returns the name of the view that will be returned. In my Spring config file I specified that my JSPs are under WEB-INF/views and have a suffix of .jsp so here you need to only include the basename. (See org.springframework.web.servlet.view.InternalResourceViewResolver)
Notice that the name of the model attribute for the form backing object in the Controller (signupForm) matches the one specified in the JSP form:form element. The path attributes on the
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Controller public class RegistrationController { private static final Logger logger = LoggerFactory .getLogger(RegistrationController.class); @Inject private GeoService geoService; @RequestMapping(value = "/register", method = RequestMethod.GET) public String get(Model model) { SignupForm form = new SignupForm(); model.addAttribute("signupForm", form); logger.debug("registering. added form to model and returning register page"); return "register"; } |
Download the current version of jQuery (see Resources). I copied the source to my WEB-INF/resources directory (I created a jquery folder there) then I linked to it in my JSP header.
1 2 3 4 5 |
<head> ... <script type="text/javascript" src="<c:url value="/resources/jquery/1.6/jquery-1.6.1.min.js" />"></script> </head> |
As you could see in the initial JSP I did not have any option elements in either select element. So let’s populate the usStates select with some data from the server side. I’ve set up an MVC controller method to respond to URL /states by returning a JSON representation of the states. I’ll cover that next.
In jQuery there is a way to wait until everything on the page has been completely loaded so we will wrap our scripts with this idiom:
1 2 3 |
$(document).ready( // at this point the page has been loaded ); |
The $ symbol represents the jQuery Javascript object.
I will use the jquery getJSON function to get the data from the Spring controller. You need to specify the url, optional key/value (a map) data to send to the server, and a callback function to handle the data. I used the JSTL c:url tag to create a variable that include the context root for the controller method URL then used that variable in the Javascript. The key/value data I send to the server simply says that I want AJAX. The returned JSON data arrives as a parameter in the callback function. At this point it is up to you to use this data however you see fit. What we need in our example is a set of options for our usStates select element. So I loop over the data and create an option for each datum. Then I set the select element with id usStates to use these new options with the html function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<c:url var="findStatesURL" value="/states" /> <script type="text/javascript"> $(document).ready( function() { $.getJSON('${findStatesURL}', { ajax : 'true' }, function(data) { var html = '<option value="">State</option>'; var len = data.length; for ( var i = 0; i < len; i++) { html += '<option value="' + data[i].name + '">' + data[i].name + '</option>'; } html += '</option>'; //now that we have our options, give them to our select $('#usStates').html(html); }); }); </script> |
Back in the RegistrationController, we see that the findAllStates() method returns a Set containing State objects obtained from the GeoService. That’s right no JSON; just a collection of Java objects. Spring and Jackson will automatically marshal (convert) your Java objects to JSON.
1 2 3 4 5 6 |
@RequestMapping(value = "/states", method = RequestMethod.GET) public @ResponseBody Set<State> findAllStates() { logger.debug("finding all states"); return this.geoService.findAllStates(); } |
To make this work you need to annotate your return value with @ResponseBody. Then in the Spring config file (servlet-context.xml if you’re using the STS template) you use this annotation:
1 |
<annotation-driven /> |
Finally you need to have Jackson on your classpath. (Well, duh). That’s it. Automatic. (Unfortunately Jackson is not so automatic with more complicated object graphs. In particular parent/child relationship require more setup. More on that in another post).
While we’re in the controller let’s peek at our next method which returns the cities. As you can see it is mostly the same as the states method. The big difference (besides returning cities instead of states!) is that I need to specify which state for which I’d like to return the cities, so I add a RequestParam which is forwarded to the GeoService.
1 2 3 4 5 6 7 |
@RequestMapping(value = "/cities", method = RequestMethod.GET) public @ResponseBody Set<City> citiesForState( @RequestParam(value = "stateName", required = true) String state) { logger.debug("finding cities for state " + state); return this.geoService.findCitiesForState(state); } |
Blah, blah, blah, How do I cascade the selects already?
Just look for the change event on the usStates select element via jQuery. The rest of this Javascript looks astonishingly similar to what we saw to populate the usStates element! I added the required stateName parameter and there is a different URL and a different select receives the data but the rest is the same.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<c:url var="findStateCitiesURL" value="/cities" /> <script type="text/javascript"> $(document).ready(function() { $('#usStates').change( function() { $.getJSON('${findStateCitiesURL}', { stateName : $(this).val(), ajax : 'true' }, function(data) { var html = '<option value="">City</option>'; var len = data.length; for ( var i = 0; i < len; i++) { html += '<option value="' + data[i].name + '">' + data[i].name + '</option>'; } html += '</option>'; $('#city').html(html); }); }); }); </script> |
Cool huh?
One final bit of UI sugar would be to set some sort of message when a city is selected on the city select element. In the body of the JSP I’ve added a div element with the id “output”. With this bit of jQuery you can set its child text.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<script type="text/javascript"> $(document).ready(function(){ $("#city").change(onSelectChange); }); function onSelectChange() { var selected = $("#city option:selected"); var output = ""; if(selected.val() != 0){ output = "You selected City " + selected.text(); } $("#output").html(output); } </script> </head> <body> ... <div id="output"></div> ... |
What do you think?
Resources
Unzip the Maven project and import into your IDE or run from the command line.
1 |
mvn jetty:run |
then point your browser to:
1 |
http://localhost:8080/springFormWithCascadingSelects/ |
or you can run the unit and integration tests. The integration tests will run jetty and Selenium 2.
1 |
mvn integration-test |
Excelente POST. 😉
thank you very much. using your example i solved my issue with ajax call in spring MVC which i was struggling for almost 2 weeks.
Hi,
I am new here learning Spring, I like this tutorial, I am new to Maven to, please , I downloaded from the link :
Download Source as a Maven Project
Can you please show me the steps on how to import this project in eclipse.
your help is appreciated,
thanks
Kamal,
You need a plugin for Eclipse to recognize Maven projects. Some versions of Eclipse, such as Spring’s STS, already have it in the bundle. If you’ve downloaded one of the distributions from eclipse.org, then you need to install it.
There is information on the plugin at the eclipse.org site on installing and using the m2e plugin.
Once you have the plugin installed, you simply unzip the projects I’ve provided and choose the Eclipse menu item “Import” then choose “Existing Maven Project”.
Excellent post. I was searching for something like that.
The good part is that i made some modifications in the code
$.ajax({
type: “POST”,
url: “${findStateCitiesURL}”,
success: function(data){
$.each(data, function(val, text){
$(‘#cboState’).append($(”).val(val).html(text));
});
}
});
And in the Controller:
@ResponseBody
@RequestMapping(value = “/cities/{state}”, method = RequestMethod.POST)
public List findStateCities(@PathVariable(“state”) String state, ModelMap map) {
logger.debug(“finding cities for state ” + state);
return this.geoService.findCitiesForState(state); // as a List or something like that
}
And of course, Jackson is in my pom.xml:
org.codehaus.jackson
jackson-mapper-asl
1.9.5
Thanks so much for the post. i used what you have but i keep getting…Servlet.service() for servlet threw exception: java.lang.ClassNotFoundException: org.codehaus.jackson.map.JsonMappingException$Reference from [Module “deployment.ear.my-web-1.0.war:main” from Service Module Loader].
Any ideas on how solve this???? Thanks in advance.
Looks like Jackson is not being found for some reason. It is indeed in the pom.
Which Maven target did you execute?
Try updating Jackson in the pom.xml to version 1.9.9.
Thanks alot. You saved my time. It was really helpful.
Very nice article. I have a question: when I do the same but for editing an entity, the second form:select shows default value instead of the actual value for the entity. should I use jquery to select it? or should it be selected automatically?
Damian,
That, of course, is up to you or your client. It is certainly possible.
Using the code I posted as an example, it wouldn’t be too hard to populate the second select element with the entity’s fields or values.
Your post really helped me a lot. Thank you for this awesome post.
Nice post. But I am not able to get this working using the spring MVC “portlet”. I get the following error
==========================
Failed to load portlet org.springframework.web.portlet.DispatcherPortlet: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘org.springframework.web.portlet.mvc.annotation.DefaultAnnotationHandlerMapping’: Initialization of bean failed; nested exception is java.lang.IllegalStateException: Mode mappings conflict between method and type level: [/deviceTypesForCarrier] versus [VIEW]
==========================
My controller is as follows
@Controller
@RequestMapping(value = “VIEW”)
public class MyController {
—-
}
My method is
@RequestMapping(value=”deviceTypesForCarrier”, method = RequestMethod.GET)
public String findDeviceTypesForCarrier(@RequestParam(“carrier”) String carrier) {
//method implementation
}
My JSP page:
—————
and the javascript on the jsp page
—————————————
$(document).ready(function() {
$(“#carrier”).change(function() {
//get what is selected
var selected = $(“option:selected”,this).val();
alert(selected);
$(“#deviceType”).children().remove().end().append(“Select a Device Type”);
//now load in new options if I picked a carrier
if(selected == “”) return;
$.getJSON(‘${findDeviceTypesForCarrierURL}’, {
carrier : $(this).val(),
ajax : ‘true’
}, function(res, code) {
…
Hi i am new to Spring ,currently i am developing one project in that i have requirement of combobox of state,city,area i have used your above code but i am getting problem of retrieving data from controller to jsp
i.e i am sending state id to the controller and from controller i am passing stateid to spring jdbc and iam getting values into controller but iam unable to retrieve into jsp page .
Unable to retrieve JSON object into jsp .
I hope you understand the problem ,if u help me from this situation i will be thankful to u lot.
Thank You
Check the output of your Controller with curl.
curl -i -H "Accept: application/json" http://localhost:8080/yourcontroller
Thanks for the post! Can it be confirmed if this line is needed:
html += ”;
It seems there are no option elements that would need to be closed by this end tag.
Thanks,
Chris
I can’t find that line, Chris. Maybe WordPress ate part of your comment.
I just looked at the generated html and it’s all valid.
Maybe that’s a dead line that I don’t see.
Thanks – when the html drop down is being populated with option tags, just after the loop, – does line #17 need to be there where it concatenates the html var with a closing option tag? I didn’t see a corresponding option opening tag that would need to be closed here on line 17.
Thanks for the post!
Awesome. You made my day easier. Struggled for a week to resolve the issue. With your post i could resolve the issue in an hour. Thanks a lot.
Great to hear, Laks! That’s why I write these things.
Thanks for the code, now me too understand Ajax and JSON.
But I still get some blockage. It simply doesn’t work.
Firebug stops by ” function(data) ” and I have the error “data is not defined” by watching the variable DATA.
My modified code below :
——————————————————————–
$(document).on(‘change’, ‘#productName’, function () {
$.getJSON(
‘${detailsURL}’,
{ productNumber : $(this).val(), ajax : ‘true’ },
function(data) {
var html = ”;
var len = data.length;
for ( var i = 0; i < len; i++) {
html += '’
+ data[i].detailName + ”;
}
$(‘#details’).html(html);
}
);
});
great…!!!this helped me a lot…thanks
Very good post, thanks.
But, in your JS functions, what is the last sentence “html += ”;” for?
I see that you close every option tag inside the loop, so what is this extra closing tag for?
Of course, I meant: “html += ‘</option>’;” …
Hi Gene,
Nice example!!
I have a query.
For me the out put of the drop down always showing for PA and not for NY and NJ. I commented the line for PA in constructor of DefaultGeoService class.
public DefaultGeoService() {
State state = new State(“NJ”);
state.addCity(“Haddonfield”).addCity(“Cherry Hill”).addCity(“Marlton”);
this.states.put(state.getName(), state);
/*state = new State(“PA”);
// Carville’s joke
state.addCity(“Philadelphia”).addCity(“Pittsburgh”).addCity(“Alabama”);
this.states.put(state.getName(), state);*/
state = new State(“NY”);
state.addCity(“Sewer”).addCity(“Flushing”).addCity(“Armpit”);
state.addCity(“Jerkville”).addCity(“Moronica”).addCity(“Shithole”);
this.states.put(state.getName(), state);
}
I build and run the application. I’m still getting output for PA alone and not for NY and NJ.
Request for your clarification on this.
Regards
Harmohan
Hi Gene,
It’s working for me.
Thanks you very much.
Regards
Harmohan
Thanks for such a great article. But I am in another problem. When I submit the form along with these dynamically loaded dropdown, it is treated as null value in controller. Why is that? Any idea? It should be the object like ‘City’. What am I missing?
Thanks in advance!
Excellent post and thanks a lot!
I tried to make it to take multiple selections and could not get it work, any idea?
Thanks,
Not Working sir please help me.
Here is my code
my jsp:
Insert title here
<link rel="stylesheet" href='’>
<link rel="stylesheet" href='’>
<script type="text/javascript" src='’>
<script type="text/javascript" src='’>
$(function() {
$( “#datepicker” ).datepicker({ maxDate: -1 });
});
$(document).ready(function() {
$(‘#states’).change(
function() {
$.getJSON(‘${findStateCitiesURL}’, {
stateName : $(this).val(),
ajax : ‘true’
}, function(data) {
var html = ‘Select District’;
var len = data.length;
for ( var i = 0; i < len; i++) {
html += '’
+ data[i].name + ”;
}
html += ”;
$(‘#district’).html(html);
});
});
});
$(document).ready(
function() {
$.getJSON(‘${findStatesURL}’, {
ajax : ‘true’
}, function(data) {
var html = ‘Select State’;
var len = data.length;
for ( var i = 0; i < len; i++) {
html += '’
+ data[i].name + ”;
}
html += ”;
//now that we have our options, give them to our select
$(‘#states’).html(html);
});
});
#success_message{ display: none;}
AWS Utility
<%– –%>
<%–
State
–%>
State
<%– –%>
District
Station
Select Station
Date
Hours
Select Hours
Minutes
Select Minutes
Success Success!.
              VIEW         
        Download         
my controller code:
@Controller
@RequestMapping(“/data”)
public class DataController {
@Autowired
private DataServiceInterface service;
public DataController() {
System.out.println(“Inside Data Controller”);
}
@GetMapping(“/index”)
public String homePage(Data data,Model map)
{
System.out.println(“in home”);
/*List states = service.getStatesType();
Collections.sort(states);
map.addAttribute(“states”, states);*/
return “data/home”;
}
@RequestMapping(value = “/states”, method = RequestMethod.GET)
public @ResponseBody List findAllStates() {
System.out.println(“in states”);
return this.service.getStatesType();
}
@RequestMapping(value = “/districts”, method = RequestMethod.GET)
public String districtList(@RequestParam(value = “stateName”, required = true) String state,Model map)
{
System.out.println(“In district list”);
List district=service.getDisticts(state);
List newDistrict=new ArrayList();
for (District dist: district) {
System.out.println(dist.toString());
newDistrict.add(dist.toString());
}
Collections.sort(newDistrict);
map.addAttribute(“districtsName”, newDistrict);
return “data/home”;
}