views:

8571

answers:

8

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

+1  A: 

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?

Phill Sacre
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
A: 

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.

Chase Seibert
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
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
A: 

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.

extraneon
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
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
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
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
A: 

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.

extraneon
+1  A: 

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>
extraneon
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
Sandman
it as soon as I can, probably the day after tomorrow. Although your solution isn't complete, I'm giving you a vote up, just for the effort you put into it :-)Hint: my solution involves using a scriptlet.
Sandman
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).
extraneon
+1  A: 

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"&gt;

<%@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>
extraneon
+2  A: 

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

Sandman
A: 

Hi Sandman,

I am using your code, but the onchange-event is not fired, if I change the selection of my first combo-box. I am also using spring, but with spring tags. This means, instead of "select" I am using "form:select". Do you have any idea to fire an onchange-event by "form:select"? Thanks

tcn