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.

Table 1. Ways to make a new widget in GWT 2.13
Strategy How Use it when Covered

Composite of existing widgets

extend Composite, build the parts, call initWidget

the new widget is an arrangement of existing ones (almost always)

this page

Widget from DOM elements

extend Widget, create elements with Document, call setElement, sink events

nothing existing renders the markup you need, or you need very few DOM nodes

this page

Widget from a template

a Composite whose initWidget receives the result of createAndBindUi

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 Widget

an existing JavaScript widget or web component must be reused

JsInterop and JSNI & Overlay Types

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.

classDiagram UIObject <|-- Widget Widget <|-- Composite Widget <|-- Panel Widget <|-- Chip Panel <|-- ComplexPanel ComplexPanel <|-- FlowPanel ComplexPanel <|-- ChipPanel Composite <|-- ResizeComposite Composite <|-- RatingWidget Composite <|-- AuthorChips Composite <|-- ReviewBox class Chip { DOM span and button sinkEvents ONCLICK } class ChipPanel { add(Widget) } class RatingWidget { HasValue of Integer HasEnabled Focusable } class AuthorChips { HasValue of List of String } class ReviewBox { HasValue of Review }

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.

flowchart TD RB["ReviewBox (Composite)"] -->|initWidget| VP["VerticalPanel"] VP --> RW["RatingWidget (Composite)"] VP --> TA["TextArea"] VP --> SB["Button: Save review"] RW -->|initWidget| FP["FocusPanel"] FP --> FL["FlowPanel"] FL --> S["five InlineLabel stars"] RB -. "same DOM element" .- VP RW -. "same DOM element" .- FP

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.

