Editors

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 javax.validation 1.0 API that GWT still bundles, checked on 2026-10-09. The Editor framework is client knowledge: the editors, the driver and the generated glue are Java compiled to JavaScript, the edited beans are usually shared classes, and the only build requirement is one <inherits> line. The server never sees an editor; it receives the flushed bean through whatever transport the application uses. Every output block was produced by compiling the examples of a private copy of the Bookshelf client with the 2.13.1 compiler and running them as GWTTestCase tests in HtmlUnit (the generator error messages come from compiling small probe modules with the same compiler); the numbers and messages are quoted as they were printed.

The Editor framework answers one question: how do I copy the data of an object graph into a form, let the user change it, and copy it back, without writing a setText(book.getTitle()) and a book.setTitle(box.getText()) for every field? You describe the form as a class whose fields are named after the properties of the bean, you declare an empty driver interface, and the GWT compiler generates the code that walks both graphs. Nothing is looked up by reflection at run time, so the framework works in the browser, where there is none.

The idea in one example

The Bookshelf book form is the simplest editor there is. The class below is the real BookEditor of the scratch Bookshelf client.

public class BookEditor extends Composite implements Editor<Book> {
  interface Binder extends UiBinder<Widget, BookEditor> {}

  public interface Driver extends SimpleBeanEditorDriver<Book, BookEditor> {}

  public interface Listener {
    void onSave(Book edited);
  }

  private static final Binder BINDER = GWT.create(Binder.class);

  @UiField TextBox title;
  @UiField TextBox author;
  @UiField IntegerBox year;
  @UiField IntegerBox copies;
  @UiField @Editor.Ignore Button save;

  private final Driver driver = GWT.create(Driver.class);
  private final Listener listener;

  public BookEditor(Listener listener) {
    this.listener = listener;
    initWidget(BINDER.createAndBindUi(this));
    driver.initialize(this);
  }

  public void edit(Book book) { driver.edit(book); }

  @UiHandler("save")
  void onSave(ClickEvent e) { listener.onSave(driver.flush()); }
}

Four things make this work, and the rest of the page is about each of them:

  • The class implements Editor<Book>, the marker that says "my package-visible fields edit the properties of a `Book`".

  • The fields title, author, year and copies are named exactly like the getTitle(), getAuthor(), getYear() and getCopies() properties of Book, and their types (TextBox, IntegerBox) know how to take part (they implement IsEditor).

  • Driver is an interface with no methods; GWT.create(Driver.class) returns a class that GWT generated for this exact pair of types.

  • edit(book) copies the bean into the widgets and flush() copies the widgets back into the same bean and returns it.

The module needs the framework once:

<module>
  <inherits name="com.google.gwt.user.User" />
  <inherits name="com.google.gwt.editor.Editor" />
  <!-- ... -->
</module>

The classes live in gwt-user, so there is no new Maven dependency. Editor.gwt.xml itself inherits com.google.gwt.core.Core, com.google.gwt.event.Event and com.google.gwt.validation.Validation, adds the source paths client and ui/client, and registers the generator described next. The last inherit is the reason the framework has a (deprecated) dependency on bean validation; see Validation and error display.

Vocabulary

Table 1. Terms of the Editor framework
Term Meaning

Bean

Any object with strongly typed getFoo() getters and optional setFoo(...) setters: a POJO, a JavaScript overlay type, an RPC object or a RequestFactory proxy. It does not implement any GWT interface.

Editor

An object that edits zero or more properties of a bean. It may be composed of sub-editors, and most editors are widgets, but the framework does not require it: a "headless" editor such as SimpleEditor changes only data.

Driver

The top-level controller that attaches a bean to the root editor and descends into the hierarchy. SimpleBeanEditorDriver and RequestFactoryEditorDriver are the two the SDK ships.

Delegate

The peer object (EditorDelegate) the framework creates for every editor. It gives the editor its path, lets it report errors and set its dirty flag.

Adapter

A provided class with a canned behaviour, for example ListEditor, OptionalFieldEditor, ValueBoxEditor.

Path

The dotted name of a property from the root bean, such as author.name or reviews[1].rating. Paths decide which editor receives which value and where an error is shown.

The edit and flush cycle

sequenceDiagram participant V as Bookshelf form participant D as SimpleBeanEditorDriver participant E as Editors and sub-editors participant B as Book bean V->>D: initialize(editor) V->>D: edit(book) D->>E: setValue(...) for every path, depth first E-->>V: widgets show the data V->>E: the user types V->>D: flush() D->>E: getValue() and flush(), depth first E->>B: setTitle(), setYear() ... D-->>V: the same Book instance V->>D: hasErrors() / getErrors()

The steps, as the Developer Guide lists them:

  • Create and initialise the editors (with UiBinder, createAndBindUi is the moment).

  • Create the driver with GWT.create and call initialize(editor). A driver is stateful, so one driver instance belongs to one editor hierarchy.

  • Call edit(bean) to push the data into the editors.

  • Let the user work.

  • Call flush() to copy the editor state into the bean, then ask hasErrors() and getErrors() whether an editor reported a problem.

Table 2. The main methods of EditorDriver and SimpleBeanEditorDriver
Method Behaviour

initialize(E editor)

Binds the driver to the root editor. Must come first.

edit(T bean)

Pushes the bean into the editors, clears the dirty state and remembers the bean. Returns nothing.

T flush()

Copies the editors into the bean passed to edit and returns that same instance. The RequestFactoryEditorDriver returns the RequestContext instead.

hasErrors(), getErrors()

Whether the last flush() (or setConstraintViolations) left unconsumed EditorError objects, and the list of them.

isDirty()

True if any editor differs from the value it was given by the last edit. flush() does not clear it (see the measurements below).

setConstraintViolations(Iterable<ConstraintViolation<?>>)

Maps javax.validation violations onto editors; see Validation and error display.

accept(EditorVisitor)

Walks the hierarchy with a visitor. The framework uses it for its own initialisation, flushing and error collection; EditorHierarchyPrinter is one that prints the tree.

The measured behaviour of the cycle, with the Bookshelf detail editor of the next sections (a book with a title, a subtitle, a year, an author and two reviews):

