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

StackOverflow https://stackoverflow.com/questions/156852

Question

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).

Was it helpful?

Solution 2

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]);
  }      
}

OTHER TIPS

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>

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?

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>

How about something like this, using prototype? First, your select box of categories:

<SELECT onchange="changeCategory(this.options[this.selectedIndex].value); return false;">
   <OPTION value="#categoryID#">#category#</OPTION>
   ...

Then, you output N different select boxes, one for each of the sub-categories:

<SELECT name="myFormVar" class="categorySelect">
...                                        

Your changeCategory javascript function disables all selects with class categorySelect, and then enables just the one for your current categoryID.

// Hide all category select boxes except the new one
function changeCategory(categoryID) {

   $$("select.categorySelect").each(function (select) {
      select.hide();
      select.disable();
   });

   $(categoryID).show();
   $(categoryID).enable();
}

When you hide/disable like this in prototype, it not only hides it on the page, but it will keep that FORM variable from posting. So even though you have N selects with the same FORM variable name (myFormVar), only the active one posts.

Not that long ago I thought about something similar.

Using jQuery and the TexoTela add-on it wasn't all that difficult.

First, you have a data structure like the map mentioned above:

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

Your HTML should look comparable to:

<select size="4" id="manufacturers">
</select>
<select size="4" id="models">
</select>

Then, you fill the first combo with jQuery code like:

$(document).ready(
  function() {
    $("#bronsysteem").change( manufacturerSelected() );
  } );
);

where manufacturerSelected is the callback registered on the onChange event

function manufacturerSelected() {
  newSelection = $("#manufacturers").selectedValues();
  if (newSelection.length != 1) {
    alert("Expected a selection!");
    return; 
  }
  newSelection = newSelection[0];
  fillModels(newSelection);     
}

function fillModels(manufacterer) {
    var models = map[manufacturer];

    $("models").removeOption(/./); // Empty combo

    for(modelId in models) {
       model = models[modelId];
       $("models").addOption(model,model); // Value, Text
    }
}

This should do the trick.

Please note that there may be syntax errors in there; I have edited my code to reflect your use case and had to strip quite a lot out.

If this helps I would appreciate a comment.

As an add-on on my previous post; You can put a script tag in your JSP where you iterate over your map. An example about iterating over maps can be found in Maps in Struts.

What you would like to achieve (if you don't care about form submission) is I think something like:

<script>
  var map = {
  <logic:iterate id="entry" name="myForm" property="myMap">
     '<bean:write name=" user" property="key"/>' : [
     <logic:iterate id="model" name="entry" property="value">
       '<bean:write name=" model" property="name"/>' ,
     </logic:iterate>
     ] ,
 </logic:iterate>
  };
</script>

You still have some superfuous "," which you might wish to prevent, but I think this should do the trick.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top