A composite of a CheckBox and a TextBox (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() and onBrowserEvent(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.

Style names on a composite that wraps a Button (output of the lab page)
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.

Resizing composites and lazy widgets

Two small relatives matter inside layout panels:

  • ResizeComposite extends Composite and implements RequiresResize; its initWidget asserts that the wrapped widget is itself a RequiresResize (the assertion message is "ResizeComposite requires that its wrapped widget implement RequiresResize") and its onResize() 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.

  • LazyPanel extends SimplePanel, is abstract, and builds its child only when it is first made visible: you implement createWidget() and the framework calls it from ensureWidget() or setVisible(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.

Table 2. Interfaces for custom widgets and what consumes them
Interface Contract What it buys you

HasText, HasHTML

getText/setText (HasHTML adds getHTML/setHTML)

widgets whose content is a caption or a text, so that generic code and UiBinder can set it

HasValue<T>

getValue, setValue(T), setValue(T, boolean fireEvents) and addValueChangeHandler; extends TakesValue<T> and HasValueChangeHandlers<T>

form fields; the Editor framework (TakesValueEditor.of(this)); generic code that reads or writes a value

HasValueChangeHandlers<T>

only addValueChangeHandler

a widget that reports a new value but does not accept one (the ReviewBox below uses it through HasValue)

HasClickHandlers, HasCloseHandlers<T>

addClickHandler / addCloseHandler

@UiHandler in UiBinder finds a handler by the addXxxHandler method, so a widget with the right method can be handled declaratively

HasEnabled

isEnabled/setEnabled

forms that disable everything with one loop

Focusable

getTabIndex, setTabIndex, setAccessKey, setFocus(boolean)

keyboard navigation; a composite delegates to the part that owns the tab stop. HasFocus is the older combination of Focusable with the listener-era SourcesFocusEvents and SourcesKeyboardEvents; new code adds addFocusHandler/addKeyDownHandler instead

HasName

getName/setName

form submission names

IsEditor<E>

asEditor() returns the editor for this widget, for example LeafValueEditor<T> from TakesValueEditor.of(this)

the Editor framework picks the widget up as a sub-editor (see the Editors page)

IsWidget

asWidget() returns the real widget; Widget implements it already

presenters and views that return IsWidget instead of Widget

HasWidgets, HasOneWidget, IndexedPanel

add/remove/clear/iterator; getWidget/setWidget; getWidget(int)/getWidgetCount/remove(int)

a panel: UiBinder children (@UiChild), layout, Panel.clear()

HasAttachHandlers

addAttachHandler; implemented by Widget

observers that need to know when a widget enters or leaves the document

HasVisibility, HasAnimation

isVisible/setVisible; isAnimationEnabled/setAnimationEnabled

ordinary opt-in interfaces for widgets that can be hidden or that animate

The interfaces carry conventions that the compiler does not check:

  • setValue(v) is setValue(v, false), and only the two-argument form with true fires a ValueChangeEvent. Code that fills a form must not trigger the handlers that react to user edits.

  • Fire with ValueChangeEvent.fireIfNotEqual(this, oldValue, newValue), not fire. The static method compares with equals (null-safe) and stays silent when nothing changed; this is why the value classes below implement equals.

  • Register handlers with addHandler(handler, ValueChangeEvent.getType()). Widget.addHandler is public final, creates the handler manager lazily, and returns the HandlerRegistration that callers use to unregister.

  • getValue of a collection returns a copy (AuthorChips does), or callers can change the widget’s state behind its back and no event fires.

  • Treat null as a value you handle, usually as "empty" (RatingWidget maps it to zero, EditableLabel to 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>.

EditableLabel.java
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.

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.

RatingWidget.java
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;
  }
}

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.

Output of the rating lab (Chrome 155, user.agent safari permutation)
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:

Rendered markup (the hidden input is added by FocusPanel in the safari permutation)
<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).

Stylesheet for the widgets of this page
.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 and rating.addValueChangeHandler(handler) observes user input.

  • UiBinder: <w:RatingWidget ui:field="rating"/> with xmlns: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 wrote rated 4 into a label.

  • Editors: a field named after a bean property, here RatingWidget rating in an Editor<BookReview>, is edited through asEditor(). driver.edit(review) pushed rating=4 and authors=[A. Author, B. Author] into the widgets; after the user changed them driver.flush() pulled rating=2 authors=[C. Author] back into the same bean instance.

The editor for a review (ReviewEditor.java), the fields are package-private on purpose
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(); }
}

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 from Document.get().createXxxElement(); the dom.client classes are typed overlays, so there are no wrapper objects (see DOM & Elemental2). The setElement(com.google.gwt.user.client.Element) overload is @Deprecated.

  • getElement() returns com.google.gwt.user.client.Element, a subclass of com.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 of Event (Event.ONCLICK), or register through addDomHandler(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 call super.onBrowserEvent(event), which dispatches to the handlers registered with addDomHandler. If you do not call super, those handlers silently stop working.

  • Put text in with setInnerText, which sets textContent. Reserve setInnerHTML for markup you built from constants, and use SafeHtml (see SafeHtml & XSS) when HTML must contain data.

Chip.java, a widget made of two DOM elements
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.

Table 3. The lifecycle methods of Widget (all protected, from the 2.13.1 source)
Method When it runs What to do there

onAttach()

sets the attached flag, installs the event listener, applies the pending sinkEvents bits, attaches children, then calls onLoad() and fires AttachEvent (isAttached() == true)

nothing: override onLoad; if you do override it, call super.onAttach()

onLoad()

after the widget and all its children are attached

start things that need the widget in the document: timers, global listeners, measuring the size

onUnload()

first thing in onDetach(), before anything is torn down

stop what onLoad started; the mirror image of onLoad

onDetach()

calls onUnload, fires AttachEvent (isAttached() == false), detaches children, removes the listener and clears the flag

nothing: override onUnload; if you do override it, call super.onDetach()

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.

Order of callbacks when a composite of composites is attached and removed
-- 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
sequenceDiagram participant R as RootPanel participant O as Outer composite participant I as Inner composite participant L as leaf widget R->>O: add(outer) O->>I: onAttach delegates down I->>L: attach children L-->>L: onLoad I-->>I: onLoad O-->>O: onLoad, then AttachEvent true R->>O: remove(outer) O-->>O: onUnload, then AttachEvent false O->>I: onDetach delegates down I-->>I: onUnload I->>L: detach children L-->>L: onUnload

The rules that follow from the measurement:

  • Attach runs from the leaves up, detach from the root down. When an outer onLoad runs, every inner widget is already attached and measurable; when an outer onUnload runs, the inner widgets are still intact.

  • Inside onUnload, isAttached() is still true. The flag is cleared at the very end of onDetach (in a finally block).

  • A composite’s isAttached() is the wrapped widget’s, as the source shows, and it is false for a composite that never called initWidget.

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

DueLabel.java
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.

Timers started in onLoad versus in the constructor (virtual time in headless Chrome)
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:

ChipPanel.java
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);
  }
}

