Dynamicaly populating a combobox with values from a Map based on what's selected in another combobox

18,558

Solution 1

Well anyway, as i said, i finally managed to do it by myself, so here's my answer...

I receive the map from my controller like this (I'm using Spring, don't know how this works with other frameworks):

<c:set var="manufacturersAndModels" scope="page" value="${MANUFACTURERS_AND_MODELS_MAP}"/>

These are my combos:

<select id="manufacturersList" name="manufacturersList" onchange="populateModelsCombo(this.options[this.selectedIndex].index);" >
                  <c:forEach var="manufacturersItem" items="<%= manufacturers%>">
                    <option value='<c:out value="${manufacturersItem}" />'><c:out value="${manufacturersItem}" /></option>
                  </c:forEach>
                </select>


select id="modelsList" name="modelsList"
                  <c:forEach var="model" items="<%= models %>" >
                    <option value='<c:out value="${model}" />'><c:out value="${model}" /></option>
                  </c:forEach>
                </select>

I imported the following classes (some names have, of course, been changed):

<%@ page import="org.mycompany.Car,java.util.Map,java.util.TreeMap,java.util.List,java.util.ArrayList,java.util.Set,java.util.Iterator;" %>

And here's the code that does all the hard work:

<script type="text/javascript">
<%  
     Map mansAndModels = new TreeMap();
     mansAndModels = (TreeMap) pageContext.getAttribute("manufacturersAndModels");
     Set manufacturers = mansAndModels.keySet(); //We'll use this one to populate the first combo
     Object[] manufacturersArray = manufacturers.toArray();

     List cars;
     List models = new ArrayList(); //We'll populate the second combo the first time the page is displayed with this list


 //initial second combo population
     cars = (List) mansAndModels.get(manufacturersArray[0]);

     for(Iterator iter = cars.iterator(); iter.hasNext();) {

       Car car = (Car) iter.next();
       models.add(car.getModel());
     }
%>


function populateModelsCombo(key) {
  var modelsArray = new Array();

  //Here goes the tricky part, we populate a two-dimensional javascript array with values from the map
<%                          
     for(int i = 0; i < manufacturersArray.length; i++) {

       cars = (List) mansAndModels.get(manufacturersArray[i]);
       Iterator carsIterator = cars.iterator();           
%>
    singleManufacturerModelsArray = new Array();
<%
    for(int j = 0; carsIterator.hasNext(); j++) {

      Car car = (Car) carsIterator.next();

 %>         
    singleManufacturerModelsArray[<%= j%>] = "<%= car.getModel()%>";
 <%
       }
 %>
  modelsArray[<%= i%>] = singleManufacturerModelsArray;
 <%
     }         
 %>   

  var modelsList = document.getElementById("modelsList");

  //Empty the second combo
  while(modelsList.hasChildNodes()) {
    modelsList.removeChild(modelsList.childNodes[0]);
  }

 //Populate the second combo with new values
  for (i = 0; i < modelsArray[key].length; i++) {

    modelsList.options[i] = new Option(modelsArray[key][i], modelsArray[key][i]);
  }      
}

Solution 2

I just love a challenge.

No jQuery, just plain javascript, tested on Safari.

I'd like to add the following remarks in advance:

  • It's faily long due to the error checking.
  • Two parts are generated; the first script node with the Map and the contents of the manufacterer SELECT
  • Works on My Machine (TM) (Safari/OS X)
  • There is no (css) styling applied. I have bad taste so it's no use anyway.

.

<body>
  <script>
  // DYNAMIC
  // Generate in JSP
  // You can put the script tag in the body
  var modelsPerManufacturer = {
    'porsche' : [ 'boxter', '911', 'carrera' ],
    'fiat': [ 'punto', 'uno' ]  
  };
  </script>

  <script>
  // STATIC
  function setSelectOptionsForModels(modelArray) {
    var selectBox = document.myForm.models;

    for (i = selectBox.length - 1; i>= 0; i--) {
    // Bottom-up for less flicker
    selectBox.remove(i);  
    }

    for (i = 0; i< modelArray.length; i++) {
     var text = modelArray[i];
      var opt = new Option(text,text, false, false);
      selectBox.add(opt);
    }  
  }

  function setModels() {
    var index = document.myForm.manufacturer.selectedIndex;
    if (index == -1) {
    return;
    }

    var manufacturerOption = document.myForm.manufacturer.options[index];
    if (!manufacturerOption) {
      // Strange, the form does not have an option with given index.
      return;
    }
    manufacturer = manufacturerOption.value;

    var modelsForManufacturer = modelsPerManufacturer[manufacturer];
    if (!modelsForManufacturer) {
      // This modelsForManufacturer is not in the modelsPerManufacturer map
      return; // or alert
    }   
    setSelectOptionsForModels(modelsForManufacturer);
  }

  function modelSelected() {
    var index = document.myForm.models.selectedIndex;
    if (index == -1) {
      return;
    }
    alert("You selected " + document.myForm.models.options[index].value);
  }
  </script>
  <form name="myForm">
    <select onchange="setModels()" id="manufacturer" size="5">
      <!-- Options generated by the JSP -->
      <!-- value is index of the modelsPerManufacturer map -->
      <option value="porsche">Porsche</option>
      <option value="fiat">Fiat</option>
    </select>

    <select onChange="modelSelected()" id="models" size="5">
      <!-- Filled dynamically by setModels -->
    </select>
  </form>

