Sunday, December 4, 2011

Marrying Wicket And jQuery Ajax

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

Note: the code demonstrated in this article uses the jquery and jquery.form Javascript libraries for client-side scripting and the Google gson library for servers-side JSON support.

One of the pleasures of working with Wicket is how easy it makes doing Ajax based DOM replacement. Wicket Ajax wires up an event listener and ties it to a DOM event such as on click, keyup, etc., and upon receipt of the Ajax request hands off an AjaxTarget to an event handler. In the event handler you have access to any form input values with which you can update a database record and update the page by adding components to the AjaxTarget.

Sometimes, though, a Web page requires some Ajax processing that doesn't neatly fit into the mold which Wicket provides out of the box. For instance, one friction point with Wicket is integration with jQuery's Ajax implementation. jQuery is the preeminent Javascript framework and for good reason: it makes doing the hard stuff easy including initiating Ajax requests based on the user actions on the page.

What then, does it take to marry (so to speak) the strengths of both jQuery and Wicket and benefit from their combined use as far as Ajax is concerned? Well, as I will show you, it isn't hard at all and in fact it is quite easy! Ok, so lets get started.

Note: For the two examples I am presenting here I am using the latest version of jQuery as well as the jQuery Form plugin. For example 2 I am also using the Google gson library which provides excellent JSON functionality and which is very easy to use.

The first example demonstrates how to use jQuery and the jQuery Form plugin to submit a form via Ajax and how to wire up Wicket to process the request as well as send back a response. Here, the user enters their first and last name and clicks the form's submit button which causes the form to be submitted via Ajax and a response sent back to the client from Wicket which mirrors their input. The key point to take away from this example is the use of Wicket's AbstractAjaxBehavior to create an event listener as well as an event handler which packages a response back to the client.

The second example demonstrates how to use jQuery and the jQuery Form plugin to implement a dynamic lookup list which consists of an input field in which the user can type and a dynamically created and displayed list of states that match the user's input. As with the first example, the key point to take away from this example is the use of Wicket's AbstractAjaxBehavior.

In both examples, client side Javascript using jQuery is responsible for the heavy lifting meaning it is the client code that wires up the events that initiate the Ajax responses and not Wicket. This is one clear difference in implementations between the way Wicket does Ajax and the way Ajax is done using client side code.

The following Java and Markup/Javascript code is for both examples, 1 and 2

 - Java Wicket Code -

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


import com.google.gson.Gson;
import com.sun.tools.internal.jxc.apt.Const;
import java.lang.String;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.markup.ComponentTag;
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 Form form1;
    private final TextField firstName;
    private final TextField lastName;
    private final AbstractAjaxBehavior aab1;
    private final Form form2;
    private final TextField state;
    private final AbstractAjaxBehavior aab2;


    public HomePage() {
        /* Example 1
         * Ajax Behavior provides an event
         * listener as well as the request
         * handler.
         */
        add(aab1 = new AbstractAjaxBehavior() {


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


                RequestCycle requestCycle = getComponent().getRequestCycle();
                Request request = requestCycle.getRequest();
                IRequestParameters irp = request.getPostParameters();
                StringBuilder sb = new StringBuilder();
                sb.append("firstName: " + irp.getParameterValue("firstName"));
                sb.append(", ");
                sb.append("lastName: " + irp.getParameterValue("lastName"));
                requestCycle.scheduleRequestHandlerAfterCurrent(new TextRequestHandler(sb.toString()));
            }
        });




        /*
         * A form whose action tag will be set to the 
         * above behavior's call back url.
         */
        add(form1 = new Form("form1") {


            // set the form's action tag to the behavior's call back url
            @Override
            protected void onComponentTag(ComponentTag tag) {
                super.onComponentTag(tag);
                tag.put("action", aab1.getCallbackUrl());
            }
        });


        /*
         * Form inputs for first and last name.
         */
        form1.add(firstName = new TextField("firstName"));


        form1.add(lastName = new TextField("lastName"));


        /* Example 2
         * Ajax Behavior provides an event
         * listener as well as the request
         * handler.
         */
        add(aab2 = new AbstractAjaxBehavior() {


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


                RequestCycle requestCycle = getComponent().getRequestCycle();
                Request request = requestCycle.getRequest();
                IRequestParameters irp = request.getPostParameters();
                StringValue state = irp.getParameterValue("state");
                List statesLike = MockDb.getStatesLike(state.toString());
                requestCycle.scheduleRequestHandlerAfterCurrent(new TextRequestHandler(convertListToJson(statesLike)));
            }
        });


        add(form2 = new Form("form2") {


            @Override
            protected void onComponentTag(ComponentTag tag) {
                super.onComponentTag(tag);
                tag.put("action", aab2.getCallbackUrl());
            }
        });


        form2.add(state = new TextField("state"));
    }


    /*
     * 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 getStatesLike(String target) {


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


- Markup And jQuery code -

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
<meta charset="UTF-8">
<title>Wicket Example</title>
<script type="text/javascript" src="javascript/jquery.js"></script>
<script type="text/javascript" src="javascript/jquery.form.js"></script>
<script type="text/javascript" >
$(document).ready(function(){
/*
* jQuery Ajax form submission and Wicket response
*/
$('#clickme').click(function(){
// get the form input values
// var fName = $('input.jqueryid_firstName').val();
// var lName = $('input.jqueryid_lastName').val();
// ajax callback to the server using jQuery
$('form.jqueryid_form1').ajaxSubmit({cache: false, success: function(html){$('#responses').append(html + "<br/>");}});
});

