S2 - Ajax Dynamic Double Select Demo - complete code included

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
Report Content as Inappropriate

S2 - Ajax Dynamic Double Select Demo - complete code included

Ray Clough
A common problem is having two related select controls on a page.  Call them the 'trigger' and the 'target'.  This is an ideal use case for Ajax, because you don't want to refresh the entire page to re-populate the 'target' control when the user selects something in the 'trigger'.  We have produced a very nice demo of this using the S2 Ajax tags, and I am enclosing the Action class and a jsp page which give the example.  I hope this is useful to people.  It was a difficult problem to solve, not helped by the very poor quality of the Ajax documentation on the S2 site (sorry for beefing, but I spent lots of extra hours because of it).  The documentation is incomplete, disorganized, contradictory, and in many cases just plain wrong.

Anyway, here is the example:

The page calling the demo uses a link to "/greetings/AjaxDynDoubleSelect_setup.action"  Here is the mapping of the action from struts.xml:

                <action name="AjaxDynDoubleSelect_*" method="{1}"
                                class="greetings.struts2.action.AjaxDDS_DemoAction" >
                        <result name="success" type="tiles" > /greetings/AjaxDemo.jspx </result>                       

This is the Action class:

package greetings.struts2.action;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionSupport;

 * @author   Ray Clough, Michael Matz.
 * Created: May 29, 2007
 * Project: GreetingsEarth_S2_S1_Tiles
 * Package: greetings.struts2.action
 * File: AjaxDDS_DemoAction.java
 * <br/>
 * Description:
 * <br/>
 * This Struts-2 Action class is used to handle the data actions for the
 * display and function of the Ajax Dynamic Double Select (DDS) demonstration.
 * The point is to use two 'select' controls, which contain related data.
 * The first select control, referred to here as the 'trigger', reacts to
 * an 'onchange' event to set the contents of the second select control,
 * referred to here as the 'target', to a corresponding list.  The update
 * of the 'target' is done with Ajax, meaning that the entire page does not
 * need to be refreshed.  This is a common, but fairly trick operation.
public class AjaxDDS_DemoAction extends ActionSupport {

        /** Objects implementing 'Serializable' should define 'serialVersionUID' */
    private static final long serialVersionUID = -2394017505624866795L;
        private Map<String,List<String>> dataModel;
        private String triggerVal = null;

