Dynamic Drop Down Selection

Dynamic Drop Down Selection in Spring using jQuery and no AJAX

In this post, I’m going to demonstrate one way to use jQuery to dynamically populate one drop down from another using values loaded to the page on the initial load.  The example application is a small Spring Boot application, although the focus of this post will be on the user interface.  In the example, the page is loaded with a JSON string enclosed in a hidden field.  That JSON string represents a map that maps values in the first drop down, to a map of values with which to populate the second drop down.

[Edited 2021-07-15]: I got a comment that my example didn’t work. Since the original is 3 years old, that’s not terribly surprising. I’ve updated the dependencies, both here and in the example code in GitHub.

Prerequisites

  • Java 1.8
  • Java IDE (example uses eclipse)
  • Basic understanding of Java and jQuery
  • Browser
  • Internet connection

Project Setup

The project is a simple Spring Boot application with just one controller and one page.  The data used to populate the drop down files is stored in a YAML file called dropdownValues.yaml.  The example uses categories of shapes.  Here’s the contents of dropdownValues.yaml to illustrate the structure of the map.

polygon:
 polygon|triangle : triangle
 polygon|rectangle : rectangle
 polygon|square : square
 polygon|rhombus : rhombus
 polygon|trapezoid : trapezoid
 polygon|pentagon : pentagon
 polygon|hexagon : hexagon
 polygon|heptagon : heptagon
 polygon|octagon : octagon
 polygon|nonagon : nonagon
 polygon|decagon : decagon
curved with circular arcs:
 curved with circular arcs|circle : circle
 curved with circular arcs|crescent : crescent
 curved with circular arcs|semicircle : semicircle
 curved with circular arcs|heart : heart
curved without circular arcs:
 curved without circular arcs|ellipse : ellipse
 curved without circular arcs|oval : oval

Gradle Dependencies

The in addition to Spring Boot, Thymeleaf and jQuery, the application uses SnakeYaml to read the dropdownValues.yaml file and Gson to create the JSON string that’s sent to the browser.

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:2.5.2'
implementation 'org.springframework.boot:spring-boot-starter-web:2.5.2'
implementation 'org.springframework.boot:spring-boot-starter-validation:2.5.2'
implementation('org.yaml:snakeyaml')
implementation('com.google.code.gson:gson:2.8.7')
implementation('org.webjars:jquery:3.6.0')
implementation('org.webjars:jquery-ui:1.12.1')
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.5.2'
testImplementation 'org.springframework.boot:spring-boot-test:2.5.2'
testImplementation 'org.junit.vintage:junit-vintage-engine:5.7.2'

The Controller

The controller constructor gets a org.springframework.core.io.Resource reference to the dropdownValues.yaml file and loads a map of the values using SnakeYaml.

private Map<String, LinkedHashMap> dropdownMap;
 
public DropdownController(@Value("classpath:dropdownValues.yaml") Resource dropdownYaml) {
   InputStream input = null;
   try {
      input = dropdownYaml.getInputStream();
      Yaml yaml = new Yaml();
      dropdownMap = (Map<String, LinkedHashMap>) yaml.load(input);
      System.out.println("Dropdown Map: " + dropdownMap);
   } catch (IOException e) {
      e.printStackTrace();
   } finally {
      if (input != null) {
         try {
            input.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
}

The controller uses the org.springframework.web.bind.annotation.ModelAttribute annotation to make the values used in the drop down available on the page.

For the Shape Types drop down, the getShapeTypes() method prepares a Map with just the keys of the dropdownMap as both key and value.  This value can be accessed on the Thymeleaf page like this ${shapeTypes}.

@ModelAttribute("shapeTypes")
public Map getShapeTypes() {
   Map shapeTypeMap = dropdownMap.keySet()
      .stream()
      .distinct()
      .sorted()
      .collect(Collectors.toMap(Function.identity(), Function.identity(),
         (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u));},
          LinkedHashMap::new));
   return shapeTypeMap;
}

The getShapeSuggestionsJSON() method uses Gson to create a JSON string out of the dropdownMap.

@ModelAttribute("shapeSuggestionsJSON")
public String getShapeSuggestionsJSON() {
   String shapeSuggestions = "";
   Gson gson = new Gson();
   shapeSuggestions = gson.toJson(dropdownMap);
   System.out.println("Shape Suggestions: " + shapeSuggestions);
   return shapeSuggestions;
}

The HTML

The first select contains the Shape Type and populates its values from the Map defined with @ModelAttribute("shapeTypes") in the controller class.  The second select has no options as it will be populated based on the selection made in the Shape Type dropdown.  The “shapeSuggestVals” span contains the string defined as @ModelAttribute("shapeSuggestionsJSON").

<div>      
  <select id="shapeType" th:field="*{shapeType}">
    <option value="" selected="selected">N/A
    <option th:each="shapeOpt : ${shapeTypes.entrySet()}" th:value="${shapeOpt.key}" th:text="${shapeOpt.value}"></option>
  </select>
  <label for="shapeType">Shape Type</label>
</div>
<div>
   <select id="shape" th:field="*{shape}"></select>
   <label class="active" for="shape">Shape</label>
   <span id="shapeSuggestVals" class="suggestVals" th:text="${shapeSuggestionsJSON}"></span>
</div>

In the jQuery/JavaScript, the JSON String is parsed into a variable using JavaScript’s built in function JSON.parse().  The shapeSuggestions variable will then contain a map of maps, similar to the map created in the controller.  The work is done in the onChange() method of the Shape Type select.  When the user changes the value of the Shape Type drop down, this method first saves the selected value in the variable called “type.”  Using that value as the key gets the map of shapes from the shape suggestions map in this line var shapeMap = shapeSuggestions[type]; After acquiring the shapeMap, we loop through the map and build up a string of HTML to represent the options.  Once we have our HTML built up, we call $("#shape").html(htmlOptions); to replace any existing options on the Shape drop down.  The final line $("#shape").trigger('contentChanged'); lets the page know that we’ve changed the content in the Shape drop down.

<script th:src="@{/webjars/jquery/3.2.0/jquery.js}"></script>
<script th:src="@{/webjars/jquery-ui/1.12.1/jquery-ui.min.js}"></script>
<script type="text/javascript">
   $(function() { $(".suggestVals").hide(); 
   var shapeSuggestions = JSON.parse($("#shapeSuggestVals").text()); 
   console.log(shapeSuggestions);        

   $("#shapeType").change(function(e) {   
      var type = $("#shapeType").val();   
      console.log("ShapeType changed to " + type);   
      var htmlOptions = "";   
      var shapeMap = shapeSuggestions[type];   
      console.log(shapeMap);   
      $.each(shapeMap, function(shapeKey, shapeVal){ 
         console.log("shape option: " + shapeKey + " " + shapeVal);     
         htmlOptions = htmlOptions + "" + shapeVal +"";   
      });   
      $("#shape").html(htmlOptions);   
      $("#shape").trigger('contentChanged');    
   });
});
</script>

Conclusion

This is one way of handling dynamic drop down population in cases where you can’t or don’t want to use AJAX for one reason or another.  This is how the finished example behaves.

DropdownExample

The full example code is available on GitHub https://github.com/amdegregorio/DropdownPopulationExample

4 thoughts on “Dynamic Drop Down Selection

Leave a comment