dirty after edit: false
dirty after typing: true
flush returns the edited instance: true, title=Effective Java, 3rd ed., hasErrors=false
dirty after flush: true
edit before initialize: java.lang.IllegalStateException: Must call initialize() first
flush before edit: java.lang.IllegalStateException: Must call edit() first

Three consequences follow:

  • flush() mutates the bean you gave to edit. If the user may cancel, give the driver a copy and discard it on cancel.

  • isDirty() is the simple "unsaved changes" test, because flush() does not reset it: it stays correct while you validate, fail and let the user fix the form.

  • To use the editors for a read-only view, call edit and never call flush.

How the generator finds the sub-editors

The driver interface is implemented by a generator, one of the deferred-binding generators described on the Deferred Binding & Generators page. The module registers it for every type assignable to SimpleBeanEditorDriver:

<generate-with class="com.google.gwt.editor.rebind.SimpleBeanEditorDriverGenerator">
  <when-type-assignable class="com.google.gwt.editor.client.SimpleBeanEditorDriver" />
</generate-with>

At compile time the generator reads the structure of your editor class and of the bean, builds a model of the whole tree, and writes a class named after the driver (for a top-level BookDetailDriver it is BookDetailDriverImpl). Because the model is built before the permutations are generated, a mistake in the structure is a compile error, not a run-time surprise.