         * This constructor initializes the data set which is displayed in
         * the two select controls.
        public AjaxDDS_DemoAction() {
                // create the data model
                List<String> grains = new ArrayList<String>();
                List<String> fruits = new ArrayList<String>();
                List<String> veggies = new ArrayList<String>();
                List<String> nuts = new ArrayList<String>();
                List<String> other = new ArrayList<String>();

                grains.add("Wheat"); //$NON-NLS-1$
                grains.add("Barley"); //$NON-NLS-1$
                grains.add("Oats"); //$NON-NLS-1$
                grains.add("Rye"); //$NON-NLS-1$
                grains.add("Corn"); //$NON-NLS-1$

                fruits.add("Apples"); //$NON-NLS-1$
                fruits.add("Oranges"); //$NON-NLS-1$
                fruits.add("Limes"); //$NON-NLS-1$
                fruits.add("Kumquats"); //$NON-NLS-1$
                fruits.add("Dates"); //$NON-NLS-1$
                fruits.add("Persimmons"); //$NON-NLS-1$

                veggies.add("Leeks"); //$NON-NLS-1$
                veggies.add("Onions"); //$NON-NLS-1$
                veggies.add("Beans"); //$NON-NLS-1$
                veggies.add("Peas"); //$NON-NLS-1$
                veggies.add("Carrots"); //$NON-NLS-1$

                nuts.add("Pecans"); //$NON-NLS-1$
                nuts.add("Pistachios"); //$NON-NLS-1$
                nuts.add("Walnuts"); //$NON-NLS-1$
                nuts.add("Filberts"); //$NON-NLS-1$
                nuts.add("Pinole"); //$NON-NLS-1$
                nuts.add("Cashews"); //$NON-NLS-1$

                other.add("Ice Cream"); //$NON-NLS-1$
                other.add("Chocolate"); //$NON-NLS-1$
                other.add("Fudge"); //$NON-NLS-1$
                other.add("Pie"); //$NON-NLS-1$
                other.add("Cake"); //$NON-NLS-1$

                dataModel = new LinkedHashMap<String,List<String>>();
                dataModel.put("Grains", grains); //$NON-NLS-1$
                dataModel.put("Fruits", fruits); //$NON-NLS-1$
                dataModel.put("Vegetables", veggies); //$NON-NLS-1$
                dataModel.put("Nuts", nuts); //$NON-NLS-1$
                dataModel.put("Other", other); //$NON-NLS-1$


         * @return The 'setup()' method sets the initial value for the 'trigger',
         * which will result in a compatible 'target' list being displayed
        public String setup() {

                // initial value for TargetList
                this.triggerVal = "Grains"; //$NON-NLS-1$

                return SUCCESS;

         * The 'update()' method is called when the user selects a value from
         * the 'trigger' list.
         * @return The method writes the option elements to the response
         * output Writer, and returns 'null'.  Returning 'null' tells the
         * Struts processor that the processing cycle is complete, and the
         * page asynchronously displays the returned values.
        public String update() {
                HttpServletRequest request = ServletActionContext.getRequest();
                triggerVal = request.getParameter("triggerValue");

                System.out.println("triggerVal = " + triggerVal);

                HttpServletResponse response = ServletActionContext.getResponse();
                response.setHeader("Cache-Control", "no-cache");

                Collection<String> targetValues = this.getTargetList(triggerVal);
                PrintWriter out = null;

                try {
                        out = response.getWriter();
                        out.print("<select id='targetSelectionId' >");
                        for (String targetVal : targetValues) {
                                out.println("<option>" + targetVal + "</option>");
                } catch (IOException e) {
                } finally {
                        if (out != null) {

                return null;

         * This method is called by the 'list' attribute on the trigger
         * select control.
         * @return Returns the collection used to populate the "trigger'
         * select box.
        public Collection<String> getTriggerList() {
                Collection<String> keys = dataModel.keySet();
                return keys;

         * @return Returns the Collection used to populate the 'target'
         * select box, which is a function of the selected value in the
         * 'trigger' select box.
        private Collection<String> getTargetList(String trigger) {
                Collection<String> target = dataModel.get(trigger);
                if (target == null) {
                        return Collections.emptyList();
                return target;

         * This method is called by the 'trigger' select box, which has
         * the 'name' attribute value of 'triggerValue'
         * @param trigVal
        public void setTriggerValue(String trigVal) {
                System.out.println("setTriggerValue() = " + trigVal); //$NON-NLS-1$

                if (trigVal == null || trigVal.length() == 0) return;
                this.triggerVal = trigVal;

         * This is called by the s:url tag for the 'target' s:div tag, which
         * uses this value to append the initial target value to the url.
         * @return Returns the current 'trigger' value.
        public String getTriggerValue() {
                System.out.println("getTriggerValue() returns: "   //$NON-NLS-1$
                                + this.triggerVal);
                return this.triggerVal;

}             // end class 'AjaxDDS_DemoAction'

And here is the jsp page (it is in 'jspx' format):

<?xml version="1.0" encoding="UTF-8"?>
<jsp:root version="2.0"
                xmlns:tiles="http://struts.apache.org/tags-tiles"  >

        <jsp:output omit-xml-declaration="true"
                doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"  />

                String basePath = request.getScheme()
                                + "://"
                                + request.getServerName()
                                + ":"
                                + request.getServerPort()
                                + request.getContextPath();

        <jsp:directive.page contentType="text/html" />

        <title>Dynamic Double Select with Ajax</title>
        <s:head theme="ajax" />
        <meta name='Author' content='Ray Clough' />
        <meta name='keywords' content='Greetings Earth - Struts_2 Demo'/>
        <meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />
        <meta http-equiv="pragma" content="no-cache" />
        <meta http-equiv="cache-control" content="no-cache" />

<script type="text/javascript"> // <![CDATA[ var basePath = "${BasePath}"; function updateTarget() { //alert('function: updateTarget()' ); var triggerId = document.getElementById("triggerId"); var triggerValue = triggerId.value; var triggerWidget = dojo.widget.byId("targetDivId"); //alert(triggerWidget); var target = document.getElementById("targetDivId"); var url = target.getAttribute("href"); var newUrl = basePath + "/greetings/AjaxDynDoubleSelect_update.action?triggerValue=" + triggerValue; triggerWidget.setUrl(newUrl); /* this publishes the topic, which is registered to the div tag, causing it to refresh itself. */ dojo.event.topic.publish("updateTargetTopic", "triggerValue:" + triggerValue); /* this is an alternative to publish (above) */ //triggerWidget.refresh(); } // ]]> </script>


<form id="ddsFormId" >

Trigger:                                 <s:select id="triggerId"  name="triggerValue"
                                                onchange="javascript: updateTarget(); " />
Target:                                 <s:url id="target_url"
                                                value="${BasePath}/greetings/AjaxDynDoubleSelect_update.action" >
                                        <s:param name="triggerValue" value="%{triggerValue}" />
                                <s:div id="targetDivId"
                                                theme="ajax" listenTopics="updateTargetTopic"
                                                href="%{target_url}"  >