Creating a GWT Wrapper for the JQuery UI Slider

Creating a GWT Wrapper for the JQuery UI Slider

by Bess Siegal on December 23, 2010


Today my wonderful and amazing colleague Bess Siegal dropped by to talk with me about wrapping JQuery controls in GWT.  You might remember Bess from her last article, Creating A Multi-Valued Auto-Complete Field Using GWT SuggestBox And REST.

JavaScript can dynamically zoom photos, develop mobile applications, and create complex animations without flash.  And GWT can’t.  Well… not easily.  

There’s a world of JQuery plugins you can use easily and safely in your GWT project.  All it takes is a little planning and a little practice.  

This article walks you through creating a GWT wrapper for the JQuery UI Slider which has more features, testing, and customizability than any slider available in GWT.

    

Default Slider

    

    

Range Slider

        

    

Snap to increments

    

    

Slider with multiple anchors

    

Importing JQuery libraries

We’ll start by adding a few libraries.  Download the minified files for JQuery and JQuery UI and add them to your WAR.  Then reference them in your HTML file like this:

<link media="all" type="text/css" href="jquery-ui.css" rel="stylesheet"> 
<script src="jquery-1.4.4.min.js" type="text/javascript" charset="utf-8"></script> 
<script src="jquery-ui.min.js" type="text/javascript" charset="utf-8"></script> 

You can also have Google host them for you like this:

<link media="all" type="text/css" 
        href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.6/themes/base/jquery-ui.css" rel="stylesheet"> 
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js" 
        type="text/javascript" charset="utf-8"></script> 
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.6/jquery-ui.min.js" 
        type="text/javascript" charset="utf-8"></script> 

Let’s add some code

Get the
source code

Now that you’ve imported the JQuery libraries, let’s start using them.  The JQuery UI Slider attaches to a DIV element in your page so our GWT Slider wrapper extends Widget.  

Picking the correct base class is an important part of creating a fully featured GWT control.  Our slider is just a DIV.  If your control uses a text field you could extend TextBox or any other GWT widget.

public class Slider extends Widget
    
    /**
     * Create a slider with the specified ID.  The ID is required
     * because the slider needs a specific ID to connect to.
     * @param id - id of the element to create
     * @param options - JSONObject of any possible option, can be null 
     *                  for defaults
     */
    public Slider(String id, JSONObject options)
    {           
        super();
        Element divEle = DOM.createDiv();
        setElement(divEle);
        divEle.setId(id); 1
        
        m_defaultOptions = options;
        if (m_defaultOptions == null) {
            m_defaultOptions = getOptions(0, 100, new int[]{0});2
        }        
    }

A DIV element is created and set as the element in the constructor 1.

Extending widget works well for the Slider, but it isn’t always the best choice.  When we wrapped the JQuery UI Date Picker we used TextBox as our base class.  Choosing the right base class let’s your JQuery wrapper act like a first class GWT widget.

Getting your slider’s options

getOptions2 is a static method to assist in creating the JSONObject
of options.  

    
    /**
     * A convenient way to create an options JSONObject.  Use SliderOption 
     * for keys.
     *
     * @param min - default minimum of the slider
     * @param max - default maximum of the slider
     * @param defaultValues - default points of each anchor
     * @return a JSONObject of Slider options
     */
    public static JSONObject getOptions(int min, int max, int[] defaultValues) 
    {
        JSONObject options = new JSONObject();
        options.put(SliderOption.MIN.toString(), new JSONNumber(min));
        options.put(SliderOption.MAX.toString(), new JSONNumber(max));
        JSONArray vals = intArrayToJSONArray(defaultValues);
        options.put(SliderOption.VALUES.toString(), vals);
        return options;
    }

    private static JSONArray intArrayToJSONArray(int[] values)
    {
        JSONArray vals = new JSONArray(); 
        for (int i = 0, len = values.length; i < len; i++) {
            vals.set(i, new JSONNumber(values[i]));
        }
        return vals;
    }

More options

There are also other constructors for the most common parameter options set during initialization. 

/**
 * Create a slider with the specified ID.  The ID is required
 * because the slider needs a specific ID to connect to.
 * @param id - id of the element
 * @param min - default minimum of the slider
 * @param max - default maximum of the slider
 * @param defaultValue - default point of a single anchor
 */
public Slider(String id, int min, int max, int defaultValue)
{
    this(id, min, max, new int[]{defaultValue});
}
    
/**
 * Create a slider with the specified ID.  The ID is required
 * because the slider needs a specific ID to connect to.
 * @param id - id of the element
 * @param min - default minimum of the slider
 * @param max - default maximum of the slider
 * @param defaultValues - default points of each anchor
 */
public Slider(String id, int min, int max, int[] defaultValues)
{           
    this(id, getOptions(min, max, defaultValues));
}

The SliderOption enumeration helps with additional slider options.  The Javadoc comments for each option is copied directly from JQuery UI Slider documentation.

Initialization

Constructing your GWT object is only the first step.  You also have to bind it to JQuery4 with the JavaScript Native Interface.  The best place to do this is the onLoad method.

