Simple Communicating Comboboxes in a jMaki Application

Note: I have updated this blog in response to Greg's comment.

It's been a long time since I've blogged because I was on maternity leave. Now that I'm back and have brushed the cobwebs off my blog, I'm trying to get into the swing of things again, including working with jMaki.

Because it's been so long, I decided to start with something simple. I heard from Greg Murray that, with the new features available with jMaki 1.0, you can now easily get a user action on one combobox widget to change the set of data in another combobox widget.

You've probably seen this use case quite a bit in forms on the web, such as when you need to enter your address. The form asks you to select your state from a list of states in a combobox. When you select a state, another combobox is updated with the list of cities located in that state, as shown in this screenshot:

So, I decided this would be a nice, simple example to help me get to know what's new with jMaki. Here are the steps I took:

  1. Install the NetBeans plugin for jMaki 1.0 into my NetBeans IDE.
  2. Create a new web application and add the jMaki framework to it.
  3. Near the top of the JSP page that the IDE loads into the editor after you create the web application, add the following useBean tag:
            <jsp:useBean id="StateBean" scope="session" class="dualCombobox.StateBean" /> 
    
    Later on in this blog, I'll show you what to put in StateBean.java.
  4. Drag two Dojo combobox widgets onto the JSP page.
  5. Change the first combobox widget tag to look like this:
           <a:widget name="dojo.combobox"
                      publish="/cb/getState"
                      value="${StateBean.states}" />
    
    This combobox widget displays the names of a set of US states, which it fetches from the states property of StateBean in the following JavaScript object literal format:
        [{label: 'Alaska', value: 'AK'},
         {label: 'Arizona', value: 'AZ'},
         {label: 'California', value: 'CA'},
         {label: 'Oregon', value: 'OR'}]
    
    When the user selects a state from this widget, the powerful publish and subscribe mechanism publishes the value of the user's selection to the topic /getState, which is a subtopic of the topic, /cb, as shown in the widget tag's publish property.

    The value that a particular kind of widget is allowed to publish is determined by the widget's data model. See Carla's blog for more information about widget data models.

  6. Change the second combobox widget tag to look like this:
            <a:widget name="dojo.combobox"
                      subscribe="/cb"
                      value="${StateBean.cities}"  />
    
    When the page loads, this combobox widget fetches its initial set of city values from the cities property of StateBean. To update its values when a user chooses a state from the other combobox, the cities combobox subscribes to the same parent topic, /cb to which the states combobox publishes the state code that the user chose, as you can see by looking at the widget's subscribe property. In the next step, you'll see how the cities combobox updates its values.
  7. Add the following script to the end of the glue.js file included in the web application under the Web Pages directory.
    jmaki.subscribe("/cb/getState/\*", function(args) {
         var message = args.value;
         jmaki.doAjax({method: "POST",
            url: "Service?message=" + encodeURIComponent(message),
            callback: function(_req) {
                var tmp = _req.responseText;
                var obj = eval("(" + tmp + ")");
                jmaki.publish('/cb/setValues', obj);
                // handle any errors
            }
        });
    });
    
    The subscribe function subscribes to all subtopics of the /cb/getState/ topic because of the wildcard character at the end of the topic argument of the function. Because the states combobox widget publishes the state code to the /cb/getState topic, the subscribe function receives the state code as the args variable that is passed to it. The subscribe function takes the state code and posts it to the Service servlet, which returns the appropriate set of cities. Finally, the function publishes this set of cities to the /cb/setValues topic.

    By default, all combobox widgets automatically obtain their values in response to a user action by subscribing to the global /setValues topic that jMaki provides unless the developer specifies otherwise. You can also make a combobox widget subscribe to a /setValues subtopic of a developer-defined parent topic, which is what I do in this example. Because the cities combobox subscribes to the parent topic, /cb, it also subscribes to the /setValues subtopic by default and is therefore automatically updated with the new set of cities.

    Normally, if you have only one combobox in a page, you do not have to create a parent topic for the purpose of updating values in the combobox; instead, you can simply publish the new values to the global /setValues topic, and the combobox will be updated with these new values automatically. We can't do that in this case because we have two comboboxes. Because all combobox widgets subscribe to the global /setValues topic automatically, we would end up setting the values of both of our comboboxes to the set of cities returned by the Service servlet if we published to the global /setValues topic.

  8. Create a JavaBeans component called StateBean that builds JSONArray objects for the state names and the city names that correspond to a chosen state:
        ...
        protected String[] states =
                new String[] {"Alaska", "Arizona", "California", "Oregon" };    
        
        protected String[] stateCodes =
                new String[] {"AK", "AZ", "CA", "OR" };
    
        private ResourceBundle cityNames = null;
        
        public StateBean() {
            this.init();
        }
        
        private void init() {
            cityNames = ResourceBundle.getBundle("mapcity.cities");
        }
        ... 
    
        public String getStates() throws JSONException {
            JSONArray statesData = new JSONArray();
            JSONObject stateData = new JSONObject();
    
            for (int loop = 0; loop < states.length; loop++) {
                stateData.put("label", states[loop]);
                stateData.put("value", stateCodes[loop]);
                statesData.put(stateData);
                stateData = new JSONObject();
            }
    
            return jsonArrayToString(statesData, new StringBuffer());
        }
    
        public String getNewCities(String state) throws JSONException {
            JSONObject city = new JSONObject();
            JSONArray cities = new JSONArray();
            String[] names = null;
            
            try {
                names = cityNames.getString(state).split(",");
                // obtaining the cities from a resource bundle keyed by state code, but you
                // can get them however you like
            } catch (Exception e){
                return null;
            }
            for(int i = 0; i < names.length; i++){
                city.put("label", names[i]);
                city.put("value", names[i]);
                cities.put(city);
                city = new JSONObject();
            }
            
            return jsonArrayToString(cities, new StringBuffer());
    
        }
        public String jsonArrayToString(JSONArray ja, StringBuffer buff)  throws JSONException {
            if (buff == null) buff = new StringBuffer("[");
            else buff.append("[");
            
            for (int key=0; (ja != null) && key < ja.length(); key++) {
                String value = null;
                if (ja.optJSONObject(key) != null){
                    jsonToObjectLibertal(ja.optJSONObject(key), buff);
                } else if (ja.optJSONArray(key) != null) {
                    jsonArrayToString(ja.optJSONArray(key), buff);
                } else if (ja.optLong(key, -1) != -1) {
                    value = ja.get(key) + "";
                    buff.append(value);
                } else if (ja.optDouble(key, -1) != -1) {
                    value = ja.get(key) + "";
                    buff.append(value);
                } else if (ja.optBoolean(key)) {
                    value = ja.getBoolean(key) + "";
                    buff.append(value);
                } else if (ja.opt(key) != null) {
                    Object obj = ja.opt(key);
                    if (obj instanceof Boolean) {
                        value = ja.getBoolean(key) + "";
                    } else {
                        value = "'" + ja.get(key) + "'";
                    }
                    buff.append(value);
                }
                if (key < ja.length() -1) buff.append(",");
            }
            buff.append("]");
            return buff.toString();
        }
        
    }
    
    
    The getStates and the getNewCities methods use the org.json APIs to create a JSONArray object representation of the data. The jsonArrayToString method is boilerplate code that you can copy to your bean. What it does is converts the JSON code created by the JSON APIs to JavaScript object literals. Greg says that he might include this code in jMaki 1.1 so that you won't have to copy it.

    In addition to these methods, you'd also need to add a getCities method that returns the initial set of city names for the second combobox.

  9. Create a servlet called Service, and add the code that gets the value from the client, passes it to StateBean, receives the list of cities from StateBean, and returns this list of cities to the client:
        public void doPost(
            HttpServletRequest request,
            HttpServletResponse response) throws IOException, ServletException {
            HttpSession session = request.getSession();
            StateBean stateBean = (StateBean)session.getAttribute("StateBean");
    
       
            String cityData = new String();
            String message = request.getParameter("message");
    
            try{
                cityData = stateBean.getNewCities(message);
            } catch (Exception e){
                System.out.println("could not get city data");
            }
       
            PrintWriter writer = response.getWriter();
            writer.write(cityData);
    
            
            session.setAttribute("stateBean", stateBean);
        }
    
Now, you're done. Next time, I will add a map to this application so that you can plot the chosen city on a map.

As this example has shown, the publish and subscribe mechanism gives you a simple and powerful way to get widgets to interact in jMaki. Thank you, jMaki development team! And thank you, Greg, for enduring all my questions.

For more in-depth information and real-world examples using publish and subscribe, take a look at these blogs:

Comments:

Post a Comment:
Comments are closed for this entry.
About

jenniferb

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today