flowchart LR A["Driver interface
SimpleBeanEditorDriver of Book and BookEditor"] --> G["SimpleBeanEditorDriverGenerator
reads editor and bean types"] G --> M["Editor model:
paths, types, accessors"] M --> I["Generated DriverImpl
and delegates"] I --> J["Compiled JavaScript
no reflection"] G -. "structure error" .-> X["Compile error:
Failed to resolve the driver
via deferred binding"]

An editor exposes its sub-editors in one of three ways:

  • A field with at least package visibility whose name is the property name, or the property name followed by Editor.

  • A no-argument method with at least package visibility and the same naming rule. This allows an editor that is an interface.

  • The @Path annotation on the field or the method, with a dotted property path or a name that bypasses the naming rule.

@Ignore removes a field or method from consideration, and a sub-editor that is null is skipped. Both annotations are nested in Editor, so they are written @Editor.Path and @Editor.Ignore, or simply @Path and @Ignore inside a class that implements Editor.

Here is the complete detail editor of the Bookshelf, using all of them.

public class BookDetailEditor extends Composite
    implements Editor<BookDetail>, HasEditorErrors<BookDetail> {
  // property name: edits getTitle()/setTitle()
  TextBox title = new TextBox();
  // property name + "Editor": edits getSubtitle()/setSubtitle()
  TextBox subtitleEditor = new TextBox();
  IntegerBox year = new IntegerBox();
  // a sub-editor for a nested bean
  AuthorEditor author = new AuthorEditor();
  // @Path reaches into the same nested bean with a dotted path
  @Path("author.name") Label authorLine = new Label();
  // nullable property: the sub-editor is attached only while the value is non-null
  OptionalFieldEditor<Publisher, PublisherEditor> publisher = OptionalFieldEditor.of(new PublisherEditor());
  // IsEditor widget wrapping a ListEditor
  ReviewListEditor reviews = new ReviewListEditor();
  // looks like a property editor but is deliberately not part of the edited bean
  @Ignore TextBox searchBox = new TextBox();

  final List<String> shown = new ArrayList<>();

  // (constructor adds the widgets to a FlowPanel)

  @Override
  public void showErrors(List<EditorError> errors) {
    shown.clear();
    for (EditorError error : errors) {
      shown.add(error.getAbsolutePath() + " -> " + error.getMessage());
    }
  }
}

public interface BookDetailDriver extends SimpleBeanEditorDriver<BookDetail, BookDetailEditor> {}

The generator’s model of that class, printed with EditorHierarchyPrinter.toString(driver) (trimmed to the paths):

 com.example.bookshelf.client.editors.BookDetail
 com.example.bookshelf.client.editors.BookDetailEditor
  title                 java.lang.String   ValueBoxEditor
  subtitle              java.lang.String   ValueBoxEditor
  year                  java.lang.Integer  ValueBoxEditor
  author                Author             AuthorEditor
    author.name         java.lang.String   ValueBoxEditor
    author.country      java.lang.String   ValueBoxEditor
  author.name           java.lang.String   HasTextEditor
  publisher             Publisher          OptionalFieldEditor
  reviews               java.util.List     ListEditor
    reviews[0]          Review             ReviewEditor
      reviews[0].reviewer  java.lang.String   ValueBoxEditor
      reviews[0].rating    java.lang.Integer  RatingEditor
      reviews[0].text      java.lang.String   ValueBoxEditor
    reviews[1]          Review             ReviewEditor
      ...

The table is a re-formatted extract: the real output prints each path, edited type and editor class on separate indented lines. It also prints a last line per node that always reads ValueAwareEditor, even for leaf editors; the source builds that string with a misplaced conditional, so ignore that line. Note that driver.toString() calls this printer only when GWT.isProdMode() is false; under HtmlUnit and in Super Dev Mode the code runs as JavaScript, the call returns the plain BookDetailDriverImpl@5d, and you have to call EditorHierarchyPrinter.toString(driver) yourself.

Table 3. How the fields of BookDetailEditor were matched
Field Property path Reason

title

title

field name equals the property

subtitleEditor

subtitle

the Editor suffix is removed

author, with AuthorEditor.name and .country

author, author.name, author.country

a sub-editor is an editor of the nested bean; the generator descends into it

authorLine

author.name

@Path("author.name")

publisher

publisher

OptionalFieldEditor handles the property that may be null

reviews

reviews, reviews[n], …​

ReviewListEditor is an IsEditor of a ListEditor; its elements are indexed

searchBox

none

@Ignore

The Developer Guide also shows the same idea for several editors on the same sub-bean (@Editor.Path("state") on two different editors of a BagOfState), which is how a bean with many properties is split into smaller editors; every editor edits only the subset it declares.

A measured detail about duplicates: when two editors map to the same path (author.name above is edited by a TextBox and by authorLine), both are asked for their value on flush() and the one visited later wins. With the output of the run, authorLine.setText("changed in the Label") followed by flush() left book.getAuthor().getName() equal to changed in the Label. Give every property one writer.

The Editor contract and its sub-types

All the interfaces are in com.google.gwt.editor.client; the adapters are in .adapters, and the widget-specific ones in com.google.gwt.editor.ui.client.

classDiagram class Editor class IsEditor class LeafValueEditor class HasEditorDelegate class ValueAwareEditor class CompositeEditor class HasEditorErrors class ListEditor class OptionalFieldEditor class ValueBoxEditor class EditorDriver class SimpleBeanEditorDriver class RequestFactoryEditorDriver Editor <|-- LeafValueEditor Editor <|-- HasEditorDelegate Editor <|-- HasEditorErrors HasEditorDelegate <|-- ValueAwareEditor ValueAwareEditor <|-- CompositeEditor CompositeEditor <|.. ListEditor CompositeEditor <|.. OptionalFieldEditor LeafValueEditor <|.. OptionalFieldEditor LeafValueEditor <|.. ValueBoxEditor HasEditorDelegate <|.. ValueBoxEditor EditorDriver <|-- SimpleBeanEditorDriver EditorDriver <|-- RequestFactoryEditorDriver IsEditor ..> Editor : asEditor

LeafValueEditor also extends TakesValue, which is why every TakesValue widget is almost a leaf editor already.

Table 4. Editor types in GWT 2.13.1
Type Use it when What the framework calls

Editor<T>

The class only exposes sub-editors and has no behaviour that depends on the value.

Nothing on the class itself: the generator reads its fields and accessors.

IsEditor<E>

A view wants to take part without implementing the editor contract, typically because it reuses the editor behaviour of something inside it.

asEditor(), once, to get the real editor E.

LeafValueEditor<T>

A non-object, immutable or opaque value that the framework must not descend into: a string in a text box, a boolean in a check box.

setValue(T) on edit, getValue() on flush.

HasEditorDelegate<T>

The editor needs its EditorDelegate to report errors, set the dirty flag or learn its path.

setDelegate(delegate), before any value is set.

ValueAwareEditor<T>

The behaviour depends on the value being edited, or the editor wants an explicit "the cycle is over" notice.

setDelegate, setValue(T) and flush(); also onPropertyChange(String...) if the driver supports subscriptions.

CompositeEditor<T, C, E>

An unknown number of homogeneous sub-editors is attached at run time (a list, an optional bean).

setEditorChain, createEditorForTraversal(), getPathElement(E), plus the ValueAwareEditor calls.

HasEditorErrors<T>

The editor wants to see, display or consume the errors of the editors below it.

showErrors(List<EditorError>)

The Developer Guide says the method that hands over the delegate is setEditorDelegate(). In GWT 2.13.1 it is HasEditorDelegate.setDelegate(EditorDelegate<T>); the guide text predates a rename, and a class that implements setEditorDelegate does not compile against the interface.

LeafValueEditor and recording an error

A leaf editor is a value in, a value out. The Bookshelf rating is a leaf that also keeps its delegate, so that it can say what is wrong with the value.

public class RatingEditor extends Composite
    implements LeafValueEditor<Integer>, HasEditorDelegate<Integer> {
  private final IntegerBox box = new IntegerBox();
  private EditorDelegate<Integer> delegate;

  public RatingEditor() {
    initWidget(box);
  }

  @Override
  public void setDelegate(EditorDelegate<Integer> delegate) {
    this.delegate = delegate;
  }

  @Override
  public void setValue(Integer value) {
    box.setValue(value);
  }

  @Override
  public Integer getValue() {
    Integer value = box.getValue();
    if (value == null || value < 1 || value > 5) {
      // Reported to the nearest HasEditorErrors ancestor, or to the driver
      delegate.recordError("Rating must be between 1 and 5", value, null);
    }
    return value;
  }
}

EditorDelegate offers four services:

Table 5. EditorDelegate (2.13.1)
Method Purpose

getPath()

The path of the editor in the attached hierarchy, for example reviews[1].rating.

recordError(String message, Object value, Object userData)

Report an input problem. The error goes to the nearest super-editor that implements HasEditorErrors, and eventually to the driver. value may be null to mean "the value now in the editor"; userData is returned by EditorError.getUserData(). Call it from getValue() or flush().

setDirty(boolean)

Set the dirty flag. For a leaf editor the flag is computed from an equals comparison between getValue() and the value last given to setValue, but setDirty(true) can force it.

subscribe()

Ask for notifications that the edited object changed elsewhere. It returns a HandlerRegistration, or null when the driver has no subscriptions. The SimpleBeanEditorDriver always returns null.

ValueAwareEditor

A ValueAwareEditor is told about the whole object, so it can change its own shape. The Bookshelf publisher editor keeps a trace of the calls it receives.

public class PublisherEditor extends Composite implements ValueAwareEditor<Publisher> {
  TextBox name = new TextBox();
  TextBox city = new TextBox();
  final List<String> trace = new ArrayList<>();
  private EditorDelegate<Publisher> delegate;

  @Override
  public void setDelegate(EditorDelegate<Publisher> delegate) {
    this.delegate = delegate;
    trace.add("setDelegate");
  }

  @Override
  public void setValue(Publisher value) {
    trace.add("setValue " + (value == null ? "null" : value.getName()) + " at path '" + delegate.getPath() + "'");
    city.setEnabled(value != null); // behaviour that depends on the value being edited
  }

  @Override
  public void flush() {
    trace.add("flush");
  }

  @Override
  public void onPropertyChange(String... paths) {
    trace.add("onPropertyChange");
  }
}

The trace for edit(book) with a publisher, then a flush(), then edit(book) after book.setPublisher(null):

trace: [setDelegate, setValue Addison-Wesley at path 'publisher']
trace after flush: [flush] publisher=Pearson
trace after edit(publisher=null): [], flush -> null

The order is the one the Developer Guide documents: the delegate first, then the value, and flush() last. flush() is called depth first by the driver, so an editor does not flush its sub-editors; an editor that mutates its peer object directly should do so only in flush(), which is what lets a workflow be cancelled. The empty trace after the null shows the OptionalFieldEditor at work: the publisher sub-editor was detached and not told anything. onPropertyChange is never called by SimpleBeanEditorDriver, which has no subscription support.

HasEditorErrors

HasEditorErrors<T> has one method, showErrors(List<EditorError>). The driver calls it, possibly with an empty list to clear a previous state. Errors that the method does not consume continue to the next ancestor and finally to driver.getErrors().

Table 6. EditorError (2.13.1)
Method Value

getAbsolutePath()

Path from the root bean, reviews[1].rating.

getPath()

Path relative to the editor that receives the error; an empty string if the error concerns that editor itself.

getEditor()

The editor that holds the invalid value.

getMessage(), getValue(), getUserData()

The message, the offending value and the object given to recordError or the original ConstraintViolation.

isConsumed(), setConsumed(boolean)

Whether the error stops here.

An invalid year typed into the IntegerBox and a rating of 9, then flush():

hasErrors=true year after failed parse=2018
driver error: abs=year path=year msg=Bad value (abc) value=abc editor=ValueBoxEditor userData=java.text.ParseException: abc
driver error: abs=reviews[1].rating path=reviews[1].rating msg=Rating must be between 1 and 5 value=9 editor=RatingEditor userData=null
showErrors received: [year -> Bad value (abc), reviews[1].rating -> Rating must be between 1 and 5]

Three details are visible there. The text box itself reports unparsable input: ValueBoxEditor calls getValueOrThrow() and, on a ParseException, records Bad value ( followed by the text and ), with the exception as user data; the bean keeps its old year. showErrors of an ancestor sees the errors of its descendants. And because the method did not consume anything, the driver reports both.

When showErrors consumes the review errors (error.setConsumed(true) for paths starting with reviews), the same run gives:

showErrors received [year -> Bad value (abc), reviews[0].rating -> Rating must be between 1 and 5]; driver reports 1:
  year

IsEditor versus Editor

IsEditor<E> is for a view that wants to reuse editor behaviour that lives in something it contains. It is legal for a class to implement both; the editor returned by asEditor() is then a co-editor of the view. The Developer Guide’s rule of thumb is: if the widget has several sub-editors in a simple, static hierarchy, implement Editor; if it reuses the editor behaviour of a type it wraps, implement IsEditor.

public class FramedLabel extends Composite implements IsEditor<LeafValueEditor<String>> {
  private final Label wrapped = new Label();

  public FramedLabel() {
    FlowPanel frame = new FlowPanel();
    frame.add(new InlineLabel("["));
    frame.add(wrapped);
    frame.add(new InlineLabel("]"));
    initWidget(frame);
  }

  @Override
  public LeafValueEditor<String> asEditor() {
    return wrapped.asEditor();
  }
}

An editor can also be an interface, with the sub-editors exposed as no-argument methods; the implementation holds the widgets. This compiled and ran in the test; the values were title=Effective Java, subtitle(FramedLabel)=Third Edition and authorName=Joshua Bloch:

interface TitleView extends Editor<BookDetail> {
  TextBox title();
  FramedLabel subtitleEditor();
  @Path("author.name") Label authorName();
}
interface TitleDriver extends SimpleBeanEditorDriver<BookDetail, TitleView> {}

CompositeEditor

CompositeEditor<T, C, E extends Editor<? super C>> edits a variable number of homogeneous sub-editors. You rarely implement it; ListEditor and OptionalFieldEditor are the two provided ones. The three methods you would implement are:

  • createEditorForTraversal() returns a canonical sub-editor that the generator uses to compute all possible paths, so that an empty collection still has a structure to examine.

  • getPathElement(E) returns the path fragment of a sub-editor, such as [2] for the third element of a list.

  • setEditorChain(EditorChain<C, E>) gives the editor an object with attach(C object, E subEditor), detach(E subEditor) and getValue(E subEditor). A sub-editor attached to the chain is flushed as if it were a static sub-editor.

Provided adapters and widgets

Table 7. Adapters shipped in gwt-user 2.13.1
Adapter Package Purpose

ListEditor<T, E>

editor.client.adapters

Keeps a List<T> in step with a list of sub-editors that an EditorSource creates.

HasDataEditor<T>

editor.client.adapters

A ListEditor whose "editors" are leaf values pushed into a cell widget (HasData), so a CellTable can display a list property.

OptionalFieldEditor<T, E>

editor.client.adapters

A composite and leaf editor for a bean property that can be null or reset.

SimpleEditor<T>

editor.client.adapters

A headless leaf editor that only holds a value (SimpleEditor.of(), SimpleEditor.of(value)).

TakesValueEditor<T>

editor.client.adapters

Adapts a TakesValue<T> to a LeafValueEditor<T> (TakesValueEditor.of(peer)).

HasTextEditor

editor.ui.client.adapters

Adapts HasText to LeafValueEditor<String>. New widgets should prefer TakesValue<String>.

ValueBoxEditor<T>

editor.ui.client.adapters

Adapts a ValueBoxBase<T>; a ParseException from getValueOrThrow() becomes an EditorError.

ValueBoxEditorDecorator<T>

editor.ui.client

A UiBinder-friendly decorator that shows the error of the wrapped box. It implements IsEditor<ValueBoxEditor<T>> and HasEditorErrors<T>.

Most of the widgets you already use implement IsEditor, which is why they can be used as fields without any adapter:

Table 8. Widgets that are editors (2.13.1)
Widget asEditor() returns

TextBox, TextArea, PasswordTextBox, IntegerBox, DoubleBox (all ValueBoxBase)

ValueBoxEditor<T>

Label, Hidden, SuggestBox

LeafValueEditor<String>

ValueLabel

LeafValueEditor<T>

CheckBox, RadioButton, SimpleCheckBox, ToggleButton

LeafValueEditor<Boolean>

ValueListBox

TakesValueEditor<T>

ValuePicker

LeafValueEditor<T>

DateBox, DatePicker

LeafValueEditor<Date>

CellWidget

LeafValueEditor<C>

The plain ListBox is not in the table: it is not an editor, so use ValueListBox with a Renderer for a drop-down bound to a property. A widget that is an IsEditor and a field of your editor is always treated as a property editor, even when you meant it as a plain label (see the pitfalls).

Three adapters in action, from one Editor<BookDetail> (the label is @Ignore`d because a `Label is an IsEditor too):

static class MiniEditor implements Editor<BookDetail> {
  @Ignore final Label shown = new Label();
  SimpleEditor<String> title = SimpleEditor.of();            // headless: just holds the value
  HasTextEditor subtitleEditor = HasTextEditor.of(shown);    // adapts HasText to LeafValueEditor<String>
  ValueListBox<Integer> year = new ValueListBox<>(new AbstractRenderer<Integer>() {
    @Override
    public String render(Integer object) { return object == null ? "" : String.valueOf(object); }
  });
}
SimpleEditor value=Effective Java, HasTextEditor label=Third Edition, ValueListBox=2018
after flush: title=Effective Java (3e) subtitle=Changed year=2001

ListEditor

A ListEditor keeps a List<T> property in sync with a list of sub-editors. You supply an EditorSource, which creates, positions and disposes the sub-editors (usually widgets). Wrapping the editor in an IsEditor widget avoids writing a delegate for each of its methods.

public class ReviewListEditor extends Composite
    implements IsEditor<ListEditor<Review, ReviewEditor>> {
  private final FlowPanel panel = new FlowPanel();
  private final ListEditor<Review, ReviewEditor> editor = ListEditor.of(new ReviewEditorSource());

  public ReviewListEditor() {
    initWidget(panel);
  }

  @Override
  public ListEditor<Review, ReviewEditor> asEditor() {
    return editor;
  }

  /** Creates one ReviewEditor per list element and keeps the panel in step with the list. */
  private class ReviewEditorSource extends EditorSource<ReviewEditor> {
    @Override
    public ReviewEditor create(int index) {
      ReviewEditor sub = new ReviewEditor();
      panel.insert(sub, index);
      return sub;
    }

    @Override
    public void dispose(ReviewEditor sub) {
      sub.removeFromParent();
    }

    @Override
    public void setIndex(ReviewEditor sub, int index) {
      panel.insert(sub, index);
    }
  }
}

ListEditor.getList() returns a wrapper that you mutate to add or remove rows, and getEditors() returns an unmodifiable list of the sub-editors. The measured run on a book with two reviews, Ana and Luis:

editors=2 widgets in panel=2
after add+remove: editors=2 list=2 first=Ana same list object=true
after flush: Luis (edited), Marta
error reviews[2].rating -> Rating must be between 1 and 5

What the numbers show, confirmed by ListEditorWrapper in the source:

  • getList() is a working copy. add and remove immediately create or dispose sub-editors (editors=2 after adding Marta and removing Ana), but the bean’s own list still has Ana first; the bean’s list is cleared and refilled only by flush(), in the same list instance.

  • A sub-editor that is added later than the others gets its path from its position at the moment it was attached. After the remove(0) the Marta editor is second but its error was reported as reviews[2], and in a second run the surviving Luis editor reported reviews[1] after Ana was removed. Do not use a path of a list element as a lasting key after a structural change; match errors by getEditor() or getUserData().

  • EditorSource.create(int count, int index), createEditorForTraversal() (which creates and disposes one editor), dispose and setIndex have default implementations, so the minimum is create(int); override dispose to detach the widget and setIndex to move it.

HasDataEditor.of(hasData) plays the same role for a cell widget: the list property becomes the rows of a CellTable or DataGrid, each row a leaf value (see the Cell Widgets page).

OptionalFieldEditor

Some properties are null until the user decides to fill them in. OptionalFieldEditor.of(subEditor) attaches the sub-editor while the value is non-null and detaches it when it is null; it is also a LeafValueEditor, so the parent reads and writes the property through it.

Table 9. OptionalFieldEditor behaviour with the Bookshelf publisher
Action Result (measured)

edit(book) with book.getPublisher() == null, then flush()

publisher stays null; the sub-editor receives nothing

editor.publisher.setValue(new Publisher("Addison-Wesley", "Boston")), set the city to Hoboken, flush()

book.getPublisher() is Addison-Wesley/Hoboken

editor.publisher.setValue(null), flush()

book.getPublisher() is null again

The same holds for a nested bean without the adapter, with a trap: when author is null and the form has an AuthorEditor, the framework skips the sub-editor; the author widgets stay empty, whatever the user types is lost at flush(), and getAuthor() is still null with hasErrors() false. Create the nested bean before edit, or use OptionalFieldEditor.

Validation and error display

The framework does not validate anything itself; it only displays what other code finds. There are three sources of errors, and they all arrive as EditorError objects:

  • an editor calls delegate.recordError(...) (the ValueBoxEditor for a parse error, the rating above for a range);

  • a javax.validation.Validator produces ConstraintViolation objects and you give them to driver.setConstraintViolations(...);

  • a RequestFactory server returns violations (see RequestFactory integration).

Constraint violations

setConstraintViolations(Iterable<ConstraintViolation<?>>) converts each violation to an error whose getUserData() is the original violation, delivers it to the editor at the violation’s property path, and returns whether unconsumed errors remain. The bean is validated with the client-side Bean Validation of GWT (the Validation page covers the setup). The Bookshelf beans carry the constraints:

public class BookDetail {
  @NotNull
  @Size(min = 3, message = "Title needs at least 3 characters")
  private String title;
  @Min(value = 1450, message = "Year must be 1450 or later")
  @Max(2100)
  private int year;
  @Valid
  private Author author; // Author.name: @NotNull @Size(min = 1, message = "Author name is required")
  // ...
}

and the save button of the form is this workflow, which compiled and ran:

public boolean save() {
  BookDetail book = driver.flush();
  if (driver.hasErrors()) {
    return false;                                   // a widget could not parse its input
  }
  Set<ConstraintViolation<BookDetail>> violations = validator.validate(book);
  if (!violations.isEmpty()) {
    // the driver takes Iterable<ConstraintViolation<?>>, so copy into a wildcard set
    driver.setConstraintViolations(new HashSet<ConstraintViolation<?>>(violations));
    return false;
  }
  sender.accept(book);
  return true;
}

The four cases of the run:

1) title 'Hi': save=false sent=0 errors=1 first=title -> Title needs at least 3 characters
2) year 'x': save=false sent=0 errors=year -> Bad value (x)
3) valid: save=true sent=1 errors=0
4) server violation: errors=author.name -> Unknown author | HasEditorErrors saw [author.name -> Unknown author]

