In legacy enterprise web applications using JSP and Spring MVC, rendering dropdowns with server-side values is a common practice. But what happens when the dropdown values need to change dynamically based on user selections? Especially when the options are served by custom JSP tags and driven by server-side session state?
In this blog post, we'll walk through how we implemented a smart, user-driven filtering mechanism for dropdowns all without touching or rewriting existing custom tags.
🔎 Problem Overview
We had a dropdown for selecting a cause of device malfunction. The list of causes depended on the types of devices selected from another multi-select dropdown.
The challenge?
The malfunction dropdown was rendered via a custom JSP tag (
<app:filteredDropdown>).The options were pulled from a generalized code table using a categoryGroup value.
We had to update the dropdown options dynamically based on selected device type codes.
✨ The Goal
✅ Detect which device types the user selected
✅ Send those types to the backend
✅ Backend updates session with new categoryGroup
✅ Page reloads and custom tag picks up new values via session
📄 Frontend (JSP) Setup
We added hidden fields to carry the server-generated filter state:
<input type="hidden" id="hiddenCategoryGroup" value="${categoryGroup}" />
<input type="hidden" id="hiddenDeviceIds" value="${chosenDeviceIds}" />
Device Dropdown
<app:select path="deviceList" id="deviceSelector" multiple="true" onchange="onDeviceChange()">
<c:forEach items="${sessionData.activeDevices}" var="device">
<app:option value="${device.id}" data-categorycode="${device.categoryCode}">
${device.name} (${device.categoryCode})
</app:option>
</c:forEach>
</app:select>
Malfunction Dropdown (Custom Tag)
<app:filteredDropdown path="issueCause"
gpc="143"
filterGroup="${categoryGroup}"
id="issueDropdown"
includeBlankOption="true"
blankOptionText="Select" />
JavaScript to Handle Dropdown Logic
function onDeviceChange() {
const selectedCodes = [];
$('#deviceSelector option:selected').each(function () {
const code = $(this).data('categorycode');
if (code) selectedCodes.push(code);
});
const categoryGroup = selectedCodes.join(',');
const selectedIds = ($('#deviceSelector').val() || []).join(',');
$.ajax({
type: "GET",
url: "/filterIssueDropdown",
data: { typeCodes: categoryGroup, selectedDeviceIds: selectedIds },
success: function () {
location.reload();
}
});
}
💻 Backend Spring Controller
@RequestMapping(value = "/filterIssueDropdown", method = RequestMethod.GET)
public String filterIssueDropdown(HttpServletRequest request,
Model model,
@ModelAttribute("sessionData") SessionData sessionData,
@RequestParam("typeCodes") String typeCodes,
@RequestParam("selectedDeviceIds") String selectedDeviceIds) {
sessionData.getDeviceState().setCategoryGroup(typeCodes);
sessionData.getDeviceState().setChosenDeviceIds(selectedDeviceIds);
return "redirect:/addDeviceIssue?reload=true";
}
🧩 Custom Tag Class: FilteredDropdownTag
public class FilteredDropdownTag extends AbstractDropdownTag {
private String gpc;
private String filterGroup;
@Override
protected int writeTagContent(TagWriter tagWriter) throws JspException {
initializeFilteredItems();
return super.writeTagContent(tagWriter);
}
private void initializeFilteredItems() {
Map<String, String> allItems = CodeHelper.getAllCodesForTypeAsMap(gpc);
if (filterGroup == null || filterGroup.isEmpty()) {
setItems(allItems);
return;
}
String[] groups = Arrays.stream(filterGroup.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toArray(String[]::new);
Set<String> allowedCodes = null;
for (String group : groups) {
List<String> groupCodes = MalfunctionGroupHelper.getAllowedCodesForGroup(group);
if (allowedCodes == null) {
allowedCodes = new HashSet<>(groupCodes);
} else {
allowedCodes.retainAll(groupCodes);
}
}
if (allowedCodes == null) {
allowedCodes = Collections.emptySet();
}
Map<String, String> filtered = new HashMap<>();
for (Map.Entry<String, String> entry : allItems.entrySet()) {
if (allowedCodes.contains(entry.getKey())) {
filtered.put(entry.getKey(), entry.getValue());
}
}
setItems(filtered);
}
public void setGpc(String gpc) {
this.gpc = gpc;
}
public void setFilterGroup(String filterGroup) {
this.filterGroup = filterGroup;
}
}
🔍 Explanation
initializeFilteredItems() retrieves all GPC values and filters them based on intersection logic.
This means only codes common to all selected device types are shown.
The custom tag does all the filtering server-side, using parameters passed from the session.
📖 Malfunction Group Helper Utility
public class MalfunctionGroupHelper {
private static final Map<String, List<String>> GROUP_CODE_MAP = new HashMap<>();
static {
GROUP_CODE_MAP.put("1", Arrays.asList("1", "2", "3", "6", "8", "9", "10"));
GROUP_CODE_MAP.put("2", Arrays.asList("2", "3", "5", "6", "8", "9", "10"));
GROUP_CODE_MAP.put("3", Arrays.asList("2", "3", "4", "5", "6", "7", "8", "9", "10"));
GROUP_CODE_MAP.put("4", Arrays.asList("2", "3", "5", "6", "8", "9", "10"));
GROUP_CODE_MAP.put("5", Arrays.asList("2", "3", "5", "6", "8", "9", "10"));
GROUP_CODE_MAP.put("6", Arrays.asList("2", "3", "4", "5", "6", "7", "8", "9", "10"));
}
public static List<String> getAllowedCodesForGroup(String group) {
return GROUP_CODE_MAP.getOrDefault(group.toUpperCase(), Collections.emptyList());
}
}
✅ Outcome
✔ Smooth, dynamic dropdown updates using existing infrastructure
✔ No need to rewrite the custom JSP tag
✔ AJAX-enabled UX while keeping server-driven rendering intact
📝 Final Thoughts
When working in JSP-heavy legacy systems, you can still build reactive behavior using a mix of:
Session state
Light AJAX
Custom tag parameter binding
This hybrid approach helps modernize the UI without a full rewrite. Have a similar challenge? Try this pattern first — it might just save you from rewriting that 10-year-old tag library!
Note: All class and method names in this post are generalized examples.
Top comments (0)