Custom Widgets & Composites
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (released 2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, the Jakarta Servlet 6 server of the Bookshelf scenario and the gwt-user widget library, checked on 2026-10-08.
Everything here is client code: the classes are compiled to JavaScript and run in the browser.
The shared and server modules of the Bookshelf do not know about widgets, and the build needs nothing beyond <inherits name="com.google.gwt.user.User"/>, plus com.google.gwt.editor.Editor for the editor integration shown near the end.
Every example on this page was compiled with the 2.13.1 compiler, loaded in headless Chrome 155 and driven by dispatching real DOM events; the console output and the error messages are quoted as they were printed, once with assertions enabled (-checkAssertions) and once without.
The rating widget also passed two GWTTestCase tests under HtmlUnit.
A widget is a Java object that owns one DOM element and knows how to react to events on it. GWT ships the common ones (buttons, text boxes, lists, tables; see the Widgets page), and an application soon needs one that does not exist: a star rating, a list of removable author names, a label that turns into a text box. There are three strategies, and this page is about the first two.
| Strategy | How | Use it when | Covered |
|---|---|---|---|
Composite of existing widgets |
extend |
the new widget is an arrangement of existing ones (almost always) |
this page |
Widget from DOM elements |
extend |
nothing existing renders the markup you need, or you need very few DOM nodes |
this page |
Widget from a template |
a |
the layout is mostly static markup with a few dynamic parts |
the UiBinder page |
Wrapper of JavaScript |
a native JsInterop type, or (legacy) JSNI methods inside a |
an existing JavaScript widget or web component must be reused |
The Developer Guide page lists the same three strategies (composite, "from scratch in Java code", JSNI) and says that the most effective way to create a new widget is to extend Composite.
The rest of this page follows that advice: composites first, then what a composite hides, then the lower level you need when a composite cannot do the job.
How the pieces relate
Every widget extends Widget, which extends UIObject (the class that owns the DOM element, the style names and the visibility).
Composite is a Widget that wraps another widget, and Panel is a Widget that contains many.
The Bookshelf classes of this page (Chip, ChipPanel, RatingWidget, AuthorChips, ReviewBox) live in the package com.example.bookshelf.client.widget of the client module.
A composite does not add a DOM element of its own: it adopts the element of the widget it wraps.
ReviewBox below wraps a VerticalPanel, so the composite and the panel are one table element in the page, even though there are two Java objects.
Composite and initWidget
The Developer Guide gives a first example: a check box that enables or disables a text box, laid out in a vertical panel.
Compiled against GWT 2.13.1 exactly as printed, that snippet does not build.
javac reported cannot find symbol: class ClickEvent (the snippet imports ClickHandler but never ClickEvent, which its onClick(ClickEvent) needs), and it compiles setChecked and isChecked with [deprecation] warnings, because CheckBox now uses setValue(Boolean) and getValue().
The version below is the same widget with the import and the current API, and it ran in the browser: before the click textBox enabled=true, after a dispatched click on the check box textBox enabled=false, checkbox checked=false.
OptionalTextBox.java)package com.example.bookshelf.client.widget;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
public class OptionalTextBox extends Composite implements ClickHandler {
private final TextBox textBox = new TextBox();
private final CheckBox checkBox = new CheckBox();
public OptionalTextBox(String caption) {
VerticalPanel panel = new VerticalPanel();
panel.add(checkBox);
panel.add(textBox);
checkBox.setText(caption);
checkBox.setValue(true);
checkBox.addClickHandler(this);
initWidget(panel); // every composite must call this exactly once
setStyleName("bk-OptionalTextBox");
}
@Override
public void onClick(ClickEvent event) {
textBox.setEnabled(checkBox.getValue());
}
public String getCaption() {
return checkBox.getText();
}
public void setCaption(String caption) {
checkBox.setText(caption);
}
}
What a Composite does
Composite.initWidget(Widget) is protected, takes the widget to wrap, and does five things (read from the 2.13.1 source):
-
it throws if a widget was already set (
Composite.initWidget() may only be called once.) or if the argument is null (widget cannot be null) -
it detaches the new child from any previous parent (
widget.removeFromParent()) -
it calls
setElement(widget.getElement()), so the composite is the wrapped element -
it records the wrapped widget and makes the composite its parent (
widget.setParent(this)) -
from then on,
isAttached(),onAttach(),onDetach()andonBrowserEvent(Event)are delegated to the wrapped widget
The class also keeps the wrapped widget in a private field behind protected Widget getWidget(); the old setWidget(Widget) still exists but is @Deprecated in favour of initWidget.
Composite additionally implements IsRenderable, an interface the Javadoc calls "very experimental" and which only UiBinder internals use; you never implement or call it.
The design reason is in the Developer Guide: you could subclass FlowPanel or VerticalPanel to build a new widget, but then every public method of the panel (add, clear, insert and so on) becomes part of your widget’s API, and callers can corrupt its structure.
A composite exposes only what you declare.
TabPanel (a TabBar plus a DeckPanel), SuggestBox, DisclosurePanel, CaptionPanel, StackLayoutPanel, TabBar, ValueListBox, ValuePicker and the DatePicker of the com.google.gwt.user.datepicker.client package are all composites in the 2.13.1 source.
Styling a composite
Because the composite and the wrapped widget share an element, they share the class attribute.
This matters more than it looks: setStyleName(String) replaces every style name, including the one the wrapped widget set for itself.
wrapped Button class before: 'gwt-Button'
after addStyleName(extra): 'gwt-Button extra'
after setStyleName(bk-mine): 'bk-mine'
Use addStyleName to add a name, setStylePrimaryName to change the primary name that dependent names are built from, and setStyleDependentName("disabled", true) to add a name such as bk-Rating-disabled (the rating widget below does).
Calling setStyleName in the constructor, as OptionalTextBox does, is fine when the wrapped widget has no style of its own; a composite around a Button or a TextBox should use addStyleName.
The Styling: CSS & GSS page covers CSS and GSS.
Source: Javadoc — UIObject
Resizing composites and lazy widgets
Two small relatives matter inside layout panels:
-
ResizeCompositeextendsCompositeand implementsRequiresResize; itsinitWidgetasserts that the wrapped widget is itself aRequiresResize(the assertion message is "ResizeComposite requires that its wrapped widget implement RequiresResize") and itsonResize()forwards to it. Use it when the wrapped widget is a layout panel (DockLayoutPanel,SplitLayoutPanel,TabLayoutPanel) and the composite sits inside another layout panel; see the page on panels and layout. -
LazyPanelextendsSimplePanel, is abstract, and builds its child only when it is first made visible: you implementcreateWidget()and the framework calls it fromensureWidget()orsetVisible(true). It suits expensive child widgets in tabs that the user may never open.
Choosing the interfaces to implement
A widget’s behaviour contract is the set of small interfaces it implements.
GWT’s own widgets, UiBinder, the Editor framework and the cell widgets all work against these interfaces, so implementing the right ones is how a custom widget plugs into the rest of the toolkit.
Implement only what the widget really supports, and delegate to the wrapped widget where one exists.
| Interface | Contract | What it buys you |
|---|---|---|
|
|
widgets whose content is a caption or a text, so that generic code and UiBinder can set it |
|
|
form fields; the Editor framework ( |
|
only |
a widget that reports a new value but does not accept one (the |
|
|
|
|
|
forms that disable everything with one loop |
|
|
keyboard navigation; a composite delegates to the part that owns the tab stop. |
|
|
form submission names |
|
|
the Editor framework picks the widget up as a sub-editor (see the Editors page) |
|
|
presenters and views that return |
|
|
a panel: UiBinder children ( |
|
|
observers that need to know when a widget enters or leaves the document |
|
|
ordinary opt-in interfaces for widgets that can be hidden or that animate |
Source: Javadoc — HasValue
The interfaces carry conventions that the compiler does not check:
-
setValue(v)issetValue(v, false), and only the two-argument form withtruefires aValueChangeEvent. Code that fills a form must not trigger the handlers that react to user edits. -
Fire with
ValueChangeEvent.fireIfNotEqual(this, oldValue, newValue), notfire. The static method compares withequals(null-safe) and stays silent when nothing changed; this is why the value classes below implementequals. -
Register handlers with
addHandler(handler, ValueChangeEvent.getType()).Widget.addHandlerispublic final, creates the handler manager lazily, and returns theHandlerRegistrationthat callers use to unregister. -
getValueof a collection returns a copy (AuthorChipsdoes), or callers can change the widget’s state behind its back and no event fires. -
Treat
nullas a value you handle, usually as "empty" (RatingWidgetmaps it to zero,EditableLabelto the empty string).
The Events & Handlers page explains the handler machinery these conventions sit on.
An editable label
The first widget of the book’s chapter on composites is a label that becomes a text box when clicked; it is a good first composite because it has two states and two parts.
This version uses a DeckPanel to show one of its two children, handlers instead of listeners, and implements HasText and HasValue<String>.
public class EditableLabel extends Composite implements HasText, HasValue<String> {
private static final int VIEW = 0;
private static final int EDIT = 1;
private final DeckPanel deck = new DeckPanel();
private final Label label = new Label();
private final TextBox box = new TextBox();
private String value = "";
public EditableLabel(String text) {
deck.add(label);
deck.add(box);
initWidget(deck);
setStyleName("bk-EditableLabel");
label.addClickHandler((ClickEvent e) -> edit());
box.addBlurHandler((BlurEvent e) -> commit());
box.addKeyDownHandler((KeyDownEvent e) -> {
int key = e.getNativeKeyCode();
if (key == KeyCodes.KEY_ENTER) {
commit();
} else if (key == KeyCodes.KEY_ESCAPE) {
cancel();
}
});
setValue(text);
}
private void edit() {
box.setText(value);
deck.showWidget(EDIT);
box.setFocus(true);
box.selectAll();
}
private void commit() {
if (deck.getVisibleWidget() == EDIT) {
deck.showWidget(VIEW);
setValue(box.getText(), true);
}
}
private void cancel() {
deck.showWidget(VIEW);
}
@Override public String getText() { return getValue(); }
@Override public void setText(String text) { setValue(text); }
@Override public String getValue() { return value; }
@Override public void setValue(String v) { setValue(v, false); }
@Override
public void setValue(String v, boolean fireEvents) {
String old = value;
value = v == null ? "" : v;
label.setText(value); // setText, never setHTML, for user data
deck.showWidget(VIEW);
if (fireEvents) {
ValueChangeEvent.fireIfNotEqual(this, old, value);
}
}
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
}
With a handler that appends the new value to a string, the lab calls setValue("Silent", false), setValue("Silent", true) and setValue("Loud", true) and prints EditableLabel events: [Loud] text=Loud: the first call is silent by contract and the second one fires nothing because the value did not change.
Source: Javadoc — ValueChangeEvent
The Bookshelf rating widget
A review in the Bookshelf has a rating from 0 to 5.
The widget is a composite of a FocusPanel (it owns the tab stop and receives key events), a FlowPanel and five InlineLabel stars.
It implements HasValue<Integer> so that it behaves like any form field, HasEnabled so that a read-only view can disable it, Focusable by delegating to the FocusPanel, and IsEditor so that the editor framework can drive it.
ARIA attributes make it a slider for assistive technology: Roles.getSliderRole() from com.google.gwt.aria.client sets role="slider", aria-valuemin, aria-valuemax, aria-valuenow and aria-disabled.
public class RatingWidget extends Composite
implements HasValue<Integer>, HasEnabled, Focusable, IsEditor<LeafValueEditor<Integer>> {
public static final int MAX = 5;
private final FocusPanel focus = new FocusPanel();
private final InlineLabel[] stars = new InlineLabel[MAX];
private int value;
private boolean enabled = true;
private LeafValueEditor<Integer> editor;
public RatingWidget() {
FlowPanel row = new FlowPanel();
for (int i = 0; i < MAX; i++) {
final int rating = i + 1;
InlineLabel star = new InlineLabel();
star.setStyleName("bk-star");
star.addClickHandler(e -> {
if (enabled) {
setValue(rating, true);
}
});
stars[i] = star;
row.add(star);
}
focus.setWidget(row);
focus.addKeyDownHandler(e -> {
if (!enabled) {
return;
}
int key = e.getNativeKeyCode();
if (key == KeyCodes.KEY_RIGHT || key == KeyCodes.KEY_UP) {
setValue(value + 1, true);
e.preventDefault();
} else if (key == KeyCodes.KEY_LEFT || key == KeyCodes.KEY_DOWN) {
setValue(value - 1, true);
e.preventDefault();
}
});
initWidget(focus);
setStyleName("bk-Rating");
Roles.getSliderRole().set(getElement());
Roles.getSliderRole().setAriaValueminProperty(getElement(), 0);
Roles.getSliderRole().setAriaValuemaxProperty(getElement(), MAX);
render();
}
private void render() {
for (int i = 0; i < MAX; i++) {
stars[i].setText(i < value ? "★" : "☆");
}
Roles.getSliderRole().setAriaValuenowProperty(getElement(), value);
}
@Override public Integer getValue() { return value; }
@Override public void setValue(Integer v) { setValue(v, false); }
@Override
public void setValue(Integer v, boolean fireEvents) {
Integer old = value;
value = v == null ? 0 : Math.max(0, Math.min(MAX, v));
render();
if (fireEvents) {
ValueChangeEvent.fireIfNotEqual(this, old, value);
}
}
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
@Override public boolean isEnabled() { return enabled; }
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
setStyleDependentName("disabled", !enabled);
Roles.getSliderRole().setAriaDisabledState(getElement(), !enabled);
}
@Override public int getTabIndex() { return focus.getTabIndex(); }
@Override public void setAccessKey(char key) { focus.setAccessKey(key); }
@Override public void setFocus(boolean focused) { focus.setFocus(focused); }
@Override public void setTabIndex(int index) { focus.setTabIndex(index); }
@Override
public LeafValueEditor<Integer> asEditor() {
if (editor == null) {
editor = TakesValueEditor.of(this);
}
return editor;
}
}
Source: Javadoc — Roles
The RangeRole setters take a Number, not a String: the first version of this class passed String.valueOf(value) and the compiler stopped with The method setAriaValuenowProperty(Element, Number) in the type RangeRole is not applicable for the arguments (Element, String).
The lab clicked the third star, pressed the right arrow three times, disabled the widget, clicked again, then asked for 99 and drove the editor adapter. The handler collected the values it received.
RatingWidget value=5 events=3 4 5 text=★★★★★
RatingWidget disabled click -> value=5 class=bk-Rating bk-Rating-disabled
RatingWidget setValue(99) clamps to 5
RatingWidget aria: role=slider now=5 min=0 max=5 disabled=true tabindex=0
RatingWidget editor value=5
RatingWidget after editor.setValue(2): 2
The third right-arrow press fired nothing because the value was already 5, which is fireIfNotEqual working as intended.
The DOM of a disabled widget showing two stars is:
<div tabindex="0" class="bk-Rating bk-Rating-disabled" role="slider" aria-valuemin="0"
aria-valuemax="5" aria-valuenow="2" aria-disabled="true">
<input type="text" tabindex="-1" aria-hidden="true"
style="opacity: 0; height: 1px; width: 1px; z-index: -1; overflow: hidden; position: absolute;">
<div><span class="bk-star">★</span><span class="bk-star">★</span><span class="bk-star">☆</span>
<span class="bk-star">☆</span><span class="bk-star">☆</span></div>
</div>
The one-pixel transparent input comes from FocusImplSafari, whose Javadoc explains that it "creates a completely transparent hidden element, since Safari will not keyboard focus on" a plain div.
It is the deferred-binding implementation of FocusPanel, chosen by the user.agent property; the gecko1_8 permutation uses a different implementation, which is why GWT widgets sometimes render differently per permutation.
A small stylesheet turns the spans into clickable stars; GSS or CSS work equally (see Styling: CSS & GSS).
.bk-star { cursor: pointer; font-size: 1.4em; color: #c98a00; }
.bk-Rating-disabled .bk-star { cursor: default; color: #999; }
.bk-Rating:focus { outline: 2px solid #3366cc; }
.bk-chip { display: inline-block; margin: 0 .3em .3em 0; padding: .1em .5em; border-radius: 1em;
background: #e6eefc; }
.bk-chip-remove { border: 0; background: none; cursor: pointer; font-size: 1em; }
Using the widget from a form
Because the widget is a normal HasValue<Integer>, three consumers work without extra code.
-
Code:
rating.setValue(4)fills it quietly andrating.addValueChangeHandler(handler)observes user input. -
UiBinder:
<w:RatingWidget ui:field="rating"/>withxmlns:w="urn:import:com.example.bookshelf.client.widget"(a no-argument constructor is all UiBinder needs) and@UiHandler("rating") void onRating(ValueChangeEvent<Integer> e). The lab did exactly this, clicked the fourth star, and the handler wroterated 4into a label. -
Editors: a field named after a bean property, here
RatingWidget ratingin anEditor<BookReview>, is edited throughasEditor().driver.edit(review)pushedrating=4andauthors=[A. Author, B. Author]into the widgets; after the user changed themdriver.flush()pulledrating=2 authors=[C. Author]back into the same bean instance.
public class ReviewEditor extends Composite implements Editor<BookReview> {
interface Driver extends SimpleBeanEditorDriver<BookReview, ReviewEditor> { }
RatingWidget rating = new RatingWidget(); // property "rating" of BookReview
AuthorChips authors = new AuthorChips(); // property "authors"
private final Driver driver = GWT.create(Driver.class);
public ReviewEditor() {
FlowPanel p = new FlowPanel();
p.add(rating);
p.add(authors);
initWidget(p);
driver.initialize(this);
}
public void edit(BookReview r) { driver.edit(r); }
public BookReview flush() { return driver.flush(); }
}
Source: GWT Developer Guide — Editors
The editor framework treats every field and every no-argument method that returns an Editor or IsEditor type as a sub-editor.
The first version of this class had two helper methods ratingWidget() and authorChips() returning the widgets, and the deferred-binding generator stopped with Could not find a getter for path ratingWidget in proxy type com.example.bookshelf.client.view.BookReview (and the same for authorChips), then Unable to create Editor model due to previous errors and Failed to resolve 'com.example.bookshelf.client.view.ReviewEditor.Driver' via deferred binding.
Helper methods on an editor must return plain values, or take an argument.
Widgets from DOM elements
A composite is the right tool until the markup you need does not exist as a widget.
A removable chip (a rounded span with a name and a small remove button) is two elements and one click rule; wrapping it in FlowPanel, Label and Button would work, but it creates three widgets and three elements for one visual.
Building it from the DOM is shorter and cheaper, and it shows the mechanism that every GWT widget is built on.
The rules for a widget that extends Widget directly:
-
Create the element and call
setElement(Element)exactly once, normally first in the constructor, with the root element of the widget. The element comes fromDocument.get().createXxxElement(); thedom.clientclasses are typed overlays, so there are no wrapper objects (see DOM & Elemental2). ThesetElement(com.google.gwt.user.client.Element)overload is@Deprecated. -
getElement()returnscom.google.gwt.user.client.Element, a subclass ofcom.google.gwt.dom.client.Element, and its Javadoc says not to override it. -
Sink the events you handle with
sinkEvents(int)and the bit constants ofEvent(Event.ONCLICK), or register throughaddDomHandler(handler, ClickEvent.getType()), which sinks the right bit for you. The bits are only recorded until the widget is attached and applied then. -
Override
onBrowserEvent(Event)and callsuper.onBrowserEvent(event), which dispatches to the handlers registered withaddDomHandler. If you do not callsuper, those handlers silently stop working. -
Put text in with
setInnerText, which setstextContent. ReservesetInnerHTMLfor markup you built from constants, and useSafeHtml(see SafeHtml & XSS) when HTML must contain data.
public class Chip extends Widget implements HasText, HasCloseHandlers<Chip> {
private final SpanElement label = Document.get().createSpanElement();
private final ButtonElement remove = Document.get().createPushButtonElement();
public Chip(String text) {
SpanElement root = Document.get().createSpanElement();
setElement(root); // exactly once, before anything else touches the element
setStyleName("bk-chip");
label.setClassName("bk-chip-label");
remove.setClassName("bk-chip-remove");
remove.setInnerText("×"); // text, not HTML
root.appendChild(label);
root.appendChild(remove);
setText(text);
sinkEvents(Event.ONCLICK); // without this bit onBrowserEvent never sees a click
}
@Override public String getText() { return label.getInnerText(); }
@Override
public void setText(String text) {
label.setInnerText(text); // textContent: <script> in an author name stays text
remove.setTitle("Remove " + text);
}
@Override
public void onBrowserEvent(Event event) {
super.onBrowserEvent(event); // keeps handlers added with addDomHandler working
if (event.getTypeInt() == Event.ONCLICK
&& Element.is(event.getEventTarget())
&& remove.isOrHasChild(Element.as(event.getEventTarget()))) {
event.stopPropagation();
CloseEvent.fire(this, this);
}
}
@Override
public HandlerRegistration addCloseHandler(CloseHandler<Chip> handler) {
return addHandler(handler, CloseEvent.getType());
}
}
CloseEvent and HasCloseHandlers<T> are the standard "something asked to go away" event from com.google.gwt.event.logical.shared, so the chip does not need an event class of its own.
Before the sink bit is set, a click on the widget goes nowhere: the lab built two otherwise identical widgets, one that never called sinkEvents and one that did, dispatched a click on each, and printed click on widget that never sank ONCLICK: clicks=0; with sinkEvents: clicks=1.
The Developer Guide’s third strategy, writing methods in JSNI, is legacy; it names TextBox and TextBoxImpl as the example and advises deferred binding to isolate browser differences.
The same advice survives in the 2.13.1 tree as the *Impl classes in com.google.gwt.user.client.ui.impl; for example FocusPanel asks for a FocusImpl, and Focus.gwt.xml replaces it with FocusImplStandard or FocusImplSafari depending on the user.agent permutation (see Deferred Binding & Generators).
For new code, reach JavaScript through JsInterop native types (see JsInterop) rather than JSNI (see JSNI & Overlay Types).
The attach and detach callbacks
A widget lives in one of two states: attached (its element is inside the document, reachable from a RootPanel) or detached.
The state changes only when an ancestor panel adds or removes the widget, and four callbacks tell the widget about it.
| Method | When it runs | What to do there |
|---|---|---|
|
sets the attached flag, installs the event listener, applies the pending |
nothing: override |
|
after the widget and all its children are attached |
start things that need the widget in the document: timers, global listeners, measuring the size |
|
first thing in |
stop what |
|
calls |
nothing: override |
addAttachHandler(AttachEvent.Handler) is the non-subclassing alternative, delivering both transitions to an observer.
The page DOM & Elemental2 shows the same machinery from the side of event handlers and memory leaks.
Order across nested composites
For a composite the order is easy to get wrong, so the lab measured it.
Outer is a composite of a FlowPanel holding an Inner composite, which wraps a panel holding a labelled leaf widget; every class logs its onLoad and onUnload.
-- lifecycle: attach
before attach: outer.isAttached=false
leaf.onAttach (before super)
leaf.onLoad isAttached=true
inner panel.onLoad
Inner(composite).onLoad isAttached=true
Outer(composite).onLoad
Outer attach handler: attached=true
-- lifecycle: detach
Outer(composite).onUnload
Outer attach handler: attached=false
Inner(composite).onUnload isAttached=true
inner panel.onUnload
leaf.onUnload isAttached=true
leaf.onDetach (after super) isAttached=false
after remove: outer.isAttached=false
The rules that follow from the measurement:
-
Attach runs from the leaves up, detach from the root down. When an outer
onLoadruns, every inner widget is already attached and measurable; when an outeronUnloadruns, the inner widgets are still intact. -
Inside
onUnload,isAttached()is still true. The flag is cleared at the very end ofonDetach(in afinallyblock). -
A composite’s
isAttached()is the wrapped widget’s, as the source shows, and it isfalsefor a composite that never calledinitWidget.
Start in onLoad, stop in onUnload
The standard use is a resource that must exist only while the widget is on screen.
DueLabel shows the seconds left on a loan and refreshes every second, using the GWT Timer (see Delayed Logic & Scheduler).
public class DueLabel extends Label {
private final Timer ticker = new Timer() {
@Override
public void run() {
refresh();
}
};
private int secondsLeft;
public DueLabel(int secondsLeft) {
this.secondsLeft = secondsLeft;
refresh();
}
private void refresh() {
setText(secondsLeft-- > 0 ? "due in " + (secondsLeft + 1) + " s" : "overdue");
}
@Override
protected void onLoad() {
super.onLoad();
ticker.scheduleRepeating(1000); // start when the widget enters the document ...
}
@Override
protected void onUnload() {
ticker.cancel(); // ... and stop when it leaves
super.onUnload();
}
}
The lab attached a DueLabel(60), removed it after 2.5 seconds and read it again after five.
A second label, whose timer was started in its constructor and never cancelled, was removed at the same moment.
DueLabel before attach: 'due in 60 s'
DueLabel attached for ~2.5s: 'due in 58 s'
DueLabel detached ~2.5s ago: 'due in 58 s'
leaky label (timer started in the constructor), detached since 2.5s: 'leaky 96'
The first label stopped at 58 when it left the page; the second kept counting for an object nobody could see, and every instance ever created would still be ticking, kept alive by the timer.
That is the memory-leak pattern the article on DOM events describes, in its modern form: the leaks that remain are not the browser’s, they are registrations on long-lived objects (timers, the Window, an event bus) made by short-lived widgets.
Register in onLoad, keep the HandlerRegistration or Timer, and release it in onUnload.
Writing a new panel
The book’s chapter on panels ends with writing your own.
A panel is a widget that owns child widgets, and ComplexPanel already implements the bookkeeping, so a new panel is often a dozen lines.
ChipPanel puts its children one after another inside a single div:
public class ChipPanel extends ComplexPanel {
public ChipPanel() {
setElement(Document.get().createDivElement());
setStyleName("bk-chips");
}
@Override
public void add(Widget child) {
add(child, getElement()); // detaches from the old parent, adds to the DOM, adopts
}
public void insert(Widget child, int beforeIndex) {
insert(child, getElement(), beforeIndex, true);
}
}
Source: GWT Developer Guide — Panels
What the class hierarchy supplies:
-
PanelimplementsHasWidgets.ForIsWidget(add(IsWidget),remove(IsWidget),clear(),iterator()) and provides the two final methods every panel uses to maintain the parent link,adopt(Widget)andorphan(Widget), plusdoAttachChildrenanddoDetachChildren, which attach and detach all children through anAttachDetachExceptioncommand so that one failing child does not strand the others. -
ComplexPanelimplementsIndexedPanel.ForIsWidget(getWidget(int),getWidgetCount(),getWidgetIndex(Widget),remove(int)), keeps the children in aWidgetCollection, and offers the two protected helpers used above.add(child, container)removes the child from its previous parent first, adds it to the collection, appends its element to the container element and callsadopt.insert(child, container, beforeIndex, domInsert)does the same at an index. -
remove(Widget)ofComplexPanelreturnsfalsefor a widget that is not a child, orphans the child (which detaches it when the panel is attached), removes its element from the parent element and takes it out of the collection. -
The container element does not have to be the panel’s own element:
HorizontalPanelcreates a table cell for each child and passes that cell toadd(child, container). A panel with several containers repeats the same call per container.
A panel that holds exactly one child extends SimplePanel instead and implements HasOneWidget; a panel that lays out with CSS layout (LayoutPanel, DockLayoutPanel) additionally implements RequiresResize or ProvidesResize.
If the panel does not override add(Widget), Panel.add throws, which the lab confirmed with a ComplexPanel subclass that forgot it: java.lang.UnsupportedOperationException: This panel does not support no-arg add().
The lab also added the same widget to two panels in a row.
ComplexPanel.add(child, container) calls child.removeFromParent() first, so the widget moved: in a: 0, in b: 1.
A widget never has two parents, and adding it somewhere else is a move, not an error.
The Bookshelf author chips
public class AuthorChips extends Composite
implements HasValue<List<String>>, HasEnabled, IsEditor<LeafValueEditor<List<String>>> {
private final ChipPanel chips = new ChipPanel();
private final TextBox input = new TextBox();
private final List<String> authors = new ArrayList<>();
private LeafValueEditor<List<String>> editor;
public AuthorChips() {
FlowPanel root = new FlowPanel();
root.add(chips);
root.add(input);
initWidget(root);
setStyleName("bk-AuthorChips");
input.getElement().setAttribute("placeholder", "Add an author");
input.addKeyDownHandler(e -> {
if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
add(input.getText().trim());
input.setText("");
}
});
}
private void add(String name) {
if (name.isEmpty() || authors.contains(name)) {
return;
}
List<String> old = getValue();
addChip(name);
authors.add(name);
ValueChangeEvent.fireIfNotEqual(this, old, getValue());
}
private void addChip(String name) {
final Chip chip = new Chip(name);
chip.addCloseHandler(e -> {
List<String> old = getValue();
authors.remove(chip.getText());
chips.remove(chip);
ValueChangeEvent.fireIfNotEqual(this, old, getValue());
});
chips.add(chip);
}
@Override public List<String> getValue() { return new ArrayList<>(authors); } // defensive copy
@Override public void setValue(List<String> v) { setValue(v, false); }
@Override
public void setValue(List<String> v, boolean fireEvents) {
List<String> old = getValue();
authors.clear();
chips.clear();
if (v != null) {
for (String name : v) {
authors.add(name);
addChip(name);
}
}
if (fireEvents) {
ValueChangeEvent.fireIfNotEqual(this, old, getValue());
}
}
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<List<String>> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
@Override public boolean isEnabled() { return input.isEnabled(); }
@Override public void setEnabled(boolean enabled) { input.setEnabled(enabled); }
@Override
public LeafValueEditor<List<String>> asEditor() {
if (editor == null) {
editor = TakesValueEditor.of(this);
}
return editor;
}
}
The lab loaded two authors, one of them a hostile string, then removed the first chip with a dispatched click, typed a name with surrounding spaces into the box and pressed Enter twice (the second press is a duplicate and is ignored).
<div class="bk-AuthorChips"><div class="bk-chips">
<span class="bk-chip"><span class="bk-chip-label">Frank Herbert</span>
<button type="button" class="bk-chip-remove" title="Remove Frank Herbert">×</button></span>
<span class="bk-chip"><span class="bk-chip-label"><script>alert(1)</script></span>
<button type="button" class="bk-chip-remove" title="Remove <script>alert(1)</script>">×</button></span>
</div><input type="text" class="gwt-TextBox" placeholder="Add an author"></div>
AuthorChips after removing first chip: value=[<script>alert(1)</script>] events=[<script>alert(1)</script>]
AuthorChips after Enter (twice): value=[<script>alert(1)</script>, Brian Herbert] events=[<script>alert(1)</script>] [<script>alert(1)</script>, Brian Herbert]
AuthorChips after setValue(null,true): [] chips children=0
AuthorChips getValue returns a copy: true
Both the visible name and the title attribute were written through setInnerText and setTitle, so the browser shows the angle brackets as text.
Using setInnerHTML with the name would have run the script.
The events are the new lists, each fired once; setValue(null, true) empties the widget and fires once as well.
Composites of composites
The book’s chapter on composites ends by assembling a colour picker from two slider composites and a few ordinary widgets.
The same idea in the Bookshelf is the review box: a RatingWidget (a composite), a TextArea and a Button in a VerticalPanel, wrapped by one more composite that exposes a single value, a Review.
public class ReviewBox extends Composite implements HasValue<ReviewBox.Review> {
/** Immutable, with equals so that fireIfNotEqual works. */
public static final class Review {
private final int rating;
private final String text;
public Review(int rating, String text) {
this.rating = rating;
this.text = text;
}
public int getRating() { return rating; }
public String getText() { return text; }
@Override
public boolean equals(Object o) {
return o instanceof Review && ((Review) o).rating == rating && ((Review) o).text.equals(text);
}
@Override public int hashCode() { return 31 * rating + text.hashCode(); }
}
private final RatingWidget rating = new RatingWidget();
private final TextArea text = new TextArea();
private final Button save = new Button("Save review");
private Review value = new Review(0, "");
public ReviewBox() {
VerticalPanel panel = new VerticalPanel();
panel.add(rating);
panel.add(text);
panel.add(save);
initWidget(panel);
setStyleName("bk-ReviewBox");
save.addClickHandler(e -> setValue(new Review(rating.getValue(), text.getText()), true));
}
@Override public Review getValue() { return value; }
@Override public void setValue(Review v) { setValue(v, false); }
@Override
public void setValue(Review v, boolean fireEvents) {
Review old = value;
value = v == null ? new Review(0, "") : v;
rating.setValue(value.getRating());
text.setText(value.getText());
if (fireEvents) {
ValueChangeEvent.fireIfNotEqual(this, old, value);
}
}
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Review> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
}
The lab called setValue(new Review(4, "Great"), true), then clicked the Save button, whose value equals the current one:
ReviewBox events=4/Great getValue=4/Great
ReviewBox element tag=TABLE class=bk-ReviewBox
One event for two operations (the click on Save changed nothing), and the element of the composite is the table of the VerticalPanel.
Three consequences of composing composites:
-
Events reach the outer composite. A
ClickHandleradded to theReviewBoxwithaddDomHandler(handler, ClickEvent.getType())saw a click that was dispatched on the inner Save button, because the click bubbles to the shared element: the lab printedClickHandler on the composite sees a click on its inner Save button: 1.Composite.onBrowserEventfirst fires the handlers registered on the composite and then delegates to the wrapped widget. -
Each level adds a level of delegation, not of DOM. Three nested composites around one panel cost three Java objects and one element.
-
Expose one value, not the parts. Callers of
ReviewBoxseeReview; the rating widget, the text area and the button stay private, so you can replace the stars with a slider without touching any caller.
Testing a custom widget
A widget needs a browser, so a plain JUnit test cannot create it: Document.get() and the style and event code are JavaScript.
GWTTestCase (see Testing) runs the test compiled to JavaScript, by default under HtmlUnit, and was used for the rating widget with a dedicated test module that fixes the user.agent property to one value:
<module>
<inherits name="com.google.gwt.user.User"/>
<inherits name="com.google.gwt.junit.JUnit"/>
<inherits name="com.google.gwt.editor.Editor"/>
<source path="client/widget"/>
<set-property name="user.agent" value="safari"/>
</module>
public class RatingWidgetGwtTest extends GWTTestCase {
@Override
public String getModuleName() {
return "com.example.bookshelf.LabTest";
}
public void testClickingAStarFiresOneValueChange() {
RatingWidget rating = new RatingWidget();
RootPanel.get().add(rating);
final int[] events = {0};
rating.addValueChangeHandler(e -> events[0]++);
rating.getElement().getElementsByTagName("span").getItem(2)
.dispatchEvent(Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false));
assertEquals(Integer.valueOf(3), rating.getValue());
assertEquals(1, events[0]);
assertEquals("3", rating.getElement().getAttribute("aria-valuenow"));
}
public void testValueIsClamped() {
RatingWidget rating = new RatingWidget();
rating.setValue(99);
assertEquals(Integer.valueOf(RatingWidget.MAX), rating.getValue());
}
}
Source: GWT Developer Guide — Testing
Run with java -Dgwt.args="-runStyle HtmlUnit -sourceLevel 17" org.junit.runner.JUnitCore com.example.bookshelf.client.widget.RatingWidgetGwtTest the result was OK (2 tests).
Keep the logic that does not touch the DOM (value clamping, list handling) in plain classes that an ordinary JUnit test can cover; only the DOM-facing glue needs GWTTestCase.
The scratch Bookshelf of this section recorded that a GWTTestCase run with assertions on (the gwt:test goal enables them) failed in com.google.gwt.dev.Permutation.assertSameAnswers while the test module still had several permutations, and that a test module with user.agent and locale fixed to one value cured it; the module above does the same for user.agent.
Pitfalls
All messages were reproduced in the lab, compiled once with -checkAssertions and once without.
Rows that quote an AssertionError only appear in a build with assertions on; the same mistakes in a build without them show up as a late JavaScript error (the second row) or as no error at all, which is why a custom widget should be exercised at least once with assertions enabled.
| Symptom or message | Cause | Fix |
|---|---|---|
|
|
call |
|
the same two mistakes in a build without assertions |
as above; test with assertions on so that the clear message appears |
|
two calls in a constructor, or a subclass that calls |
one call; let a subclass use |
|
|
create the child before |
|
two |
one call, with the final root element |
|
|
override |
|
|
never call |
|
a |
override |
a |
|
call |
|
|
call |
|
an editor class has a no-argument method that returns a widget or editor |
make the helper return a plain value, or give it a parameter |
the stars lose their styling after |
|
use |
a ticking label keeps counting after it was removed |
timer or listener started in the constructor |
start in |
a change event fires for every |
|
|
a hostile name runs script in the page |
|
|
Where the book is outdated
GWT in Action (Hanson and Tacy, Manning, 2007; GWT 1.3/1.4) devotes three chapters to this page. The concepts stand; the code and a few facts do not.
| Book topic | Today | Notes |
|---|---|---|
Chapter 4: widgets as Java objects over DOM elements, new widgets by DOM manipulation and by extension |
unchanged in principle: a |
the DOM helpers differ: |
Chapter 4: |
replaced by handlers ( |
the listener interfaces and their |
Chapter 4: |
deprecated; use |
|
Chapter 5: panels and writing a new panel |
the panel family is the same; layout panels ( |
|
Chapter 7: |
|
the advice to always call |
Chapter 7: events from a composite by implementing |
implement |
|
Chapter 7: sliders made of images and a colour picker built from them |
the idea of composing composites is current (see above) |
a modern slider uses CSS and the |
Source: Manning — GWT in Action
What changed recently
-
GWT 1.1.0 (RC): the release notes announce that composites "must now call initWidget() in their constructors, rather than setWidget()", and
Composite.setWidgethas been deprecated since; it still exists in the 2.13.1 source. -
GWT 2.8.0: Java 8 syntax, so the lambdas used for handlers on this page compile with
-sourceLevel 8or later; the examples were built with-sourceLevel 17. -
GWT 2.12.0: the browser
unloadevent is deprecated, and with itWindow.addCloseHandlerandRootPanel.detachOnWindowClose; a widget should be released withremoveFromParent(), andonUnloadis the place to drop registrations (see DOM & Elemental2). -
GWT 2.9.0 to 2.13.1: the release notes do not mention
Composite,ComplexPanel,onLoador the attach lifecycle, so the code and the messages on this page are the 2.13.1 source behaviour, measured. -
org.gwtproject.*(the J2CL-ready modules such asgwt-domandgwt-event) has only release candidates on Maven Central, and thegwt-userwidget library is not part of it: custom widgets based onCompositeare GWT code that J2CL does not compile (see J2CL & the Future).