Saturday, December 17, 2011

Marrying Wicket And jQuery UI Autocomplete Ajax

* One in a series of articles on Wicket Interfaces & Methods To Master *



Note: the code demonstrated in this article uses the query (v1.6.2) and jquery.ui (v1.8.16) Javascript libraries for client-side scripting and the Google gson library for servers-side JSON support.

Here we pick up from my previous article, Marrying Wicket And jQuery Ajax, but today I will demonstrate how to marry Wicket and jQuery UI, specifically the jQuery UI autocomplete component.

Auto completion allows the user to quickly lookup and select values with minimum key strokes, a fact that has made it a very popular user interface design element. And while it is certainly possible to code the javascript necessary to support auto completion by hand, the jQuery UI library provides a very nice implementation and as a bonus you also get all the graphical sugar that the UI provides.

When I began trying to implement this I tried Googling to find out how others have approached this and as a result I learned that no one seemed to have posted a pure Wicket solution but rather relied upon wiQuery's implementation. As I prefer to not use wiQuery and to roll my own reusable "pure" Wicket based solutions I came up with the solution that you will find here in this article which only uses Wicket 1.5 and the jQuery UI library. So lets roll up our sleeves and get on with it...

Here's the Java code:

/*
 * HomePage.java
 *
 * Created on December 3, 2011, 10:21 AM
 */
package com.myapp.wicket;

import com.google.gson.Gson;
import java.lang.String;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.handler.TextRequestHandler;
import org.apache.wicket.util.string.StringValue;

public class HomePage extends WebPage {

    private final Form<HomePage> form1;
    private final AbstractAjaxBehavior aab1;
    private final TextField<String> state;

    static class State {

        State() {}

        State(String name) {
            this.name = name;
        }
        
        String name;
    }

    public HomePage() {

        add(form1 = new Form<HomePage>("form1"));

        form1.add(state = new TextField<String>("state"));

        /*
         * Ajax Behavior provides an event
         * listener as well as the request
         * handler.
         */

        add(aab1 = new AbstractAjaxBehavior() {

            @Override
            public void renderHead(Component component, IHeaderResponse response) {
                super.renderHead(component, response);
                response.renderJavaScript("var callbackUrl = '" + aab1.getCallbackUrl() + "';", "callbackurl");
            }

            // handle the ajax request
            @Override
            public void onRequest() {
                System.out.println("ajax request received");

                RequestCycle requestCycle = RequestCycle.get();
                Request request = requestCycle.getRequest();
                IRequestParameters irp = request.getRequestParameters();
                StringValue state = irp.getParameterValue("term");
                List<State> statesLike = MockDb.getStatesLike(state.toString());
                String json = convertListToJson(statesLike);
                requestCycle.scheduleRequestHandlerAfterCurrent(new TextRequestHandler("application/json", "UTF-8", json));
            }
        });

    }

    /*
     * Convert List to json object
     */
    private String convertListToJson(List<?> matches) {
        Gson gson = new Gson();
        String json = gson.toJson(matches);
        return json;
    }

    /*
     * A Mock Database
     */
    static class MockDb {

        private static final String[] states = new String[]{
            "Alabama",
            "Alaska",
            "Arizona",
            "Arkansas",
            "California",
            "Colorado",
            "Connecticut",
            "Delaware",
            "Florida",
            "Georgia",
            "Hawaii",
            "Idaho",
            "Illinois",
            "Indiana",
            "Iowa",
            "Kansas",
            "Kentucky",
            "Louisiana",
            "Maine",
            "Maryland",
            "Massachusetts",
            "Michigan",
            "Minnesota",
            "Mississippi",
            "Missouri",
            "Montana",
            "Nebraska",
            "Nevada",
            "New Hampshire",
            "New Jersey",
            "New Mexico",
            "New York",
            "North Carolina",
            "North Dakota",
            "Ohio",
            "Oklahoma",
            "Oregon",
            "Pennsylvania",
            "Rhode Island",
            "South Carolina",
            "South Dakota",
            "Tennessee",
            "Texas",
            "Utah",
            "Vermont",
            "Virginia",
            "Washington",
            "West Virginia",
            "Wisconsin",
            "Wyoming"
        };