The server-side case uses a small adapter, because the framework reads only the root bean, the path text and the message:

public class ServerViolation implements ConstraintViolation<BookDetail> {
  private final BookDetail root;
  private final String path;
  private final String message;

  public ServerViolation(BookDetail root, String path, String message) {
    this.root = root;
    this.path = path;
    this.message = message;
  }

  @Override public BookDetail getRootBean() { return root; }
  @Override public String getMessage() { return message; }
  @Override public Path getPropertyPath() {
    return new Path() {
      @Override public Iterator<Node> iterator() { throw new UnsupportedOperationException(); }
      @Override public String toString() { return path; }
    };
  }
  @Override public String getMessageTemplate() { return message; }
  @Override public Class<BookDetail> getRootBeanClass() { return BookDetail.class; }
  @Override public Object getLeafBean() { return root; }
  @Override public Object getInvalidValue() { return null; }
  @Override public ConstraintDescriptor<?> getConstraintDescriptor() { return null; }
}

The detailed run with three violations (title too short, year 1200, author name empty) printed:

violations=3
  title | Title needs at least 3 characters | invalid=ab
  author.name | Author name is required | invalid=
  year | Year must be 1450 or later | invalid=1200
setConstraintViolations returned true, driver.hasErrors=true
driver error: title -> Title needs at least 3 characters editor=ValueBoxEditor userData=true
driver error: year -> Year must be 1450 or later editor=ValueBoxEditor userData=true
driver error: author.name -> Author name is required editor=ValueBoxEditor userData=true
HasEditorErrors on BookDetailEditor received: [title -> ..., year -> ..., author.name -> ...]