</body>

Solution 3

Are you using Struts?

You will need some JavaScript trickery (or AJAX) to accomplish this.

What you'd need to do is, somewhere in your JavaScript code (leaving aside how you generate it for the minute):

var map = {
   'porsche': [ 'boxter', '911', 'carrera' ],
   'fiat': ['punto', 'uno']
};

This is basically a copy of your server-side data structure, i.e. a map keyed by manufacturer, each value having an array of car types.

Then, in your onchange event for the manufacturers, you'd need to get the array from the map defined above, and then create a list of options from that. (Check out devguru.com - it has a lot of helpful information about standard JavaScript objects).

Depending on how big your list of cars is, though, it might be best to go the AJAX route.

You'd need to create a new controller which looked up the list of cars types given a manufacturer, and then forward on to a JSP which returned JSON (it doesn't have to be JSON, but it works quite well for me).

Then, use a library such as jQuery to retrieve the list of cars in your onchange event for the list of manufacturers. (jQuery is an excellent JavaScript framework to know - it does make development with JavaScript much easier. The documentation is very good).

I hope some of that makes sense?

Solution 4

Here is a working, cut-and-paste answer in jsp without any tag libraries or external dependencies whatsoever. The map with models is hardcoded but shouldn't pose any problems.

I separated this answer from my previous answer as the added JSP does not improve readability. And in 'real life' I would not burden my JSP with all the embedded logic but put it in a class somewhere. Or use tags.

All that "first" stuff is to prevent superfluos "," in the generated code. Using a foreach dosn't give you any knowledge about the amount of elements, so you check for last. You can also skip the first-element handling and strip the last "," afterwards by decreasing the builder length by 1.

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<%@page import="java.util.Map"%>
<%@page import="java.util.TreeMap"%>
<%@page import="java.util.Arrays"%>
<%@page import="java.util.Collection"%>
<%@page import="java.util.List"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Challenge</title>
</head>
<body onload="setModels()">
<% // You would get your map some other way.
    Map<String,List<String>> map = new TreeMap<String,List<String>>();
    map.put("porsche", Arrays.asList(new String[]{"911", "Carrera"}));
    map.put("mercedes", Arrays.asList(new String[]{"foo", "bar"}));
%>

<%! // You may wish to put this in a class
  public String modelsToJavascriptList(Collection<String> items) {
    StringBuilder builder = new StringBuilder();
    builder.append('[');
    boolean first = true;
    for (String item : items) {
        if (!first) {
          builder.append(',');
        } else {
          first = false;
        }
        builder.append('\'').append(item).append('\'');
    }
    builder.append(']');
    return builder.toString();
  }

  public String mfMapToString(Map<String,List<String>> mfmap) {
    StringBuilder builder = new StringBuilder();
    builder.append('{');
    boolean first = true;
    for (String mf : mfmap.keySet()) {
      if (!first) {
          builder.append(',');
      } else {
          first = false;
      }
      builder.append('\'').append(mf).append('\'');
      builder.append(" : ");
      builder.append( modelsToJavascriptList(mfmap.get(mf)) );
    }
    builder.append("};");
    return builder.toString();
  }
%>

<script>
var modelsPerManufacturer =<%= mfMapToString(map) %>
  function setSelectOptionsForModels(modelArray) {
    var selectBox = document.myForm.models;

    for (i = selectBox.length - 1; i>= 0; i--) {
    // Bottom-up for less flicker
    selectBox.remove(i);
    }

    for (i = 0; i< modelArray.length; i++) {
     var text = modelArray[i];
      var opt = new Option(text,text, false, false);
      selectBox.add(opt);
    }
  }

  function setModels() {
    var index = document.myForm.manufacturer.selectedIndex;
    if (index == -1) {
    return;
    }

    var manufacturerOption = document.myForm.manufacturer.options[index];
    if (!manufacturerOption) {
      // Strange, the form does not have an option with given index.
      return;
    }
    manufacturer = manufacturerOption.value;

    var modelsForManufacturer = modelsPerManufacturer[manufacturer];
    if (!modelsForManufacturer) {
      // This modelsForManufacturer is not in the modelsPerManufacturer map
      return; // or alert
    }
    setSelectOptionsForModels(modelsForManufacturer);
  }

  function modelSelected() {
    var index = document.myForm.models.selectedIndex;
    if (index == -1) {
      return;
    }
    alert("You selected " + document.myForm.models.options[index].value);
  }
  </script>
  <form name="myForm">
    <select onchange="setModels()" id="manufacturer" size="5">
      <% boolean first = true;
         for (String mf : map.keySet()) { %>
          <option value="<%= mf %>" <%= first ? "SELECTED" : "" %>><%= mf %></option>
      <%   first = false;
         } %>
    </select>

    <select onChange="modelSelected()" id="models" size="5">
      <!-- Filled dynamically by setModels -->
    </select>
  </form>

