Creating a Facebook-style Autocomplete with GWT
Have you used the "To:" widget on on Facebook or LinkedIn when composing a message? It's an autocompleter that looks up contact names and displays them as you type. It looks like a normal textbox (a.k.a. <input type="text">), but wraps the contact name to allow you to easily delete it. Here's a screenshot of what Facebook's widget looks like.
Last week, I was asked to create a similar widget with GWT. After searching the web and not finding much, I decided to try writing my own. The best example I found on how to create this widget was from James Smith's Tokenizing Autocomplete jQuery Plugin. I used its demo to help me learn how the DOM changed after you selected a contact.
GWT's SelectBox allows you to easily create an autocompleter. However, it doesn't have support for multiple values (for example, a comma-delimited list). The good news is it's not difficult to add this functionality using Viktor Zaprudnev's HowTo. Another feature you might want in a SelectBox is to populate it with POJOs. GWT SuggestBox backed by DTO Model is a good blog post that shows how to do this.
Back to the Facebook Autocompleter. To demonstrate how to create this widget in GWT, I put together a simple application. You can view the demo or download it. The meat of this example is in an InputListWidget. After looking at the jQuery example, I learned the widget was a <div> with a unordered list (<ul>). It starts out looking like this:
<ul class="token-input-list-facebook">
<li class="token-input-input-token-facebook">
<input type="text" style="outline-color: -moz-use-text-color; outline-style: none; outline-width: medium;"/>
</li>
</ul>
I did this in GWT using custom BulletList and ListItem widgets (contained in the download).
final BulletList list = new BulletList();
list.setStyleName("token-input-list-facebook");
final ListItem item = new ListItem();
item.setStyleName("token-input-input-token-facebook");
final TextBox itemBox = new TextBox();
itemBox.getElement().setAttribute("style",
"outline-color: -moz-use-text-color; outline-style: none; outline-width: medium;");
final SuggestBox box = new SuggestBox(getSuggestions(), itemBox);
box.getElement().setId("suggestion_box");
item.add(box);
list.add(item);
After tabbing off the input, I noticed that it was removed and replaced with a <p> around the value and a <span> to show the "x" to delete it. After adding a couple items, the HTML is as follows:
<ul class="token-input-list-facebook">
<li class="token-input-token-facebook">
<p>What's New Scooby-Doo?</p>
<span class="token-input-delete-token-facebook">x</span>
</li>
<li class="token-input-token-facebook">
<p>Fear Factor</p>
<span class="token-input-delete-token-facebook">x</span>
</li>
<li class="token-input-input-token-facebook">
<input type="text" style="outline-color: -moz-use-text-color; outline-style: none; outline-width: medium;"/>
</li>
</ul>
To do this, I created a deselectItem() method that triggers the DOM transformation.
private void deselectItem(final TextBox itemBox, final BulletList list) {
if (itemBox.getValue() != null && !"".equals(itemBox.getValue().trim())) {
/** Change to the following structure:
* <li class="token-input-token-facebook">
* <p>What's New Scooby-Doo?</p>
* <span class="token-input-delete-token-facebook">x</span>
* </li>
*/
final ListItem displayItem = new ListItem();
displayItem.setStyleName("token-input-token-facebook");
Paragraph p = new Paragraph(itemBox.getValue());
displayItem.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
displayItem.addStyleName("token-input-selected-token-facebook");
}
});
Span span = new Span("x");
span.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
list.remove(displayItem);
}
});
displayItem.add(p);
displayItem.add(span);
list.insert(displayItem, list.getWidgetCount() - 1);
itemBox.setValue("");
itemBox.setFocus(true);
}
}
This method is called after selecting a new item from the SuggestBox:
box.addSelectionHandler(new SelectionHandler<SuggestOracle.Suggestion>() {
public void onSelection(SelectionEvent selectionEvent) {
deselectItem(itemBox, list);
}
});
I also added the ability for you to type in an e-mail address manually and to delete the previous item when you backspace from the input field. Here's the handler that calls deselectItem() and allows deleting with backspace:
// this needs to be on the itemBox rather than box, or backspace will get executed twice
itemBox.addKeyDownHandler(new KeyDownHandler() {
public void onKeyDown(KeyDownEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
// only allow manual entries with @ signs (assumed email addresses)
if (itemBox.getValue().contains("@"))
deselectItem(itemBox, list);
}
// handle backspace
if (event.getNativeKeyCode() == KeyCodes.KEY_BACKSPACE) {
if ("".equals(itemBox.getValue().trim())) {
ListItem li = (ListItem) list.getWidget(list.getWidgetCount() - 2);
Paragraph p = (Paragraph) li.getWidget(0);
list.remove(li);
itemBox.setFocus(true);
}
}
}
});
I'm happy with the results, and grateful for the jQuery plugin's CSS. However, it still has one issue that I haven't been able to solve: I'm unable to click on a list item (to select it) and then delete it (with the backspace key). I believe this is because I'm unable to give focus to the list item. Here's the code that highlights the item and you can see the commented-out code that doesn't work.
displayItem.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
displayItem.addStyleName("token-input-selected-token-facebook");
}
});
/** TODO: Figure out how to select item and allow deleting with backspace key
displayItem.addKeyDownHandler(new KeyDownHandler() {
public void onKeyDown(KeyDownEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_BACKSPACE) {
list.remove(displayItem);
}
}
});
displayItem.addBlurHandler(new BlurHandler() {
public void onBlur(BlurEvent blurEvent) {
displayItem.removeStyleName("token-input-selected-token-facebook");
}
});
*/
If you know of a solution to this issue, please let me know. Feel free to use this widget and improve it as you see fit. I'd love to see this as a native widget in GWT. In the meantime, here's the GWT Facebook-style Autocomplete demo and code.













