Rules the measurements and the source (SimpleViolation, BaseEditorDriver) establish:

  • The violation is matched to editors by the identity of its root bean, then by its property path string. Violations computed on a copy of the bean are ignored: setConstraintViolations returned false and left no errors for violations of another instance. Validate the object returned by flush().

  • The argument type is Iterable<ConstraintViolation<?>>. A Set<ConstraintViolation<BookDetail>> does not convert; copy it into a Set<ConstraintViolation<?>> as above.

  • An editor must exist for the path. A violation on a property that no editor edits has nowhere to go.

  • To show errors computed somewhere else, such as a server that validated the bean with Jakarta Validation and returned property paths and messages, implement ConstraintViolation with a root bean, a property path whose toString() is the path and a message. The framework reads only getRootBean(), getPropertyPath().toString() and getMessage(); the ServerViolation class of the example returned Unknown author for author.name that way (case 4 above). Keep the server authoritative: the client check is a convenience, and the server must validate again.

Showing errors next to the field

ValueBoxEditorDecorator wraps a text box with an error label and is written to be used in UiBinder through the valuebox child element:

<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
             xmlns:g="urn:import:com.google.gwt.user.client.ui"
             xmlns:e="urn:import:com.google.gwt.editor.ui.client">
  <g:HTMLPanel>
    <e:ValueBoxEditorDecorator ui:field="title"><e:valuebox><g:TextBox /></e:valuebox></e:ValueBoxEditorDecorator>
    <e:ValueBoxEditorDecorator ui:field="author"><e:valuebox><g:TextBox /></e:valuebox></e:ValueBoxEditorDecorator>
    <e:ValueBoxEditorDecorator ui:field="year"><e:valuebox><g:IntegerBox /></e:valuebox></e:ValueBoxEditorDecorator>
    <e:ValueBoxEditorDecorator ui:field="copies"><e:valuebox><g:IntegerBox /></e:valuebox></e:ValueBoxEditorDecorator>
  </g:HTMLPanel>
