GWT SuggestBoxReloaded

This article will present how to implement a GWT 1.6 (RC2) SuggestBox with backing hidden fields, RPC calls and not query the server on every character event.
While integrating GWT on work, I needed to implement a SuggestBox with a backend RPC service but I had some requirements that the default SuggestBox didn't fulfill. Namely, I needed that the suggestion sets hidden fields (ex. displaying a human readable name for the user and setting a hidden id field). The solution presented in GWT SuggestBox backed by DTO Model and the Using the GWT SuggestBox with RPC were very helpful in creating this example project. I advise you to read/implement them before reading further in order to understand the issues.
The first article hit the sweet point on how to pass hidden values using the DTO pattern, you can either wrap your hidden fields (Hidden.wrap) or as in this example we'll instantiate them using GWT and add them to the panel. The next issues were following, 1. You need to be able to clear the value either by a button or by deleting the text in the TextBox, 2. The Lombardi solution presented an issue as it sent out every query string, 3. Enable some sort of reusability of the generated javascript client.
First of all I created three basic Suggestion classes, IdEnabledSuggestion, TextEnabledSuggestion and SimpleSuggestion. These three probably cover 90% of the cases (actually, the TextEnabledSuggestion can replace IdEnabledSuggestion just as well), otherwise you can add a DTO with the data you need, just remember that you must put the DTO class in the /client package, in my first trials I had it outside which led to problems.
 
public class IdEnabledSuggestion implements IsSerializable, Suggestion {

private String displayString, replacementString;
private Long id;

public IdEnabledSuggestion(){

}

public IdEnabledSuggestion(String displayString, String replacementString, Long id){
this.displayString = displayString;
this.replacementString = replacementString;
this.id = id;
}

public String getDisplayString() {
return displayString;
}

public String getReplacementString() {
return replacementString;
}

public Long getId() {
return id;
}
}

Then you extend the RemoteService interface and also the Async version. In order to achieve reusability, I've added an enum that acts as a switch, that way I can group all suggestion methods under the same servlet. A even more flexible solution would be to add a text parameter to the javascript client that would switch the endpoint ending on separate url's, probably using ServiceDefTarget.setServiceEntryPoint(String), but as I don't really like declaring a servlet for each endpoint I would recommend that solution with some kind of rest/mvc framework, I would love to see someone extending this example with struts2 and spring mvc endpoints :)
 
@RemoteServiceRelativePath("oracle")
public interface OracleService extends RemoteService {

public SuggestOracle.Response getSuggestions(SuggestOracle.Request search, Oracles oracle);

public static class Util {
public static OracleServiceAsync getInstance() {
OracleServiceAsync instance = (OracleServiceAsync) GWT.create(OracleService.class);
return instance;
}
}
}

In order to solve the second issue, and avoid sending queries on every key event, I have extracted a SuggestOracle.Request and SuggestOracle.Callback on a instance level. When a user inserts a query, I temporarily set the variables and schedule the timer, this way, even if new events come, they overwrite the instance variables effectively caching them for later so when the time passes the service gets invoked once and only once with the last query.
It's a simple trick that produces the expected actions and I hope I'm not missing some threading, timer or leakage issue?
I've added also setters so that you can set your delay and minimum number of characters needed to send the query.
I then wrap the Hidden, the SuggestBox and a widget that implements HasClickHandlers in a object that adds the various handlers to manage form state and you are good to go. :)
 
public class HiddenSuggestBox {

private SuggestBox suggestBox;
private Hidden hidden;
private HasClickHandlers clearWidget;

public HiddenSuggestBox(SuggestBox suggestBox, Hidden hidden, HasClickHandlers clearWidget) {
super();
this.suggestBox = suggestBox;
this.hidden = hidden;
this.clearWidget = clearWidget;
this.suggestBox.addSelectionHandler(new MySelectionHandler());
this.suggestBox.getTextBox().addValueChangeHandler(new ClearTextBox());
this.clearWidget.addClickHandler(new ClearValueHandler());
}

private class MySelectionHandler implements SelectionHandler<suggestion> {
public void onSelection(SelectionEvent<suggestion> event) {
if(event.getSelectedItem() instanceof TextEnabledSuggestion){
TextEnabledSuggestion suggestion = (TextEnabledSuggestion)event.getSelectedItem();
hidden.setValue(suggestion.getText());
} else if(event.getSelectedItem() instanceof IdEnabledSuggestion){
IdEnabledSuggestion suggestion = (IdEnabledSuggestion)event.getSelectedItem();
hidden.setValue(suggestion.getId()+"");
} else {
hidden.setValue(event.getSelectedItem().getReplacementString());
}

}
}

private class ClearTextBox implements ValueChangeHandler<string> {
public void onValueChange(ValueChangeEvent<string> event) {
if(event.getValue().trim().equals("")){
suggestBox.getTextBox().setValue("");
hidden.setValue("");
}
}
}
private class ClearValueHandler implements ClickHandler {
public void onClick(ClickEvent event) {
hidden.setValue("");
suggestBox.setValue("");
}
}
}

eclipse project archive
You might need to set JVM in launcher and ant script.

Comments

Popular posts from this blog

Relaxing SSL validation for JaxWS

Search and Replace in ODT using AODL

Job Hunting in the Time of COVID-19