Blog

Extending components in Vaadin 7

By  
Tapio Aali
·
On Feb 25, 2013 1:12:00 PM
·

The ability to extend components is one of the great new features of Vaadin 7. By using extensions you don't have to reinvent the wheel—you can adjust the outlook or the behavior of the existing components instead of creating new ones, thus minimizing the need for copy-paste code.

I have been working at Vaadin for a bit over a year, and in about every project that I have worked on, there has been a need for one simple feature: a reset button on a text field. Take a look, for example, at your favorite touch-device or Spotify and you see it in action. When you type something in a text field, an X button appears. And when you click or tap the button the text is gone! So easy—no need to use delete or backspace ever again.


A resettable text field in action

Thus far, I have implemented it separately project-by-project. Copy-paste, boilerplate, stupidity? Call it whatever you want, but every time I have done this, I haven't been able to stop myself from wondering: could this be a place for an add-on?

From ResettableTextField to ResetButtonForTextField

In Vaadin 6, I would probably have created a CustomComponent containing a layout that would have a TextField and a Button. In order to make the component usable, I would probably have used CustomField add-on. There we would have it: another text field component, ResettableTextField.

But then Vaadin 7 was released with an API for creating extensions for components. Excitement was in the air: there was no need to recreate the TextField anymore; I was just able to extend the existing one. Since I was now creating an extension instead of a component, the project needed a new name: ResetButtonForTextField.

Let's keep it simple

So the idea is to create an extension for the TextField class that adds a button that resets the field? As expected, the server-side is the easy part because it has only one responsibility: to grab the TextField component to be extended.

public class ResetButtonForTextField extends AbstractExtension {
    public static void extend(TextField field) {
        new ResetButtonForTextField().extend((AbstractClientConnector) field);
    }
}

Client-side: a text field, a button, and some listeners

The client-side is responsible for creating the reset button and changing its visibility. It needs to listen to the changes in the text field so that the button is shown and hidden appropriately. Naturally it also handles the clicks of the button itself.

Our client-side class is ResetButtonForTextFieldConnector that extends AbstractExtensionConnector. It is connected to the server-side by using the @Connect annotation.

As the first thing, we add a state change handler to the ServerConnector object given as a parameter so that we can notify the client-side if the value of the text field is changed from the server-side. Since there's no guarantee that our state change handler is always invoked after the TextFieldConnector has added the new value to the DOM, the actual checking is deferred to ensure that we always get the latest value.

target.addStateChangeHandler(new StateChangeEvent.StateChangeHandler() {
    @Override
    public void onStateChanged(StateChangeEvent stateChangeEvent) {
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
            @Override
            public void execute() {
                updateResetButtonVisibility();
            }
        });
    }
});

Then the VTextField object is retrieved from the connector and a style name is applied to it so that we can apply our own custom styles to it. After that, we create the actual reset button. A VButton could be used here, but because the parent of the text field can most likely hold only one widget, trying to insert another one next to the field would be hard, if not impossible. Instead, we only create a DIV element and apply a style name to it too.

textField = (VTextField) ((ComponentConnector) target).getWidget();
textField.addStyleName(CLASSNAME + "-textfield");

resetButtonElement = DOM.createDiv();
resetButtonElement.addClassName(CLASSNAME + "-resetbutton");

Notice that the updateResetButtonVisibility() function used in these snippets is a helper function that shows or hides the button based on whether the value of the text field is empty or not.

private void updateResetButtonVisibility() {
    if (textField.getValue().isEmpty()) {
        resetButtonElement.getStyle().setDisplay(Display.NONE);
    } else {
        resetButtonElement.getStyle().clearDisplay();
    }
}

The reset button element is inserted next to the field during the attach event of the text field and removed during the detach. This is to make sure that it is always there when the text field is appended to the DOM.

public void onAttachOrDetach(AttachEvent event) {
    if (event.isAttached()) {
        textField.getElement().getParentElement()
                .insertAfter(resetButtonElement, textField.getElement());
        updateResetButtonVisibility();
        addResetButtonClickListener(resetButtonElement);
    } else {
        Element parentElement = resetButtonElement.getParentElement();
        if (parentElement != null) {
            parentElement.removeChild(resetButtonElement);
        }
        removeResetButtonClickListener(resetButtonElement);
    }
}

