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.
Posted by Scott Battaglia on June 05, 2009 at 01:42 PM MDT #
He he. That's what you get for being in my Facebook Friends. ;-)
On a related note, the list of contacts in the GWT demo is a random assortment of folks I exported from my LinkedIn Connections.
Posted by Matt Raible on June 05, 2009 at 04:45 PM MDT #
Posted by Pace on June 08, 2009 at 02:40 AM MDT #
Posted by Sakuraba on June 08, 2009 at 08:16 AM MDT #
Posted by Iván on June 11, 2009 at 10:46 PM MDT #
Posted by James Heggs on June 18, 2009 at 12:19 PM MDT #
Posted by Matt Raible on June 18, 2009 at 01:26 PM MDT #
With IE it would seem that because the input field is smaller than the list width sometimes clicking on the whitespace doesn't focus the input field, the problem is alleviated by setting the width of the input element to 100%, mozilla and chrome don't suffer this problem.
Strangely, my first idea, adding a HasMouseDownHandler to the BulletList and adding a handler that focuses the input box didn't work.
For all the poor souls that need to send that data from a form, you can generate (or wrap) in parallel a hidden ListBox, just don't forget to select the items :) Of course, what to show the user if form fails validation server side is another issue, fix one problem, open the next :D
Also, you use the text in the input filed as the selected value, however, if one uses the Suggestions+DTO value, perhaps it's better to show the ReplacementString and save the DTO value in itemsSelected but that would require some further code changing.
I can't see if there is a way to make suggestions on more than 1-row? Anyhow, great job!
Posted by Srgjan Srepfler on June 19, 2009 at 10:56 PM MDT #
Posted by Srgjan Srepfler on June 22, 2009 at 09:44 AM MDT #
Hello,
thanks for your reply. The first thing I did (before posted my first comment) was download the source code and integrate it into my project. Instead of having a panel I changed some code to simply have a textbox to put into my panels. Then I change the code to fill the oracle with the information from my database and it works ok.
Then, I needed to set in the suggestTextBox the data that I read from the database. After debuging and trying to understand the code, I created the following method to set the values I read from the database. That was easy.
The most complicated part to me was reset the values by code. I needed to simulate the behaviour produced when the user clicks on the "x" button in the element. After a lot of test and debuging I created the following method:
And now, finally it works!!.
This component is amazing and it is what I've been looking for a lot of time. The best for me would be something like the labels management in gmail, but with this I'm happy.
Now it is integrated in Mufly (my GWT personal project). There is no demo to try it but it will be available in few days.
Regards, Iván.
Posted by Iván on June 23, 2009 at 02:30 PM MDT #
a small problem, if the itemsSelected.size() is zero, should stop going on, otherwise there will be an exception
Posted by Vincent Song on July 06, 2009 at 09:21 AM MDT #
Posted by Srgjan Srepfler on July 06, 2009 at 10:05 AM MDT #
Admittedly, I haven't read this too closely, but to solve the click + backspace, I'd suggest making each name in the list a span. Add an onclick handler that marks it (perhaps by adding a css class "selected"). Each time you click it toggles whether the "selected" class is added. You could click multiple names and have multiple spans marked as selected. If you click a particular name again, it would remove the selected class. That's a very simple function to write.
At that point, pressing "delete" makes sense -- and you just remove all the ones that have the "selected" class. I personally wouldn't think to use backspace except if my blinking indicator were at the end of the line (I'd expect it to delete the last item). But if you wanted, you could have "backspace" delete the selected items, as well.
Posted by Richard Morgan on July 21, 2009 at 01:47 PM MDT #
Posted by mumuri on January 29, 2010 at 09:50 PM MST #
Posted by 201.240.70.192 on August 17, 2010 at 09:53 PM MDT #
Posted by Sebastian on August 18, 2010 at 12:58 AM MDT #
Thanks for the code! I don't know if you consider this but having a focus panel (FocusPanel) around your paragraph in this case can give you all the handlers you need: Focus, Blur, KeyDown, etc. That will give you the access to backspace-to-delete thing...
my 2 cents!
--javo
Posted by Javier Ochoa on November 17, 2010 at 02:21 PM MST #
Hi Matt,
Could you update a link in your blog post for me please?
Posted by James Heggs on November 17, 2010 at 02:26 PM MST #
Posted by Matt Raible on November 23, 2010 at 05:18 PM MST #
Posted by Manos on January 30, 2012 at 04:06 AM MST #
Posted by Manos on February 01, 2012 at 12:29 AM MST #
Is this open source or whats the licence to use it?
I am looking for a Vaadin Widget like this and was considering wrapping this example to support Vaadin.
Thanks
Lars.
Posted by Lars on February 27, 2012 at 12:35 PM MST #
Posted by Matt Raible on February 27, 2012 at 12:38 PM MST #
First of all, thank you Matt for sharing your knowledge, experience and great code!
I have played around with and tested the code today and seem to have the following functionality working:
Here is the code with the minor changes needed:
I have renamed the style names as follows:
Posted by Cengiz on July 31, 2012 at 04:33 PM MDT #
Very useful, thanks a lot! I wanted 2 of those in my webapp and I had problem with the focus so I added a
in the constructor and changed the 2 lines referencing the DOM element as follow :
Best,
Thomas
Posted by Thomas on October 18, 2012 at 04:44 PM MDT #