/*
* jQuery Ajax lookup dropdown list
*/
$('input.jqueryid_state').keyup(function(){

var showStatesJson = function(json){
var listDomElement = ""
$.each(json, function(index, value){
if(listDomElement == ""){
listDomElement = "<select size=\"5\">";
}
listDomElement += "<option>" + value + "</option>"
});
if(listDomElement != ""){
listDomElement += "</select>";
}
$('#statelist').html(listDomElement);
};

// get the form input values
// var target = $(this).val();

var options = {
dataType: "json",
cache: false,
success: showStatesJson
};


if($(this).val() != ""){
$('form.jqueryid_form2').ajaxSubmit(options);
}else{
$('#statelist').html("");
}
});

});
</script>
</head>
<body>
<h1>Marrying Wicket And jQuery Ajax</h1>
<h2>jQuery Ajax form submission and Wicket response</h2>
<form class="jqueryid_form1" wicket:id="form1">
First Name: <input class="jqueryid_firstName" type="text" wicket:id="firstName"/><br/>
Last Name: <input class="jqueryid_lastName" type="text" wicket:id="lastName" /><br/>
</form>
<button id="clickme">Click Me, Please</button>
<div id="responses"/>

<h2>jQuery Ajax lookup dropdown list</h2>
<form class="jqueryid_form2" wicket:id="form2">
US State Lookup: <input class="jqueryid_state" type="text" wicket:id="state"/><br/>
<div id="statelist"/>
</form>
</body>
</html>


Example 1 - Submit a form via Ajax

As the above Wicket code for example 1 shows, what we are doing is adding an AbstractAjaxBehavior to the page and using its callback URL (it points to an event listener which delegates the response to the onRequest method) as the value for the form's action tag which we set by overriding the form's onComponentTag method. The jQuery Form plugin will use the value of the action tag as the URL to call when making the Ajax call.

The really interesting code is inside the AbstractAjaxBehavior's onRequest method which is responsible for handling the request as well as the response back to the client. If you have done similar in Wicket versions prior to v1.5 then you will notice that a lot has changed with v1.5 and is in fact much neater.

In onRequest we obtain a RequestCycle from the page from which we get the actual Request. From the request we are able to easily obtain the params that were posted from the form which are firstName and lastName and with which we create a string and return that string to the client by using a TextRequestHandler.

Basically, all onRequest processing will follow a similar model regardless of its use case - get a request, get the params, do something with the params and send back a response.

The response we are sending back is text and that is why we are using a TextRequestHandler which takes a string parameter representing the text to send back to the client in its constructor.

The code on the client side for example 1 couldn't be easier. Using common jQuery practices we create a call back function to handle the click event on the form. In the handler for the click event we submit the form via Ajax. The Form plugin has numerous options but here you can see that it requires minimal configuration. I am merely requesting that the response not be cached (IE is notorious in this regard) and passing a callback method to handle the success response from the server which just appends it to the DOM in a div whose id is response.

As example 1 above demonstrated, the key to marrying jQuery and Wicket is having jQuery call the correct URL on the server which points to a component based event listener and which delegates the event to AbstractAjaxBehavior's onRequest method. This is the glue that binds the 2 sides of the coin so to speak. As we will now see, it is this same glue that allows us to implement something more intricate than simply echoing back the values of a form submit.

Example 2 - Dynamic Lookup List via Ajax