        static List<State> getStatesLike(String target) {

            List<State> matches = new ArrayList<State>();
            for (String s : states) {
                if (s.toLowerCase().startsWith(target.toLowerCase())) {
                    State state = new State(s);
                    matches.add(state);
                }
            }
            return matches;
        }
    };
}


In the code above we create an AbstractAjaxBehavior and add it to the page to provide a request handler for the ajax call. Our AbstractAjaxBehavior class overrides two methods:

  1. renderHead, in which we grab the behavior's call back url and contribute it via Javascript to the page where it will be used as the url to call by the jQuery UI autocomplete method.
  2. onRequest, in which we process the ajax request and in which we use the request parameter whose name is 'term' to look up all the states that begin with its value and convert the names of those states to an array of json objects in the form of [{name : "statename"}...]. Finally, we schedule a TextRequestHandler passing that array as a parameter which will be sent back to the client as the result of the ajax call. Note that our response content type is "application/json", "UTF-8". 

And that's really all it takes for Wicket v1.5 to support ajax request that must return json. Now, lets take a look at the markup including the Javascript needed to support our integration with jQuery UI and Wicket:

<!DOCTYPE html> 
<html xmlns:wicket="http://wicket.apache.org"> 
    <head> 
        <meta charset="UTF-8"> 
        <title>Wicket Example</title> 
        <link type="text/css" href="css/start/jquery-ui-1.8.16.custom.css" rel="Stylesheet" />    
        <script type="text/javascript" src="js/jquery.js"></script>
        <script type="text/javascript" src="js/jquery-ui-1.8.16.custom.min.js"></script>
        <wicket:head>
        </wicket:head>    
        <script type="text/javascript" >
        $(document).ready(function(){
            $('input.jqueryid_state').autocomplete({source: function(req, add){
                //pass request to server  
                $.ajax({
                    url: callbackUrl,
                    type: 'GET',
                    cache: false,
                    data: req,
                    dataType: 'json',
                    success: function(json){
                        var suggestions = [];  

                        //process response  
                        $.each(json, function(i, val){  
                            suggestions.push(val.name);  
                        });
                        
                        // call autocomplet callback method with results
                        add(suggestions);
                    },
                    error: function(XMLHttpRequest, textStatus, errorThrown){
                        //alert('error - ' + textStatus);
                        console.log('error', textStatus, errorThrown);
                    }
                });            
            }});
        });
        </script>
    </head> 
<body>
    <h1>Marrying Wicket And jQuery Ajax</h1>
    <h2>jQuery UI Auto Complete State Lookup</h2>
    <form class="jqueryid_form2" wicket:id="form1">
        US State Lookup: <input class="jqueryid_state" type="text" wicket:id="state"/><br/>
                    <div id="statelist"/>
    </form>
</body> 
</html>


In the above Javascript code we create a jQuery ready handler which sets up jQuery UI autocomplete. Here we use a json object that contains the property named source and which provides a callback function that autocomplete will call to retrieve the results matching the user's input. In this callback we use jQuery's ajax method to make an ajax call back to the Wicket event listener whose url is stored in the global variable callbackUrl which we obtained from the AbstractAjaxBehavior we created in our Java code.

Finally, the result from the ajax call, which is an array of json objects whose val properties are the names of the states are passed back to autocomplete by calling the add callback method. jQuery Ui autocomplete uses these to present a list of options to the user as can be seen from the following screen shot:

That's pretty impressive I think, especially considering the minimal amount of code we had to write, both on the client and the server. Being able to use this technique along with using Wicket's default Ajax implementation is empowering, allowing us to use the right technologies that best suits the use case.

I hope you enjoyed following along with me and please remember to come back soon because I always have something interesting to write about Wicket.

