An Editable Label for GWT with UiBinder and EventHandlers
I just learned how to make custom widgets EventHandler-aware so that you can add EventHandlers to them. It turned out to be a little tricky so I thought I’d share that :) To make it more interesting, I’ll also make use of UiBinder and develop a particularly useful widget. The EditableLabel widget. It consists of a label, that becomes editable when clicked and can have attached ValueChangeHandlers (which a Label usually doesn’t have). For this tutorial I used GWT 2.0.
Click here to see a demo of the EditableLabel widget.
The User Interface with UiBinder
This is the easy part and if you’ve never used UiBinder before, it will be fun, too :) If you’re using the Google plugin for Eclipse you can start by creating a new GWT project and creating a new UiBinder class from the File -> New menu. Let’s call it EditableLabel.
First, open EditableLabel.ui.xml and modify it like follows:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent"> <ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui"> <ui:style> .editableLabel { cursor: pointer; cursor: hand; } </ui:style> <g:FocusPanel ui:field="focusPanel"> <g:DeckPanel ui:field="deckPanel"> <g:Label styleName="{style.editableLabel}" ui:field="editLabel"/> <g:TextArea styleName="edit" ui:field="editBox"/> </g:DeckPanel> </g:FocusPanel> </ui:UiBinder>
Our widget is made up of a DeckPanel wrapped inside a FocusPanel because it needs to be able to catch focus events. DeckPanels are like a stack of cards; at any given time, they display exactly one of their child widgets. We use this functionality to hide the Label child and show the TextArea child when the Label is clicked.
Next, open the corresponding EditableLabel.java file that should have been created for you and add the following class members that you defined in the .ui.xml file:
@UiField protected Label editLabel; @UiField protected DeckPanel deckPanel; @UiField protected TextArea editBox; @UiField protected FocusPanel focusPanel;
Now you can already compile an application with the widget. Just go to your entry point class and add something along the lines of
RootPanel.get().add(new EditableLabel());
Implementing HasValueChangeHandlers and HasValue<String>
My intention was to implement HasValueChangeHandlers<String> so that the Widget could be used instead of a normal TextArea or TextBox. I ended up implementing HasValue<String> because it extends HasValueChangeHandlers<String> and adds convenient setValue and getValue functions.
I’ll go ahead and post the code implementing that interface and explain later (make sure to put “implements HasValue<String>” in your class definition):
@Override public HandlerRegistration addValueChangeHandler( ValueChangeHandler<String> handler) { return addHandler(handler, ValueChangeEvent.getType()); } @Override public String getValue() { return editLabel.getText(); } @Override public void setValue(String value) { editLabel.setText(value); editBox.setText(value); } @Override public void setValue(String value, boolean fireEvents) { if(fireEvents) ValueChangeEvent.fireIfNotEqual(this, getValue(), value); setValue(value); }
There are a two things going on in that code that were not obvious to me:
- Every Widget subclass already has the ability to add EventHandlers to it. There is no need to implement your own handler list. You add handlers using the protected addHandler() function that takes the handler and the type of event to be handled as arguments.
- Firing events is implemented as a static function of the event type that takes the source widget of the event as argument. EventHandlers that have been added to that source will then get called. Having used the MVP pattern a lot before, I was expecting to create a new ValueChangeEvent and then send that on some eventBus or other HandlerManager.
The Logic
Now that we have implemented HasValue, we can easily implement the logic required for our EditableLabel. Let’s start by defining the following two functions that switch between edit and label mode:
public void switchToEdit() { if(deckPanel.getVisibleWidget() == 1) return; editBox.setText(getValue()); deckPanel.showWidget(1); editBox.setFocus(true); } public void switchToLabel() { if(deckPanel.getVisibleWidget() == 0) return; setValue(editBox.getText(), true); // fires events, too deckPanel.showWidget(0); }
The code should be straight-forward. Using the event-firing version of setValue we can easily push changes that were made in edit mode to registered EventHandlers.
Next, we’ll add the UI logic that wires everything up. Add this to your constructor after initWidget has been called.
deckPanel.showWidget(0); focusPanel.addFocusHandler(new FocusHandler() { @Override public void onFocus(FocusEvent event) { switchToEdit(); } }); editLabel.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { switchToEdit(); } }); editBox.addBlurHandler(new BlurHandler() { @Override public void onBlur(BlurEvent event) { switchToLabel(); } }); editBox.addKeyPressHandler(new KeyPressHandler() { @Override public void onKeyPress(KeyPressEvent event) { if (event.getCharCode() == KeyCodes.KEY_ENTER) { switchToLabel(); } else if (event.getCharCode() == KeyCodes.KEY_ESCAPE) { editBox.setText(editLabel.getText()); // reset to the original value } } });
It goes like this:
- When somebody clicks on the label, we’ll switch to edit mode
- When somebody focuses (or tabs to) our widget, we’ll switch to edit mode
- When the TextArea loses focus, we’ll switch back to label mode
- When somebody presses the return key in edit mode, we’ll save the value and switch to label mode
- When somebody presses ESC in edit mode, we’ll discard the new value and switch to label mode
This is it! We have built a super-useful editable label widget that can be focused, handles clicks and keyboard events and can have registered ValueChangeHandlers! Obvious enhancements would be to have some features be optional, such as the ability to focus. Here is the full sourcecode for my EditableLabel.java:
package com.onetwopoll.gwt.framework.widget; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.FocusEvent; import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.DeckPanel; import com.google.gwt.user.client.ui.FocusPanel; import com.google.gwt.user.client.ui.HasValue; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.TextArea; import com.google.gwt.user.client.ui.Widget; /** * Allows in-place editing of Labels. Activated via click. * * The reference value is always the one in the label and change events are only fired after editing has finished. * * TODO make it optional how to activate the editing mode * * @author Jonas Huckestein * */ public class EditableLabel extends Composite implements HasValue<String> { private static EditableLabelUiBinder uiBinder = GWT .create(EditableLabelUiBinder.class); interface EditableLabelUiBinder extends UiBinder<Widget, EditableLabel> { } @UiField protected Label editLabel; @UiField protected DeckPanel deckPanel; @UiField protected TextArea editBox; @UiField protected FocusPanel focusPanel; public EditableLabel() { initWidget(uiBinder.createAndBindUi(this)); deckPanel.showWidget(0); focusPanel.addFocusHandler(new FocusHandler() { @Override public void onFocus(FocusEvent event) { switchToEdit(); } }); editLabel.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { switchToEdit(); } }); editBox.addBlurHandler(new BlurHandler() { @Override public void onBlur(BlurEvent event) { switchToLabel(); } }); editBox.addKeyPressHandler(new KeyPressHandler() { @Override public void onKeyPress(KeyPressEvent event) { if (event.getCharCode() == KeyCodes.KEY_ENTER) { switchToLabel(); } else if (event.getCharCode() == KeyCodes.KEY_ESCAPE) { editBox.setText(editLabel.getText()); // reset to the original value } } }); } public void switchToEdit() { if(deckPanel.getVisibleWidget() == 1) return; editBox.setText(getValue()); deckPanel.showWidget(1); editBox.setFocus(true); } public void switchToLabel() { if(deckPanel.getVisibleWidget() == 0) return; setValue(editBox.getText(), true); // fires events, too deckPanel.showWidget(0); } @Override public HandlerRegistration addValueChangeHandler( ValueChangeHandler<String> handler) { return addHandler(handler, ValueChangeEvent.getType()); } @Override public String getValue() { return editLabel.getText(); } @Override public void setValue(String value) { editLabel.setText(value); editBox.setText(value); } @Override public void setValue(String value, boolean fireEvents) { if(fireEvents) ValueChangeEvent.fireIfNotEqual(this, getValue(), value); setValue(value); } }
Thank you so much. I sat down this morning intending to write this exact widget, then though, “Hey, maybe someone else has already done this and put it on the internet.” A quick search and your solution popped up. Awesome.
Ben Olsen
2010/02/17 at 12:17 pm