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.