</ui:UiBinder>
public class BookFormEditor extends Composite implements Editor<Book> {
  interface Binder extends UiBinder<Widget, BookFormEditor> {}

  public interface Driver extends SimpleBeanEditorDriver<Book, BookFormEditor> {}

  private static final Binder BINDER = GWT.create(Binder.class);

  @UiField ValueBoxEditorDecorator<String> title;
  @UiField ValueBoxEditorDecorator<String> author;
  @UiField ValueBoxEditorDecorator<Integer> year;
  @UiField ValueBoxEditorDecorator<Integer> copies;

  public BookFormEditor() {
    initWidget(BINDER.createAndBindUi(this));
  }
}

The decorator is an IsEditor (its editor is the one of the contained box) and a HasEditorErrors that displays the messages of its own editor. Rendered, with the year box set to abc and flush() called:

<!-- before -->
<div class="PPTOIHC-a-b"></div> <div class="PPTOIHC-a-a"><input type="text" value="2,018"></div>
<!-- after flush with "abc" -->
<div class="PPTOIHC-a-b" style="display: inline-block;">Bad value (abc)</div> <div class="PPTOIHC-a-a"><input type="text" value="abc"></div>

The title decorator beside it stayed display: none with an empty label. The class names are obfuscated and differ between builds; style them from the decorator’s ui.xml or with your own wrapper if you need a look. The decorator writes the message with setInnerText, not as HTML, so a message that contains markup is shown as text.

The same output shows a formatting trap: an IntegerBox renders 2018 as 2,018, because its renderer is the locale’s decimal format. It parses 2,018 back to 2018, so nothing is lost, but a year or an identifier does not look right. Use a TextBox with a validated text for such fields, or your own ValueBox subclass whose Renderer formats without grouping.

Validation in GWT is deprecated and needs one flag on current JDKs

Two facts about the validation module that the Editor module inherits:

  • The 2.8.0 release notes state: "Deprecate JSR 303 Bean Validation support (javax.validation, c.g.g.validation). The code hasn’t been maintained for years." It is javax.validation 1.0, not jakarta.validation, and it needs Hibernate Validator 4.1.0.Final classes and sources on the compile classpath (see the Validation page). The Editor framework itself does not need it: recordError and your own ConstraintViolation implementation work without a Bean Validation provider.

  • The 2.13.0 release notes: "jaxb and xml-apis are now optional when using GWT’s javax.validation support, set the gwt.validation.ignoreXml system property to avoid needing these". On JDK 21 there is no JAXB in the JDK, and without the property the validator generator fails (see the pitfalls table).