What the class hierarchy supplies:

  • Panel implements HasWidgets.ForIsWidget (add(IsWidget), remove(IsWidget), clear(), iterator()) and provides the two final methods every panel uses to maintain the parent link, adopt(Widget) and orphan(Widget), plus doAttachChildren and doDetachChildren, which attach and detach all children through an AttachDetachException command so that one failing child does not strand the others.

  • ComplexPanel implements IndexedPanel.ForIsWidget (getWidget(int), getWidgetCount(), getWidgetIndex(Widget), remove(int)), keeps the children in a WidgetCollection, 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 calls adopt. insert(child, container, beforeIndex, domInsert) does the same at an index.

  • remove(Widget) of ComplexPanel returns false for 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: HorizontalPanel creates a table cell for each child and passes that cell to add(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

The authors of a book are an ordered list of names. The widget has to show them as chips, let the user add one with the Enter key, remove one with its button, and report changes like any HasValue. It is a composite of the custom ChipPanel and a TextBox, so it combines everything above: a composite, a custom panel and custom widgets.

AuthorChips.java
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).

Rendered chips and the events received (the second name is data, not markup)
<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">&lt;script&gt;alert(1)&lt;/script&gt;</span>
    <button type="button" class="bk-chip-remove" title="Remove &lt;script&gt;alert(1)&lt;/script&gt;">×</button></span>
</div><input type="text" class="gwt-TextBox" placeholder="Add an author"></div>
Console output of the author chips lab
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.

ReviewBox.java
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 ClickHandler added to the ReviewBox with addDomHandler(handler, ClickEvent.getType()) saw a click that was dispatched on the inner Save button, because the click bubbles to the shared element: the lab printed ClickHandler on the composite sees a click on its inner Save button: 1. Composite.onBrowserEvent first 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 ReviewBox see Review; 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:

LabTest.gwt.xml, the test module with a single permutation
<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>
RatingWidgetGwtTest.java
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());
  }
}

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.

Table 4. Mistakes in custom widgets, measured on GWT 2.13.1
Symptom or message Cause Fix

java.lang.AssertionError: This UIObject's element is not set; you may be missing a call to either Composite.initWidget() or UIObject.setElement()

RootPanel.add (or any getElement()) on a composite that never called initWidget, or on a widget that never called setElement

call initWidget / setElement in the constructor, before the widget is used

com.google.gwt.core.client.JavaScriptException: (TypeError) : Cannot read properties of undefined (reading '__gwt_resolve')

the same two mistakes in a build without assertions

as above; test with assertions on so that the clear message appears

java.lang.IllegalStateException: Composite.initWidget() may only be called once.

two calls in a constructor, or a subclass that calls initWidget after its superclass did

one call; let a subclass use getWidget() or a protected hook of the base class

java.lang.NullPointerException: widget cannot be null

initWidget(null), for example a field initialised after the call

create the child before initWidget

java.lang.AssertionError: Element may only be set once

two setElement calls (assertions on); without assertions nothing is reported and the second element wins

one call, with the final root element