16 comments:

  1. Thanks for sharing the knowledge!

    Here is another integration that is available in wicketstuff repo for several months:
    https://github.com/wicketstuff/core/tree/core-1.5.x/jdk-1.5-parent/autocomplete-tagit-parent

    The implementation is really close to what you did.

    ReplyDelete
  2. Thanks for the article. Can you give some insight into why you choose not to use wiQuery; I'm currently evaluating the different options for Wicket + jQuery integration and some pros and cons of the different approaches would be very useful.

    ReplyDelete
    Replies
    1. I am a born minimalist :) and as such I really strive to keep things simple. Simplicity is what drove me to Wicket in the first place and in keeping with my principle I tend to avoid Wicket wrappers which seem to offer little rewards for their added complexity especially in regard to bugginess; with a complete framework such as wiQuery for example there seems to be a disproportionate amount of noise compared to its benefits.

      Delete
  3. Hi, thanks for a very helpful write up. I was looking into both WiQuery and JQWicket, but after reading this, with its clarity and simplicity, I reckon I'll go the same way for any of the JQ-UI components I want to use. I felt with those libraries that there was a bit too much behind the scenes things happening. I'd prefer to see it all up front like you've done. It will probably be a better way to learn Wicket and JQuery-UI anyway.

    Thanks

    John MacEnri

    ReplyDelete
  4. Hi. Do you have an example using Wicket 1.5.3 or 1.5.4? You can't override AbstractAjaxBehavior.onRequest() because it is defined as package scope in IBehaviorListener in Wicket 1.5.3 or 1.5.4.

    ReplyDelete
    Replies
    1. Hi JP,

      Interesting that they would have made such a code breaking change without deprecating it first and allowing users to be weaned off of the api. but when I get a chance I'll take a look and get back to you here and post a followup if need be.

      Jeff

      Delete
    2. Hi JP,

      "You can't override AbstractAjaxBehavior.onRequest() because it is defined as package scope in IBehaviorListener in Wicket 1.5.3 or 1.5.4."

      No, it is not as :
      +++++
      package org.apache.wicket.behavior;

      import org.apache.wicket.IRequestListener;
      import org.apache.wicket.RequestListenerInterface;

      public interface IBehaviorListener extends IRequestListener {

      public static final RequestListenerInterface INTERFACE;

      public void onRequest();
      }
      +++++

      As per above, onRequest is public.

      Also note that AbstractAjaxBehavior in both v.1.5.3 and v.1.5.4 doesn't override onRequest which is why the class is abstract.

      Jeff

      Delete
    3. Could you post a modified example that supports 1.5.4 as I cannot get it to compile as it is.

      Josh

      Delete
    4. Hi Josh,

      This example is compatible with Wicket 1.5.4. What compiler errors are you getting?

      Jeff

      Delete
    5. In the following code:

      final PageParameters pageParameters = new PageParameters(requestCycle.getRequest().getParameterMap());
      requestCycle.setRequestTarget(new StringRequestTarget("application/json", "utf-8", json));

      The getParameterMap() and setRequestTarget() methods are unrecognised and the StringRequestTarget class in unrecognised.

      I suspect I'm missing a lib import. I've tried "import org.apache.wicket.*" but it didn't help.

      Josh

      Delete
    6. Aha! I dont have "org.apache.wicket.request.target" in my classpath. The jars imported by maven are: wicket-core, wicket-request & wicket-util. Do I require another for the missing classes?

      Delete
    7. Oops! So sorry - it seems you updated the onRequest() code and I didn't notice! Compile fine with the new code. Sorry about that.

      Thanks for replying anyway.

      Delete
    8. Good going, Josh. Now that you got this working why not check out another article of mine, A Reusable jQuery UI Autocomplete Wicket Component, at http://thewicketevangelist.blogspot.com/2011/12/reusable-jquery-ui-autocomplete-wicket.html which is a more advance implementation but using the same techiques.

      Jeff

      Delete
  5. Hi Jeff,
    I want to save the preferences of the components e.g., the order of the columns, sorting style of the columns in a container. Show/hide columns etc. GWT provides a mechanism to do this. Is there any API/mechanism to do this? Please help.
    Thanks in advance.

    Sanjay Mengani

    ReplyDelete
    Replies
    1. Sanjay,

      With all due respect this has nothing to do with the subject covered in this article. There are numerous resources for you to turn to as I am sure you already know (i.e. Google and Wicket's own user groups).

      Jeff

      Delete