The test that used the validator ran with -DargLine=-Dgwt.validation.ignoreXml=true on the gwt:test goal (a surefire argument); for gwt:compile the plugin’s jvmArgs parameter passes JVM options to the forked compiler. The Maven coordinates added to the client module for that test:

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>4.1.0.Final</version>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>4.1.0.Final</version>
  <classifier>sources</classifier>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.6.1</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.6.1</version>
</dependency>
<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
  <classifier>sources</classifier>
</dependency>

The last dependency takes its version from the GWT BOM (validation-api 1.0.0.GA); since gwt-user already brings validation-api and its sources transitively, that entry is redundant in a project that depends on gwt-user. The run printed log4j:WARN No appenders could be found for logger (org.hibernate.validator.util.Version), which is harmless.

RequestFactory integration

RequestFactoryEditorDriver<P, E> is the driver for editing RequestFactory proxies; it extends EditorDriver<RequestContext>, so its flush() returns the RequestContext that accumulated the edits. The difference from the simple driver is how the bean becomes editable: RequestFactory proxies are immutable until a context calls edit(proxy), and the driver does that for every proxy it meets.

Table 10. RequestFactoryEditorDriver methods (2.13.1)
Method Purpose

initialize(E)

No subscription support: EditorDelegate.subscribe() does nothing.

initialize(RequestFactory, E), initialize(EventBus, RequestFactory, E)

Subscription support: the driver listens for EntityProxyChange events on the event bus (a custom bus can be supplied) so the editors see changes made elsewhere.

getPaths()

The property paths of the editor hierarchy, to pass to Request.with(...) so that the server returns the referenced proxies too.

edit(P proxy, RequestContext request)

Start editing. The context gets the edits; the driver does not fire it.

display(P proxy)

Read-only: show the data without a RequestContext.

flush()

Copy the editors into the proxies and return the RequestContext passed to edit.

setConstraintViolations(...)

Show violations; setViolations(Iterable<Violation>) is deprecated in favour of it.

The module for the generator is com.google.web.bindery.requestfactory.RequestFactory; it registers RequestFactoryEditorDriverGenerator for the driver the same way the Editor module does for the simple one. This sketch compiled with the 2.13.1 compiler (client side only; no server was started):

static class BookRfEditor extends Composite implements Editor<BookProxy> {
  TextBox title = new TextBox();
  IntegerBox year = new IntegerBox();
}

interface Driver extends RequestFactoryEditorDriver<BookProxy, BookRfEditor> {}

private final Driver driver = GWT.create(Driver.class);
private final BookRfEditor editor = new BookRfEditor();

void start(Factory rf) {
  driver.initialize(rf, editor);              // with a RequestFactory: subscriptions work
}

void load(Factory rf, long id) {
  BookRequest ctx = rf.bookRequest();
  ctx.find(id).with(driver.getPaths()).fire(new Receiver<BookProxy>() {
    @Override
    public void onSuccess(BookProxy proxy) {
      driver.edit(proxy, ctx);                // the context is the one that will persist
    }
  });
}

void save() {
  RequestContext ctx = driver.flush();        // the same context
  if (driver.hasErrors()) {
    return;                                   // editor-level problems
  }
  ctx.fire(new Receiver<Void>() {
    @Override
    public void onSuccess(Void ignored) { /* done */ }

    @Override
    public void onConstraintViolation(Set<ConstraintViolation<?>> violations) {
      driver.setConstraintViolations(violations);   // server-side violations next to the fields
    }
  });
}

Receiver.onConstraintViolation(Set<ConstraintViolation<?>>) is the current hook; Receiver.onViolation(Set<Violation>) and RequestFactoryEditorDriver.setViolations are deprecated. The Developer Guide’s FAQ says a read-only view is simply an editor whose driver is never flushed, and display(proxy) is the convenience for that. The RequestFactory & AutoBeans page covers proxies, contexts and the server side; GWT 2.11 added the Jakarta artifacts for it.

The Bookshelf book form, end to end

The pieces above make up the Bookshelf’s edit screen:

