The Zukunft

Jonas Huckestein's blog about technology, entrepreneurship and stuff like that

An Editable Label for GWT with UiBinder and EventHandlers

with one comment

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);
	}

}

Written by Jonas Huckestein

2010/02/05 at 1:48 pm

Posted in technology

Tagged with , , , ,

One Response

Subscribe to comments with RSS.

  1. 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


Leave a reply to Ben Olsen Cancel reply