</body>
</html>
Share:
18,558
Sandman
Author by

Sandman

I'm a java developer at a large company in Croatia. I enjoy going out, listening to music and supporting my favorite football club, NK Zagreb. I also like to learn new stuff, especially when it's something about programming. Neil Gaiman's "Sandman" is my favorite graphic novel... hence the nickname :-)

Updated on June 04, 2022

Comments

  • Sandman
    Sandman about 2 years

    Ok, here's one for the Java/JavaScript gurus:

    In my app, one of the controllers passes a TreeMap to it's JSP. This map has car manufacturer's names as keys and Lists of Car objects as values. These Car objects are simple beans containing the car's name, id, year of production etc. So, the map looks something like this (this is just an example, to clarify things a bit):

    Key: Porsche
    Value: List containing three Car objects(for example 911,Carrera,Boxter with their respectable years of production and ids)
    Key: Fiat
    Value: List containing two Car objects(for example, Punto and Uno)
    etc...

    Now, in my JSP i have two comboboxes. One should receive a list of car manufacturers(keys from the map - this part I know how to do), and the other one should dynamicaly change to display the names of the cars when the user selects a certain manufacturer from the first combobox. So, for example, user selects a "Porsche" in the first combobox, and the second immediately displays "911, Carrera, Boxter"...

    After spending a couple of days trying to find out how to do this, I'm ready to admit defeat. I tried out a lot of different things but every time I hit a wall somewehere along the way. Can anybody suggest how I should approach this one? Yes, I'm a JavaScript newbie, if anybody was wondering...

    EDIT: I've retagged this as a code-challenge. Kudos to anybody who solves this one without using any JavaScript framework (like JQuery).

  • Sandman
    Sandman over 15 years
    Thanks for your answer, Phill. Actually, I'm using Spring. While your answer is a nice one, I'd still like to hear some other opinions :-)
  • Sandman
    Sandman over 15 years
    If I understand this correctly, every time a car manufacturer is added or removed from the Map, I'd have to add or remove the appropriate select tag in the JSP. Since I get this Map from a vendor's web service, I have no way of knowing how many elements the Map might have.
  • Sandman
    Sandman over 15 years
    And, I still don't know of a way to retrieve the names of the cars. The name of the car being a String in a Car object, that's stored in a List that's stored as a value in a Map. Whew... talk about complicated...
  • Sandman
    Sandman over 15 years
    First of all, thanks for your answer, extraneon. I'll use it if I don't find any other way. However, there are two issues here. The first one is that I'm not doing this for my own personal project, but as a part of a larger enterprise app for the company I'm currently employed at. Because of that,
  • Sandman
    Sandman over 15 years
    I might not be allowed to use JQuery. They're a bit touchy when it comes to introducing new frameworks in the app. But, if I can't get it to work any other way, I'll just have to get my boss to convince the business guys that that's the only way to do it. However, the larger issue is getting the map
  • Sandman
    Sandman over 15 years
    to work in JavaScript. I already mentioned that I'm getting it from a webservice, and my controller passes it to JSP, so I can't just hardcode it in JavaScript, I have to be able to map my map to a JavaScript object, so that I can manipulate it there. I'm still working on my own solution, though, so
  • Sandman
    Sandman over 15 years
    if I get anywhere, I'll post the solution here. Once again, everybody, thanks for all the answers, and excuse me for the long comment.
  • Sandman
    Sandman over 15 years
    Very nice, extraneon! However, the map is still hardcoded, so the challenge is still on... :-) To be honest, i managed to solve the problem, although the solution isn't pretty. The populating part is done pretty much the way you did it. I haven't posted it yet 'cause i have a lot to do but I'll post
  • extraneon
    extraneon over 15 years
    OK. I should have written a JSP and dynamically generate modelsPerManufacturer and the manufacturer SELECT. using nothing but the input Map. I'll update the answer tonight (it's now 7am here).