I spotted the new SuggestBox which has been added in the latest RC. It
looks great, but I am not sure if it does what I want.
I have a list of several million items (Strings) and I want to create
an autocompleter based on the first few letters that somebody types in
an input box. Obviously the size of the data would make it unfeasible
to simply send everything to the client (as SuggestBox currently
works), so I'm looking for a GWT widget that can send the contents of
the box to the server, get a response and use that for its
completions.
If MultiWordSuggestOracle supported clear() I could probably come up
with something to do this, but as it can't, I am unable to solve this
problem.
Any ideas?
Thanks and kind regards,
Sam
I (almost) solved this problem by creating an alternative
implementation of SuggestOracle which uses RPC to get the suggestions.
Now I have the servlet offering suggestions :-)
I should have offered the code in the last post, which I release under
the Apache 2.0 open source license. For future reference, what is the
best way to share code on this group? Hope this is of use to somebody!
/**
* Aug 18, 2007
*
* @author Samuel Halliday, ThinkTank Mathematics Limited
* @copyright ThinkTank Mathematics Limited 2007
*/
package thinktank.gwt.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.SuggestOracle;
/**
* An implementation of {@link SuggestOracle} which uses RPC to get
suggestions from a
* servlet. The servlet name is taken in the constructor, and the
servlet must implement
* the {@link ServicedSuggestOracleService} interface.
*
* @author Samuel Halliday, ThinkTank Mathematics Limited
*/
public class ServicedSuggestOracle extends SuggestOracle {
public interface ServicedSuggestOracleService extends RemoteService {
/**
* @param partialText
* the text to complete
* @param limit
* the maximum number of suggestions to return
* @return the list of String suggestions
* @gwt.typeArgs <java.lang.String>
*/
public List getSuggestions(String partialText, int limit);
}
public interface ServicedSuggestOracleServiceAsync {
public void getSuggestions(String partialText, int limit,
AsyncCallback callback);
}
public static class StringSuggestion implements Suggestion {
final private String string;
public StringSuggestion(String string) {
this.string = string;
}
public String getDisplayString() {
return string;
}
public Object getValue() {
return string;
}
}
private final ServicedSuggestOracleServiceAsync service;
/**
* @param servletName
*/
public ServicedSuggestOracle(String servletName) {
// ?? wrapper for this boilerplate
service = (ServicedSuggestOracleServiceAsync) GWT
.create(ServicedSuggestOracleService.class);
String moduleRelativeURL = GWT.getModuleBaseURL() + servletName;
((ServiceDefTarget)
service).setServiceEntryPoint(moduleRelativeURL);
}
public void requestSuggestions(final Request request,
final Callback callback) {
String query = request.getQuery();
final int limit = request.getLimit();
AsyncCallback rpcCallback = new AsyncCallback() {
final Collection collection = new ArrayList();
final Response response = new Response();
public void onFailure(Throwable caught) {
Suggestion suggest = new StringSuggestion("ERROR: "
+ caught.getMessage());
collection.add(suggest);
response.setSuggestions(collection);
callback.onSuggestionsReady(request, response);
}
public void onSuccess(Object result) {
List results = (List) result;
Iterator iterator = results.iterator();
String word = (String) iterator.next();
int i = 0;
for (; iterator.hasNext() && (i < limit); word = (String) iterator
.next()) {
Suggestion suggest = new StringSuggestion(word);
collection.add(suggest);
i++;
}
response.setSuggestions(collection);
callback.onSuggestionsReady(request, response);
}
};
service.getSuggestions(query, limit, rpcCallback);
}
}
Before I get to the code, I want to point out something that seems to
be misunderstood: For a server-side updating SuggestBox, you do not
extend or use the "MultiWordSuggestionOracle" at all. That's defined
for the sole purpose for providing suggestions from a client-side
populated list. Sure, you could create a service to load ALL your data
and then populate the MWSO with the results from your service call,
but if you have more than a few hundred possible suggestions, then
that method really is not viable. You wouldn't want to extend
MultiWordSuggestionOracle to do RPC calls even if you could -- because
the suggestions have to be made by your server-side code. It can be
single word, multi-word, reverse word, or "any old word" -- however
you wish to implement it on your server-side, because that's where you
have to find and make the matches to suggest to the user. Bottom-line:
Extending SuggestOracle is the only way to add RPC (not to mention
that you can't extend MWSO anyways since it's marked as "final").
**** Somewhere in your client-side code, create a SuggestBox like this
*****
ItemSuggestOracle oracle = new ItemSuggestOracle();
suggestBox = new SuggestBox(oracle);
suggestBox.setLimit(5); // Set the limit to 5 suggestions
being shown at a time
**** ItemSuggestOracle.java - An extended SuggestOracle that makes the
RPC calls for getting suggestions ****
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.SuggestOracle;
public class ItemSuggestOracle extends SuggestOracle {
public boolean isDisplayStringHTML() { return true; }
public void requestSuggestions(SuggestOracle.Request req,
SuggestOracle.Callback callback) {
ItemSuggestService.Util.getInstance().getSuggestions(req, new
ItemSuggestCallback(req, callback));
}
class ItemSuggestCallback implements AsyncCallback {
private SuggestOracle.Request req;
private SuggestOracle.Callback callback;
public ItemSuggestCallback(SuggestOracle.Request _req,
SuggestOracle.Callback _callback) {
req = _req;
callback = _callback;
}
public void onFailure(Throwable error) {
callback.onSuggestionsReady(req, new
SuggestOracle.Response());
}
public void onSuccess(Object retValue) {
callback.onSuggestionsReady(req,
(SuggestOracle.Response)retValue);
}
}
}
**** ItemSuggestServiceAsync.java - Defining the async service
(standard stuff, no magic here) ****
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface ItemSuggestServiceAsync {
public void getSuggestions(SuggestOracle.Request req,
AsyncCallback callback);
}
**** ItemSuggestService.java - Defining the client side service API
for the RPC (just standard declarations, no magic here either) ****
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.SuggestOracle;
public interface ItemSuggestService extends RemoteService {
public static final String SERVICE_URI = "/services/
itemsuggestservice";
public static class Util {
public static ItemSuggestServiceAsync getInstance() {
ItemSuggestServiceAsync instance =
(ItemSuggestServiceAsync) GWT.create(ItemSuggestService.class);
ServiceDefTarget target = (ServiceDefTarget) instance;
target.setServiceEntryPoint(GWT.getModuleBaseURL() +
SERVICE_URI);
return instance;
}
}
public SuggestOracle.Response getSuggestions(SuggestOracle.Request
req);
}
**** ItemSuggestion.java - Defines the data structure for the
suggestion data that will be sent back ****
import com.google.gwt.user.client.rpc.IsSerializable;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
public class ItemSuggestion implements IsSerializable, Suggestion {
private String display;
private String replacement;
// Required for IsSerializable to work
public ItemSuggestion() {}
// Convenience method for creation of a suggestion
public ItemSuggestion(String _disp, String _rep) {
display = _disp;
replacement = _rep;
}
public String getDisplayString() {
return display;
}
public String getReplacementString() {
return replacement;
}
}
And then, the actual server-side:
**** ItemSuggestServiceImpl.java ****
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
public class ItemSuggestServiceImpl extends RemoteServiceServlet
implements ItemSuggestService {
public SuggestOracle.Response getSuggestions(SuggestOracle.Request
req) {
SuggestOracle.Response resp = new SuggestOracle.Response();
// Create a list to hold our suggestions (pre-set the length
to the limit specified by the request)
List<Suggestion> suggestions = new
ArrayList<Suggestion>(req.getLimit());
// Replace the code below with something to create and popular
ItemSuggestion objects
// This is where all your magic should happen to find matches
and create the suggestion list, usually from a database
// The dummy code below creates bogus suggestions "Suggestion
1", "Suggestion 2", etc..
for(int i=1; i<11; i++) {
suggestions.add(new ItemSuggestion("Suggestion " + i,
"Suggestion " + i));
}
// Now set the suggestions in the response
resp.setSuggestions(suggestions);
// Send the response back to the client
return resp;
}
}
On Aug 18, 7:38 pm, Sam <Sam.Halli...@gmail.com> wrote:
> Yay! It all works... the only issue I have now is that the user cannot
> use the cursor keys to choose the preferred match... but at least theRPCis working.
>
> I should have offered the code in the last post, which I release under
> the Apache 2.0 open source license. For future reference, what is the
> best way to share code on this group? Hope this is of use to somebody!
>
> /**
> * Aug 18, 2007
> *
> * @author Samuel Halliday, ThinkTank Mathematics Limited
> * @copyright ThinkTank Mathematics Limited 2007
> */
> package thinktank.gwt.client;
>
> import java.util.ArrayList;
> import java.util.Collection;
> import java.util.Iterator;
> import java.util.List;
>
> import com.google.gwt.core.client.GWT;
> import com.google.gwt.user.client.rpc.AsyncCallback;
> import com.google.gwt.user.client.rpc.RemoteService;
> import com.google.gwt.user.client.rpc.ServiceDefTarget;
> import com.google.gwt.user.client.ui.SuggestOracle;
>
> /**
> * An implementation of {@link SuggestOracle} which usesRPCto get
You can control what is displayed in the list of choices by setting
the "display string". You can do this on the server-side oracle, where
it actually figures out what should be suggested, by constructing an
HTML display string that would produce the desired effect.. Something
like this might work (but I haven't tried it):
<span style="display: block; position: absolute; text-align:left">[KEY
GOES HERE]</span><span style="display: block; width: 100%; position:
relative; text-align:right">[VALUE HERE]</span>
Use whatever HTML code you need to format the list of suggestions
properly. You'll just need to either generate the HTML on the server
side. Alternatively, you could put more logic into your implementation
of the Suggestion class's getDisplayString method. I prefer to do it
on the server since it will be much faster than interpreted JavaScript
on the client side. Your suggestion can contain as much data, or as
little, as you want. What gets displayed to the user is determined
soley by your implementation of the getDisplayString method.
Once the user has made a selection, the TextBox portion of the widget
(where the user has been typing to get suggestions) will be set with
whatever your Suggestion class's getReplacementString returns for a
particular suggestion. I don't think the replacement string can
contain any HTML, though, so you'll need to decide what to show.
Perhaps just "J1 John" would do.
As for matching what the user types to either the key or value
portion, this is something that must happen in your server-side code.
The server side gets sent a SuggestOracle.Request class, which
contains the "query" from the user. This is whatever the user has
typed thus far in the TextBox portion of the widget. Use that to match
data in your database. Perhaps something like this:
Connection conn = ds.getConnection(); // ds being a DataSource
object
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("SELECT * FROM MyTable WHERE
(key LIKE '%" + request.getQuery() + "%') OR (value LIKE '%" +
request.getQuery() + "%')");
[do some processing of matches]
The "request" object in the code above is an instance of the
SuggestOracle.Request that was sent to the server from the client to
request suggestions. (See my code from the previous post for the class
"ItemSuggestServiceImpl". In that code, I named it "req", but it's the
same thing)
On a side note, which might be helpful to you, I also keep an id
number stored with my Suggestion implementation that references the
identifier for the record in the database, which makes it easier to do
something with the suggestion once the user has selected it. The id
number is never displayed, but I added a method "getProductID" so that
the client side code could get the id for easier processing. For
example, in my "onSuggestionSelected" method, I get the selected
suggestion from the SuggestionEvent and grab, cast it to my
implementation of a Suggestion (called ItemSuggestion) and then use it
to kick off a "addItemToCart" RPC. See below:
public void onSuggestionSelected(SuggestionEvent event) {
// Item selected for addition to cart
ItemSuggestion suggestion = (ItemSuggestion)
event.getSelectedSuggestion();
// Kick-off an RPC to add the item to the shopping cart
CartService.Util.getInstance().addItemToCart(suggestion.getProductID(),
someCallback);
suggestBox.setText("Adding...");
}
Thanks for your help - I was able to get the dual elements shown in
SuggestBox pop-up.
Thanks again.
Joster