Web 2.0 has brought numerous improvements to the user's experience and Ajax is to thank for that obviously. One of the nicer features is the ways we can use partial input to provide results to the user as in the case of auto complete elements. Let's expand on example 1 above and show how by marrying jQuery and Wicket this is quite easy to implement.

The use case here is simple: Provide a text box in which the user can type the characters for a US state and provide the user with a list of states that match their input from which they can select one.

Just as we did for example 1 we are again creating an AbstractAjaxBehavior and overriding its onRequest method. Notice how the code is similar to that in example 1. The only difference is what we do with the params we receive and how we use it to create the response back to the client which in this cases requires JSON. The client is wired to submit the form on every keyup event which will post the content of the text field back to the server. In Wicket we get that value and use it to look up all the states that start with that value which are returned to the client as JSON.

The client creates an event handler to handle the keyup event on the text field which it uses to obtain the text field's value and issue an Ajax request to the event listener pointed to by the form's action attribute. It processes the returned JSON object by creating a select element along with options which contain the states that match the user's input and adds the select element to the DOM.

Screen Images Of Examples


Screen Image Of Example 1 and 2 - User hasn't entered anything yet
Screen Image Of Example 1 and 2 - User has entered data in both



Summary

As these examples have demonstrated, marrying jQuery and Wicket to work together isn't rocket science and in fact is quite easy to do. The benefit of this marriage is that you can now easily leverage the strength of each, jQuery and Wicket, to their best advantages; you can now comfortably implement that which is easier to do in either jQuery or Wicket, taking advantage of each of their strengths. The option of having the ability to take advantage of the strengths of each layer in the development stack is an empowering asset.



5 comments:

  1. Interesting.

    That implies that you submit your form for a single Ajax purpose though, and the "action" value of your form is stuck to the Wicket Ajax callback.

    This seems to prevent the user from actually submitting the form itself (or (s)he would receive the Ajax response).

    Did I miss something?

    ReplyDelete
    Replies
    1. @Unknown,

      All Ajax calls, regardless of the Web framework, require a url that points to a request handler on the server.

      In both of the above examples both forms are submitted but both of their action urls are not the default form action urls that Wicket generated for the forms; they are replaced by the call back urls from the AbstractAjaxBehaviors which handles the form submission.

      So in both example both forms are submitted but submitted using ajax and are handled by the AbstractAjaxBehaviors onRequest methods.

      Jeff

      Delete
  2. Jeff, I understood that, but my concern is: how do you get the standard "form behaviour" if you want to actually submit the content of the form?

    I implemented the first example, which works fine, then added a submit button (standard or wicketized, it doesn't really matter). When I click on it, I see the TextHandler response, but the onSubmit() method of my button is never called.

    I'm therefore wondering : how can you get both aspects (the ajax submission through jquery and the normal form handling)?

    ReplyDelete
    Replies
    1. Ok, now I understand and thank you for taking the time clarifying the issue.

      Yes, this is easily done - having one url for the ajax call back and one url for the form's action but note that I purposely used ajax form submission for this article due to its inherent advantages from the user's perspective as far as the UI is concerned.

      So how can you achieve what you are seeking? There are a few ways, actually. One way is to provide an override in the AbstractAjaxBehavior for its renderHead method. In that method render javascript to the browser that would amount to: String ajaxCallbackUrl = "var ajaxCallbackUrl = " + abstactAjaxBehavior.getCallbackUrl() + ";";

      On the client, you wouldn't use jQuery's form submit but rather would just use jQuery's normal ajax method to call the server using the ajaxCallback variable's value as the url. For an example of this technique, please see my related article 'A Reusable jQuery UI Autocomplete Wicket Component' at http://thewicketevangelist.blogspot.com/2011/12/reusable-jquery-ui-autocomplete-wicket.html. It is a little more involved than the examples I presented here but it should serve to provide you with a concrete example.

      Thank you for taking the time to respond to my article and I hope you will continue to enjoy the other articles I have written and will write.

      Jeff

      Delete
    2. I'm in the process of catching up with all your other Wicket articles. (pretty interesting to read , especially since I'm not very fond of Wiquery myself).

      I did it in a slightly different way:
      Server side:
      protected void onComponentTag(ComponentTag tag) {
      super.onComponentTag(tag);
      tag.put("callback", aab.getCallbackUrl());
      }

      Client side:
      (in the "clickme" anonymous function)
      var callback = $('form.jqueryid_form1').attr("callback")

      Then used the Jquery ajax submission (of course, I had to collect the field values "manually")

      Delete