java.lang.AssertionError: Failure of com.example.bookshelf.client.widget.RatingWidget to call super.onAttach()

onAttach overridden without super.onAttach() (the same text with onDetach for the other one); no error without assertions, and the widget is then broken silently

override onLoad/onUnload instead; if you must override onAttach/onDetach, call super

java.lang.IllegalStateException: Should only call onAttach when the widget is detached from the browser's document

onAttach called by hand on a widget that is already attached (this error is thrown in both builds)

never call onAttach/onDetach; add and remove the widget from a panel

java.lang.UnsupportedOperationException: This panel does not support no-arg add()

a Panel subclass that did not override add(Widget)

override add(Widget), delegating to add(child, container)

a ClickHandler added with addDomHandler never fires in a Widget subclass

onBrowserEvent overridden without super.onBrowserEvent(event)

call super first

onBrowserEvent never runs for clicks on a custom widget

sinkEvents(Event.ONCLICK) missing (measured: clicks=0), or the handler added with a low-level call that does not sink

call sinkEvents, or use addDomHandler

Could not find a getter for path ratingWidget in proxy type com.example.bookshelf.client.view.BookReview

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 setStyleName

setStyleName replaced the style of the wrapped widget (gwt-Button became bk-mine)

use addStyleName, setStylePrimaryName or setStyleDependentName

a ticking label keeps counting after it was removed

timer or listener started in the constructor

start in onLoad, stop in onUnload

a change event fires for every setValue

fire used instead of fireIfNotEqual, or setValue(v) firing events

setValue(v) calls setValue(v, false); fire only with a changed value

a hostile name runs script in the page

setInnerHTML/setHTML with data

setInnerText/setText, or SafeHtml

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.

Table 5. The book’s chapters on widgets, panels and composites against GWT 2.13.1
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 Widget owns one element (setElement) and can be extended

the DOM helpers differ: DOM.setStyleAttribute is @Deprecated (use getStyle() on the element), and widgets are built from com.google.gwt.dom.client

Chapter 4: ClickListener and friends (FocusListener, KeyboardListener)

replaced by handlers (addClickHandler, addFocusHandler, addKeyDownHandler), see the Events & Handlers page

the listener interfaces and their *ListenerCollection helpers are still in the 2.13.1 source but marked @Deprecated

Chapter 4: ImageBundle for icons

deprecated; use ClientBundle (see ClientBundle & Images)

ImageBundle.java is @Deprecated in 2.13.1

Chapter 5: panels and writing a new panel

the panel family is the same; layout panels (DockLayoutPanel, LayoutPanel, which implement RequiresResize and ProvidesResize) came later, next to the table-based DockPanel, HorizontalPanel and VerticalPanel, which are not deprecated

Panel, ComplexPanel, adopt and orphan work as the book describes

Chapter 7: Composite and initWidget (the book notes that early versions called it setWidget)

setWidget remains as a deprecated alias

the advice to always call initWidget is still the first rule

Chapter 7: events from a composite by implementing SourcesChangeEvents and a ChangeListenerCollection

implement HasValueChangeHandlers<T> (or HasValue<T>) and return addHandler(handler, ValueChangeEvent.getType())

SourcesChangeEvents, ChangeListener and ChangeListenerCollection are @Deprecated in 2.13.1

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 slider ARIA role (as RatingWidget does) rather than swapping images

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.setWidget has 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 8 or later; the examples were built with -sourceLevel 17.

  • GWT 2.12.0: the browser unload event is deprecated, and with it Window.addCloseHandler and RootPanel.detachOnWindowClose; a widget should be released with removeFromParent(), and onUnload is the place to drop registrations (see DOM & Elemental2).

  • GWT 2.9.0 to 2.13.1: the release notes do not mention Composite, ComplexPanel, onLoad or 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 as gwt-dom and gwt-event) has only release candidates on Maven Central, and the gwt-user widget library is not part of it: custom widgets based on Composite are GWT code that J2CL does not compile (see J2CL & the Future).