flowchart TD P["EditPlace book id"] --> A["EditActivity.start"] A --> R["RPC getBook"] R --> E["editor.edit(book)
driver.edit"] E --> U["User types in the
BookFormEditor boxes"] U --> S["Save: driver.flush"] S --> H{"hasErrors?"} H -- "yes" --> D["Decorators show
Bad value messages"] H -- "no" --> V["validator.validate and
setConstraintViolations"] V --> W{"violations?"} W -- "yes" --> D W -- "no" --> C["RPC saveBook
XSRF-protected"] C --> N["PlaceController goes to the catalog"]

The activity of the scratch Bookshelf (the real class, reduced to the editor lines) loads the book over GWT-RPC, hands it to the editor, and on save sends the flushed bean:

public class EditActivity extends AbstractActivity implements BookEditor.Listener {
  @Override
  public void start(AcceptsOneWidget panel, EventBus eventBus) {
    BookEditor editor = new BookEditor(this);
    panel.setWidget(editor);
    factory.getCatalog().getBook(id, new AsyncCallback<Book>() {
      @Override
      public void onSuccess(Book book) { editor.edit(book); }

      @Override
      public void onFailure(Throwable caught) { GWT.log("getBook failed", caught); }
    });
  }

  @Override
  public void onSave(Book edited) {
    factory.getCatalog().saveBook(edited, new AsyncCallback<Book>() { /* ... */ });
  }
}

The server must not trust the bean it receives because an editor produced it. The editor is a convenience of the browser, so the server re-validates every field, re-checks that the signed-in user may edit that book, and copies only the fields that the form is allowed to change from the received bean into the stored entity; sending a whole entity back and saving it as it arrives lets a hand-made request overwrite copies or an owner id (the over-posting problem). The XSRF protection of the RPC service (see GWT-RPC and XSRF, RPC & Server Security) applies to the save call as usual.

Testing editors

  • The generated driver needs GWT.create, so a test that uses the real driver is a GWTTestCase (see Testing). The examples on this page ran that way, in HtmlUnit, in a few seconds each.

  • The package com.google.gwt.editor.client.testing provides test doubles for code that depends on the framework without compiling it: MockSimpleBeanEditorDriver, MockEditorDelegate, MockEditorError, MockEditorChain, FakeEditorContext, FakeEditorSource, FakeLeafValueEditor and the EditorHierarchyPrinter used above. The RequestFactory side has MockRequestFactoryEditorDriver.

  • Keep the logic of a presenter independent of the driver and test it with plain JUnit; the Bookshelf’s CatalogPresenterTest follows that approach.

Pitfalls

Table 11. Common problems with the Editor framework
Symptom Cause Fix

Could not find a getter for path notes in proxy type pit.client.Bean followed by Unable to create Editor model due to previous errors and Failed to resolve 'pit.client.P3UnknownProperty.D' via deferred binding

A package-visible field of a type that is an editor (TextBox, Label, …​) has no matching property. The message says "proxy type" even for a plain bean. Typical for a Label that you use as a caption.

Rename the field to a property, add @Path, or mark it @Ignore; a private field is not considered at all

Could not find a getter for path author.nme in proxy type pit.client.Bean

A misspelt @Path

Fix the path; every segment needs a getter

Found unexpected type int while evauating path "year.length" using getter expression ""

A @Path that continues through a primitive property (the typo "evauating" is in the GWT source)

End the path at the primitive

Cycle detected in editor graph. Editor type pit.client.P4Cycle.E at path <Root Object> can be reached again at path next.next

The editor class contains an editor of its own type for a self-referencing bean (Bean.getNext())

Break the cycle: write a second editor type for the deeper level, or edit the self-reference with a CompositeEditor or by hand

The type pit.client.P8TwoInterfaces.D extends more than one interface

The driver interface extends something besides SimpleBeanEditorDriver

The driver must extend exactly one interface

You must declare an interface that extends the SimpleBeanEditorDriver type

GWT.create(SimpleBeanEditorDriver.class) on the library interface itself

Declare interface MyDriver extends SimpleBeanEditorDriver<Bean, MyEditor> {} and create that

Bound mismatch: The type P5RawEditor.E is not a valid substitute for the bounded parameter <E extends Editor<? super T>> of the type SimpleBeanEditorDriver<T,E>

The editor implements the raw Editor (no type argument)

implements Editor<Bean>

java.lang.IllegalStateException: Must call initialize() first / Must call edit() first

edit before initialize, or flush before edit

Follow the order: initialize, edit, flush

A field is never filled in and nothing is reported

The field is private, or it is null when the driver is initialised

Package-visible or accessor; assign the sub-editor before initialize

Text typed into a sub-editor of a null nested bean is lost

The nested bean is null, so the framework skips its editor (measured: getAuthor() is still null, no error)

Create the bean before edit, or use OptionalFieldEditor

Two errors for one bad box after isDirty()

isDirty() reads getValue() of every leaf, and a ValueBoxEditor records an error each time (measured: one error after flush(), two after isDirty() then flush())

Avoid calling isDirty() between a bad input and a flush(), or read getErrors() once per flush

An error for a list element has an old index (reviews[2])

The path is fixed when the sub-editor is attached; removing a row does not renumber the others

Identify the row by getEditor() or getUserData()

setConstraintViolations returns false and nothing is shown

The violations belong to another instance than the one passed to edit, or no editor exists for their path

Validate the object flush() returned; make sure the property has an editor

incompatible types: Set<ConstraintViolation<BookDetail>> cannot be converted to Iterable<ConstraintViolation<?>> (javac)

Generic invariance

new HashSet<ConstraintViolation<?>>(violations)

Unexpected error trying to instantiate Generator 'com.google.gwt.validation.rebind.ValidatorGenerator' with java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException

The validator generator reads validation XML with JAXB, which is not in JDK 11 and later

Set -Dgwt.validation.ignoreXml=true for the compiler and test JVM (GWT 2.13.0 and later)

An IntegerBox shows 2,018

Its renderer is the locale decimal format

Use a TextBox for identifiers and years, or a renderer without grouping

Two widgets write the same property

Both map to the same path; the later one in the hierarchy wins at flush()

One editor per property

The driver is created with new

The class does not exist; it is generated

GWT.create(Driver.class)

Where older material is outdated

Statement State in 2.13.1

Forms are wired by hand (the approach of GWT in Action)

The book describes GWT 1.3 and 1.4 and predates the framework by several years (the Developer Guide marks Editors as new in 2.1): its forms read and write each widget by hand, with FormPanel used only for HTML form submission. For binding forms to beans the Developer Guide is the source.

setEditorDelegate() in the Developer Guide

HasEditorDelegate.setDelegate(EditorDelegate<T>).

@Path("manager.name"); with a semicolon in the Developer Guide quick-start

A typo in the guide; the annotation takes the path and no semicolon.

setConstraintViolations with client-side Bean Validation is presented as the normal validation path

The client-side validation module is deprecated since 2.8.0 and stays on javax.validation 1.0; validation on the server (Jakarta Validation) is the maintained path, and the editors only display the results.

What changed recently

  • 2.8.0 (2016): the Bean Validation support that setConstraintViolations relies on was deprecated.

  • 2.11.0 (2024): Jakarta artifacts for GWT-RPC and RequestFactory, which matter for the save call of an editor-based screen.

  • 2.13.0 (2026): JAXB and xml-apis are optional for the validator when gwt.validation.ignoreXml is set.

  • org.gwtproject.editor:gwt-editor (GWT Editor) is a port of the framework without the dependency on gwt-user, aimed at J2CL and GWT 3. The repository gwtproject/gwt-editor was last pushed on 2024-12-17, Maven Central has only 1.0.0-RC1 (2020-10-05), and it replaces the generator with an annotation processor (gwt-editor-processor) and the module org.gwtproject.editor.Editor. It requires GWT 2.9.0 or newer. Use it when the application is moving to J2CL; keep com.google.gwt.editor.Editor for a GWT 2 application.