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:
1 2 3 4 5 | < 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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | < 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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:
1 2 3 4 5 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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.