We need this method because GWT causes a timing problem.  With a regular JQuery page you would just call the binding when your page loads.  GWT uses a delayed loading mechanism which loads the correct GWT code for your browser and language.  The onLoad3 method gives us a good place to bind to JQuery after our GWT object has been loaded into the page.

    @Override
    protected void onLoad()3
    {
        if (m_defaultOptions == null) {
            m_defaultOptions = new JSONObject();
        }
        
        createSliderJS(this, getElement().getId(), m_defaultOptions.getJavaScriptObject());
        super.onLoad();
    }
    
    private native void createSliderJS(Slider x, String id, JavaScriptObject options) /*-{
        options.start = function(event, ui) {
            x.@com.example.slider.client.widget.slider.Slider::fireOnStartEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/core/client/JsArrayInteger;)(event, ui.values);
        };
        options.slide = function(event, ui) {
            return x.@com.example.slider.client.widget.slider.Slider::fireOnSlideEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/core/client/JsArrayInteger;)(event, ui.values);
        };
        options.change = function(event, ui) {
            var has = event.originalEvent ? true : false;
            x.@com.example.slider.client.widget.slider.Slider::fireOnChangeEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/core/client/JsArrayInteger;Z)(event, ui.values, has);                
        };
        options.stop = function(event, ui) {
            x.@com.example.slider.client.widget.slider.Slider::fireOnStopEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/core/client/JsArrayInteger;)(event, ui.values);
        };5
        
        $wnd.$("#" + id).slider(options);4
    }-*/;
    

In addition to the options set in the constructor, the createSliderJS method also maps the slider’s events to corresponding fireOnXEvent Java methods using the GWT technique for passing JavaScript values into Java code.  The values we pass are the native event and the values selected by the slider. 5.  Thus we have our cornerstone for event handling.

Event handling

When we’re done with our Slider it will look just like a standard GWT widget from the outside.  That way we can switch implementations and never worry about our choice of JavaScript library leaking out to other code in our project.  We’ll continue that strong encapsulation by creating a SliderEvent object and SliderListener interface to allow other code to get events from the Slider.

    private void fireOnStartEvent(Event evt, JsArrayInteger values)
    {
        int[] vals = jsArrayIntegerToIntArray(values);
        SliderEvent e = new SliderEvent(evt, this, vals);
        
        for (SliderListener l : m_listeners) {
            l.onStart(e);
        }
    }
    
    private boolean fireOnSlideEvent(Event evt, JsArrayInteger values)
    {
        int[] vals = jsArrayIntegerToIntArray(values);
        SliderEvent e = new SliderEvent(evt, this, vals);
        
        for (SliderListener l : m_listeners) {
            l.onStart(e);
        }
        
        boolean ret = true;
        
        for (SliderListener l : m_listeners) {
            if (!l.onSlide(e)) {
                //if any of the listeners returns false, return false,
                //but let them all do their thing
                ret = false;
            }
        }
        
        return ret;6
    }
    
    private void fireOnChangeEvent(Event evt, JsArrayInteger values, 
                                   boolean hasOriginalEvent)
    {
        int[] vals = jsArrayIntegerToIntArray(values);        
        SliderEvent e = new SliderEvent(evt, this, vals, hasOriginalEvent);
        
        for (SliderListener l : m_listeners) {
            l.onChange(e);
        }
    }
    
    private void fireOnStopEvent(Event evt, JsArrayInteger values)
    {
        int[] vals = jsArrayIntegerToIntArray(values);
        SliderEvent e = new SliderEvent(evt, this, vals);
        
        for (SliderListener l : m_listeners) {
            l.onStop(e);
        }
    }

Each fire event method notifies all listeners and passes the native event and an int[] of values. The onSlide event allows for cancellation of the action by returning false6.  

Changing options after initialization

You may change a slider’s options after initialization.  There are get/setIntOption, get/setBooleanOption and get/setStringOption methods to maintain type-safety.  Each of these methods calls a corresponding JSNI method to get or set the option.  Getting and setting the slider’s values is a little different, so they have their own methods.

    /**
     * Convenience method for only 1 anchor
     * @return Returns the value.
     */
    public int getValue()
    {
        return getValueAtIndex(0);
    }
    
    /**
     * Sets the value of each anchor
     * @param values - int array of values
     */
    public void setValues(int[] values)
    {
        JSONArray vals = intArrayToJSONArray(values);
        setValuesJS(getElement().getId(), vals.getJavaScriptObject());
    } 
        
    /**
     * Gets the value of a anchor at the specified index
     * 
     * @param index  the index to retrieve the value for
     * 
     * @return the value
     */
    public int getValueAtIndex(int index)
    {
        return getValueJS(getElement().getId(), index);
    }
    
    
    private native void setValuesJS(String id, JavaScriptObject values) /*-{
        $wnd.$("#" + id).slider("option", "values", values);
    }-*/;
    
    
    private native int getValueJS(String id, int index) /*-{
        return $wnd.$("#" + id).slider("values", index);
    }-*/;

Extending the Slider

Because a slider that uses the range option is unique in that the values are limited to 2, a RangeSlider subclass was created to only allow get and set of 2 values.  You could extend the slider for any of the specific options required by your application.

Cleanup

The widget will cleanup after itself by calling destroy on the slider during onUnload, which is called immediately before a widget will be detached from the browser’s document.

    @Override
    protected void onUnload()
    {
        destroySliderJS(this, getElement().getId());
        super.onUnload();        
    }
    
    private native void destroySliderJS(Slider x, String id) /*-{
        $wnd.$("#" + id).slider("destroy");
    }-*/;

The Takeaway

For our web application projects at Novell, we like to use GWT for maintainability.  For all GWT’s benefits, we understand that it’s standard widgets are limited, especially when compared to its corresponding JQuery widget.  Fortunately, we’ve developed a repeatable method for wrapping JQuery UI controls, and we hope you can employ it with this slider as an example.  You may also feel free to use the slider code in your application.  Either way, please let us know if you do.

The code in this example is free and released under the Apache 2.0 license. The other programs needed to run this example are also free, but some of them may use different licenses. Make sure to read and understand each license before using a tool.

Bess Siegal is a software engineer at Novell.  She enjoys her one-minute commute so she can spend more time with her husband and 3 daughters.