For the same reason, the click listener for the reset button is also added and removed during attach and detach. Since in GWT it is not possible to add event listeners directly to element objects, we'll need to use pure JavaScript by using JavaScript Native Interface. In case you want to know more about JSNI, I recommend reading a blog post by our GWT Expert, Michael Vogt.

The function addResetButtonClickHandler() simply applies an onclick listener to the reset button element.

public native void addResetButtonClickListener(Element el)
/*-{
    var self = this; 
    el.onclick = $entry(function() { 
        self.@org.vaadin.resetbuttonfortextfield.widgetset.client.ResetButtonForTextFieldConnector::clearTextField()();
    }); 
}-*/;

All it does is a call to clearTextField() function that sets the value of the text field to an empty string, notifies the server about the new value and then hides the reset button. Finally, it focuses the text field. This is a usability thing: if the user resets the field, she wants to write something in it, right?

private void clearTextField() {
    textField.setValue("");
    textField.valueChange(true);
    updateResetButtonVisibility();
    textField.getElement().focus();
}

The removeResetButtonClickListener() function is as simple as it gets. It just sets the onclick listener to null so that unused event listeners are not left lingering when the component is detached.

public native void removeResetButtonClickListener(Element el)
/*-{
    el.onclick = null
}-*/;

One more event handler is required. It is a key up handler that is activated when the user types something in the field, showing and hiding the reset button whether the field is empty or not.

public void onKeyUp(KeyUpEvent event) {
    updateResetButtonVisibility();
}

There we have it: a nice text field with a miraculously appearing reset button! You can view the full code for the ResetButtonForTextFieldConnector class in GitHub.

One final thing: styling

Without any styles our reset button is just an empty DIV. Since we want to position the button over the text field, some padding is added to the right side of the field. Then the reset button is sized, positioned and visualized.

From the outlook of the reset button I wanted two things: it had to look constant in every browser and it had to scale nicely. Therefore, I decided to use SVG images. I also made PNG renders of them so that IE8 users would get nice round images too. The latest version of the CSS file can be found from GitHub.

In my style names, I have used resetbuttonfortextfield instead of the v prefix used in the Vaadin core components. There are simple reasons for that, covered in a blog post by Vaadin Expert Marc Englund. To summarize, the styles prefixed with v are intended for the Vaadin core only. This is to make sure that no collisions between styles are caused; neither mine nor the core styles should affect each other unintentionally.

In order to load the stylesheet file as a part of the widgetset, I added the line <stylesteet src="resetbuttonfortextfield/styles.css" /> in the ResetButtonForTextFieldWidgetset.gwt.xml file. Now the widgetset compiler knows to look for the file from the path public/resetbuttonfortextfield located under the directory of the xml file.

Final structure of the project
The final structure of the project

How is it used?

The usage of this extension is really simple. Just add its JAR to you project, compile the widgetset and then extend any TextField component.

TextField tf = new TextField();
ResetButtonForTextField.extend(tf);

That's it. Really. Now your text field has a reset button.


The four phases of a TextField extended with a ResetButtonForTextField

Final words and links

As you probably can imagine, everybody here in the office has been talking endlessly about how Vaadin 7 changes everything, offering great new possibilities to the developers. At least in this context, it means that instead of a need for hacky approaches, the framework offers you the possibility to modify the way things work. Directly.

I hope that you find ResetButtonForTextField as a good example on how to make extensions for components. I tried to make an example that would be as simple as possible while still creating an extension that has a real purpose.

As final words, I want to thank Johannes Dahlström and John Ahlroos for writing these fine Wiki articles: Creating an UI extension and Creating a Component extension.

The complete source code for this project is available in GitHub. You can also check out the demo application. As always, the easiest way to use the extension in your project is to grab it from the Vaadin Directory.


Tapio works at the Vaadin team helping our customers in building solutions on top of Vaadin Framework. His main responsibility is to solve the support requests made by our pro account customers. Ask us anything and we'll answer it.