Cell Widgets
|
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 (2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, and the Jakarta Servlet 6 server of the Bookshelf scenario, checked on 2026-10-08.
Cell widgets are client knowledge: they are classes of gwt-user (the com.google.gwt.user.User module inherits them) that the GWT compiler turns into JavaScript together with your code.
The data they show is usually shared with the server (the Book and BookPage classes travel over GWT-RPC), the server side of paging and sorting is a few lines of plain Java, and the build needs nothing beyond what any GWT module already has (see Project Structure & Modules).
Every example on this page was compiled with the 2.13.1 compiler and loaded in headless Chrome; the Bookshelf catalogue also ran against the Jetty 12 backend of the scratch application, and the HTML, the request payloads and the messages quoted below are what the browser and the compiler printed.
Cell widgets are the GWT answer to the question "how do I show a thousand rows without a thousand widgets?".
An ordinary widget (a Label, a Button, a row of a FlexTable) is a Java object with a DOM element, event handlers and a place in the widget hierarchy, so a table of 1,000 rows by 5 columns is 5,000 objects that have to be created, attached and eventually detached.
A cell widget (CellList, CellTable, DataGrid, CellTree, CellBrowser) keeps one HTML string per page of data instead: it asks a small, stateless cell to render each value into that string, assigns the whole string to the browser with innerHTML, and lets the cell look at events afterwards.
The same handful of cell objects serve every row, which is the flyweight pattern the official guide names.
What a cell widget is
Four ideas explain almost everything on this page.
-
A cell renders a value into
SafeHtml.Cell<C>has arender(Context, C, SafeHtmlBuilder)method that appends HTML for one value. It does not create DOM nodes. A cell for aStringis a few lines; a cell for a wholeBookcan be a<div>with an image and two lines of text. -
A cell is a flyweight. One
TextCellobject renders the title of row 1 and of row 10,000, and it receives the browser events of all those rows. It must not keep per-row state in its fields; theContextargument tells it which row and column it is working for. -
A cell widget shows a page of rows. Cell widgets are always paged: a widget has a visible range (a start index and a length), it renders only those rows, and it announces range changes. The page size defaults to 25 for
CellList, 15 forCellTableand 50 forDataGrid. "No paging" simply means a page size as big as the data. -
Data arrives through a data provider. The widget does not own the data. A data provider is told which range the widget wants and pushes rows back with
updateRowData. When the data changes, the provider pushes again and the widget re-renders the affected rows.
CellPanel, in spite of its name, is not a cell widget: it is a base class of table-based panels such as HorizontalPanel (the official guide makes the same remark).
The Grid and FlexTable panels that the 2007 book teaches hold ordinary widgets in table cells and are covered with the other panels (see the Panels & Layout and Widgets pages).
FlexTable or Grid with widgets |
CellTable, DataGrid, CellList |
|
|---|---|---|
Object per visible cell |
A |
None: the column’s single |
Rendering |
DOM calls, widget by widget |
One HTML string per page, assigned with |
Events |
One handler per widget |
One listener on the widget; the cell is told the row, column and key |
Large data |
Slows down with the number of rows on screen |
Cost follows the page size, not the data size |
Paging, sorting, selection |
Hand-written |
|
Arbitrary widget inside a cell |
Natural |
Not directly: a cell renders HTML, not widgets, so a cell that must contain a full widget calls for a different design |
Choose a cell widget when the data set is large or changes often, when the columns map to data fields, when you need paging, sorting or selection, or when you want the table to stay cheap on a phone. Choose a plain widget (or UiBinder) for a small, form-like layout where each part is a different component. A cell widget can still contain interactive parts: buttons, check boxes, text inputs and drop-downs are all built-in cells.
The architecture
The pieces are small and independent; each can be replaced.
ListDataProvider or AsyncDataProvider"] keys["ProvidesKey"] sel["SelectionModel"] end subgraph view["Cell widget (HasData)"] w["CellTable, DataGrid, CellList
CellTree, CellBrowser"] col["Column and Header
(tables only)"] cell["Cell"] pager["Pager
SimplePager, PageSizePager"] end dp -- "updateRowData / setRowData" --> w w -- "RangeChangeEvent" --> dp w -- "renders through" --> col col -- "getValue then render" --> cell cell -- "ValueUpdater / FieldUpdater" --> col sel -- "isSelected" --> w w -- "setSelected" --> sel keys -. "identifies a row" .-> dp keys -. "identifies a row" .-> sel pager -- "setVisibleRange" --> w w -- "RangeChangeEvent, RowCountChangeEvent" --> pager
| Piece | What it is | Package |
|---|---|---|
|
Renders one value and handles events on what it rendered |
|
|
A table column: how to get a |
|
|
The widgets. The list and the two tables implement |
|
|
The contract between a widget and its data provider (visible range, row count, |
|
|
Data providers |
|
|
Selection state, row identity, and the translation of clicks and keys into selection |
|
|
The model behind |
|
|
Paging controls |
|
The module com.google.gwt.user.User already inherits com.google.gwt.user.cellview.CellView, so an application needs no extra inherits line.
The widgets bring their own styles as ClientBundle`s (`CellTable.Resources, DataGrid.Resources, CellList.Resources and so on), injected the first time a widget is created.
All the cell widgets that show a flat page of rows (CellList, CellTable, DataGrid) extend AbstractHasData, which is where the page size, setRowData, setRowCount, the selection model, the key provider and the keyboard policies live.
Since the data model is in com.google.gwt.view.client and not in the widgets, a view can be tested with a hand-written fake of HasData, and the same provider can feed a table and a pager at the same time.
Cells
The Cell interface
Cell<C> is an interface, but the Javadoc warns against implementing it directly: it may change in small but breaking ways as the library is tuned for speed, so extend AbstractCell<C> instead.
These are its methods in 2.13.1.
| Method | Purpose |
|---|---|
|
Append the HTML for a value. The only method |
|
The event types the cell wants ( |
|
Called when one of those events happens on something the cell rendered. |
|
True while the cell shows an editor; the widget then leaves the cell alone when it re-renders. |
|
Re-render one cell in place ( |
|
Give focus back to the cell’s focusable part after a re-render. |
|
Whether the cell’s HTML changes with the row’s selection state, and whether the cell manages selection itself (a check box that is the selection). |
Cell.Context carries three facts about the instance that is being rendered: getIndex() (the row, as an absolute index), getColumn() and getKey() (the row’s key from the ProvidesKey); a fourth, getSubIndex(), is 0 unless the row value renders to more than one row element (see the table builders below).
Nothing else about the row is passed: a cell receives the value of its column, not the row object.
The value of a cell can be null.
The official guide warns that cell widgets can pass null for rows that have no data yet, for example when data arrives in the middle of the visible range, so every render starts with a null check.
Source: GWT Javadoc — Cell
Built-in cells
All of these live in com.google.gwt.cell.client; every class name in the table exists in 2.13.1.
| Cell | Value type | What it renders and does |
|---|---|---|
|
|
Escaped text (no element at all, only the text). The default for a |
|
|
Text (no element of its own, like |
|
|
Text that becomes an |
|
|
An always-visible |
|
|
A |
|
|
A button with a fixed label and a |
|
|
A check box. |
|
|
A |
|
|
A formatted date ( |
|
|
A number formatted with a |
|
|
Images from a URL, a sanitised URI, a |
|
|
Already-safe HTML, appended as is. |
|
|
Several cells side by side, each described by a |
|
|
Adds an icon (an |
The cells that take input (EditTextCell, TextInputCell, CheckboxCell, SelectionCell, DatePickerCell) extend AbstractEditableCell, which adds view data: a map from the row key to what the user typed but has not saved.
That is the second reason a ProvidesKey matters (see below): with the default key (the row object itself) a re-fetched row looks like a new row and the pending edit is lost.
The pending value stays until the row’s underlying value changes (a successful commit) or until AbstractEditableCell.clearViewData(key) is called, which is what a failed save should do. An editable cell object shared by two columns or two widgets shares that pending state too, and the other widget shows it when it is next redrawn.
ValueUpdater and FieldUpdater
A cell tells the outside world about a user action through a ValueUpdater<C>: its single method update(C newValue) is called with the new value (the text typed, the box state, the selected option; for a button, the same value again as a "something happened" signal).
A cell widget with no columns, such as CellList, takes the updater directly with setValueUpdater.
A table column wraps it: Column.setFieldUpdater(FieldUpdater<T, C>) provides update(int index, T row, C value), which is what you almost always want, because it knows the row.
An updater does not re-render anything. It is a notification. The code in it must change the model and then ask for the affected row to be drawn again, as the pitfalls table below shows with a measurement.
CellList
CellList<T> is the simplest cell widget: one cell type renders every row, one row per <div>.
It is the right choice for a list of "cards" (a result list, a chat, a menu) and for the nodes of the two tree widgets, which are CellList-like internally.
The example fills a CellList<Book> through a ListDataProvider, identifies rows by the book id and selects one.
The cell is an AbstractCell<Book> that escapes the title and the author with SafeHtmlBuilder.
CellList of books with a key provider and a selection model (CellLab.java)ProvidesKey<Book> keys = book -> book == null ? null : book.getId();
CellList<Book> list = new CellList<>(new AbstractCell<Book>() {
@Override
public void render(Context context, Book book, SafeHtmlBuilder sb) {
if (book != null) {
sb.appendEscaped(book.getTitle()).appendHtmlConstant(" <i>")
.appendEscaped(book.getAuthor()).appendHtmlConstant("</i>");
}
}
}, keys);
SingleSelectionModel<Book> selection = new SingleSelectionModel<>(keys);
list.setSelectionModel(selection);
ListDataProvider<Book> provider = new ListDataProvider<>(keys);
provider.addDataDisplay(list);
provider.getList().addAll(books);
selection.setSelected(books.get(2), true);
// Rename the selected book, then push every row again:
books.get(2).setTitle("Refactoring, 2nd ed.");
provider.refresh();
Headless Chrome printed this for the five rows. The compiler obfuscates the style names (they were short generated names such as GBG2FFLDAB); the excerpt writes each one as the CellList.Style method it stands for.
The third row is the selected one and it is still selected after the rename and refresh(); the first row carries the keyboard-focus style and the only tabindex. Because the lab renamed the very same object, this does not prove the key provider; the section on keys below measures that.
The fourth title is the text <script>alert(1)</script> Tricks: it arrived as text, not as markup.
CellList (excerpt)<div class="cellListWidget" __gwtcellbasedwidgetimpldispatchingfocus="true" __gwtcellbasedwidgetimpldispatchingblur="true">
<div>
<div __idx="0" class="cellListEvenItem cellListKeyboardSelectedItem" style="outline:none;" tabindex="0">Effective Java <i>Joshua Bloch</i></div>
<div __idx="1" class="cellListOddItem" style="outline:none;">Clean Code <i>Robert C. Martin</i></div>
<div __idx="2" class="cellListEvenItem cellListSelectedItem" style="outline:none;">Refactoring, 2nd ed. <i>Martin Fowler</i></div>
<div __idx="3" class="cellListOddItem" style="outline:none;"><script>alert(1)</script> Tricks <i>Eve & Co</i></div>
<div __idx="4" class="cellListEvenItem" style="outline:none;">Clean Architecture <i>Robert C. Martin</i></div>
</div>
</div>
The __idx attribute is how CellList finds the row for an event: it walks up from the event target to the first element that carries one.
CellList also offers setEmptyListMessage/setEmptyListWidget and setLoadingIndicator for the two states without rows.
CellTable
CellTable<T> renders a page of rows of type T as an HTML <table>; each column shows one aspect of the row.
A Column<T, C> has to implement getValue(T row), and a Cell<C> (given to its constructor) renders that value.
TextColumn<T> is the shortcut for a text column (Cell<String> is a TextCell), and IdentityColumn<T> passes the row itself to the cell, which is what a CompositeCell or an "actions" cell needs.
A table with the built-in cells
The example below shows a table of loans, with one column per interesting built-in cell.
Loan is a client-side class here (title, due date, returned flag, status); the table itself would look the same with any row type.
CellTable<Loan>: check box, editable text, date, drop-down, number, clickable text, button and a composite (CellLab.java)ProvidesKey<Loan> keys = loan -> loan.id;
CellTable<Loan> table = new CellTable<>(10, keys);
// A selection model driven by the check boxes only:
MultiSelectionModel<Loan> selection = new MultiSelectionModel<>(keys);
table.setSelectionModel(selection, DefaultSelectionEventManager.<Loan>createCheckboxManager());
Column<Loan, Boolean> pick = new Column<Loan, Boolean>(new CheckboxCell(true, false)) {
@Override
public Boolean getValue(Loan loan) { return selection.isSelected(loan); }
};
table.addColumn(pick, "");
table.setColumnWidth(pick, 3, Unit.EM);
Column<Loan, String> title = new Column<Loan, String>(new EditTextCell()) {
@Override
public String getValue(Loan loan) { return loan.title; }
};
title.setSortable(true);
title.setFieldUpdater((index, loan, value) -> loan.title = value);
table.addColumn(title, "Title", "titles"); // header and footer text
table.addColumn(new Column<Loan, Date>(new DateCell(DateTimeFormat.getFormat("yyyy-MM-dd"))) {
@Override
public Date getValue(Loan loan) { return loan.due; }
}, "Due", "dates");
Column<Loan, String> status =
new Column<Loan, String>(new SelectionCell(Arrays.asList("ok", "late", "lost"))) {
@Override
public String getValue(Loan loan) { return loan.status; }
};
status.setFieldUpdater((index, loan, value) -> loan.status = value);
table.addColumn(status, "Status");
Header<String> fineTotal = new Header<String>(new TextCell()) { // a footer that follows the data
@Override
public String getValue() {
double total = 0;
for (Loan loan : table.getVisibleItems()) {
total += loan.id * 1234.5;
}
return "Total " + NumberFormat.getFormat("#,##0.00").format(total);
}
};
table.addColumn(new Column<Loan, Number>(new NumberCell(NumberFormat.getFormat("#,##0.00"))) {
@Override
public Number getValue(Loan loan) { return loan.id * 1234.5; }
}, new TextHeader("Fine"), fineTotal);
table.addColumn(new Column<Loan, String>(new ClickableTextCell()) {
@Override
public String getValue(Loan loan) { return "details"; }
});
table.addColumn(new Column<Loan, String>(new ButtonCell()) {
@Override
public String getValue(Loan loan) { return "Return"; }
});
List<HasCell<Loan, ?>> parts = new ArrayList<>();
parts.add(/* a HasCell<Loan, String> with a TextCell showing "#" + id */);
parts.add(/* a HasCell<Loan, Loan> with ActionCell<>("Renew", loan -> loan.due = nextWeek(loan)) */);
table.addColumn(new IdentityColumn<>(new CompositeCell<>(parts)), "Actions");
Loaded with three loans, the table produced the following markup (data URIs and class names shortened).
Note the pieces the compiler and the widget add: a <colgroup> with one <col> per column (the first has the width set with setColumnWidth), __gwt_row and __gwt_cell attributes that route events to the right column, a <select> with the current value marked selected, a number formatted by the NumberFormat, and a <tfoot> whose cell under the "Fine" column holds the computed total.
CellTable (first body row and the footer, excerpt)<table class="…" cellspacing="0">
<colgroup><col style="width: 3em;"><col><col><col><col><col><col><col></colgroup>
<thead><tr __gwt_header_row="0">
<th colspan="1" class="…" role="button" tabindex="-1"><div …>sort arrow</div><div __gwt_header="header-gwt-uid-39">Title</div></th>
<th colspan="1" class="…">Due</th> <th colspan="1" class="…">Status</th> <th colspan="1" class="…">Fine</th>
<th colspan="2" class="…"></th> <!-- the two columns with no header share one cell -->
<th colspan="1" class="…">Actions</th>
</tr></thead>
<tbody>
<tr __gwt_row="0" __gwt_subrow="0" class="…"> <!-- the selected row carries one more style than the others -->
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-55" tabindex="0"><input type="checkbox" tabindex="-1" checked=""></div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-56">Clean Code</div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-57">2026-02-04</div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-58"><select tabindex="-1"><option value="ok" selected="selected">ok</option><option value="late">late</option><option value="lost">lost</option></select></div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-59">3,703.50</div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-60">details</div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-61"><button type="button" tabindex="-1">Return</button></div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-62"><span>#3</span><span><button type="button" tabindex="-1">Renew</button></span></div></td>
</tr>
…
</tbody>
<tfoot><tr __gwt_header_row="0">
<th class="…"></th> <th class="…">titles</th> <th class="…">dates</th> <th class="…"></th>
<th class="…" __gwt_header="header-gwt-uid-62">Total 7,407.00</th> <th colspan="3" class="…"></th>
</tr></tfoot>
</table>
What the experiment also showed:
-
The
CheckboxCell(true, false)combined withcreateCheckboxManager()made a click on a check box select a row (the row got the selected style and the box was checked), while a click on any other cell of the row only moved the keyboard focus. WithcreateDefaultManager()(the default) a click on the row would select it, with the control key to add to the selection and, for aMultiSelectionModel, the shift key to select a range. -
Headers and footers are
Headerobjects, rendered every time the row data changes or the table is redrawn. Passing the same header instance to adjacent columns makes it span them: the two columns without a header receivednullfor it and shared onecolspan="2"cell. -
The footer total is recomputed on every render because
Header.getValue()is called then; that is why a header can show a sum, a count or a filter box.
Column and cell styling
Column.setCellStyleNames("name") sets a style for every cell of a column, and Column.getCellStyleNames(Cell.Context, T row) can return a style per cell, as the Bookshelf catalogue does for books with no copies left.
AbstractCellTable.setRowStyles(RowStyles<T>) takes an object with getStyleNames(T row, int rowIndex) for the whole row.
setColumnWidth(Column, double, Unit) and setColumnWidth(Column, String) control widths, Column.setHorizontalAlignment(...) and setVerticalAlignment(...) the alignment, and addColumnStyleName(int, String) a style on a <col>.
The widths need a fixed layout to be reliable.
By default the columns grow to fit the contents of their cells, which looks jumpy when the content changes with paging; table.setWidth("100%", true) switches to the fixed table layout and lets you give the columns percentages (which should add up to 100%), pixels, or a mix: a narrow pixel column for a check box and percentages for the rest.
The official guide adds that with table.setWidth("auto", true) a column without a width is invisible, because its width defaults to 0.
The look of the widget itself comes from its Resources bundle.
CellTable.Resources defines cellTableStyle() (a CellTable.Style CssResource whose default stylesheet is CellTable.css) and the images for the header and footer backgrounds, the loading indicator and the sort arrows; CellTable.BasicResources is the variant for the GWT standard style theme and uses CellTableBasic.css.
To restyle it, extend CellTable.Resources, re-declare cellTableStyle() with @Source({CellTable.Style.DEFAULT_CSS, "mytable.gss"}) and pass an instance to a constructor such as CellTable(int pageSize, Resources resources); the CSS and GSS directives are the subject of the Styling: CSS & GSS page.
Taking over the table’s HTML
For layouts the column model cannot express (several rows per item, a detail row under an expanded item, group headers) a table can be given its own markup builders: AbstractCellTable.setTableBuilder(CellTableBuilder), setHeaderBuilder(HeaderBuilder) and setFooterBuilder(FooterBuilder), with AbstractCellTableBuilder, DefaultCellTableBuilder and AbstractHeaderOrFooterBuilder as starting points.
They came with GWT 2.5 (see the older-material section) and are the escape hatch to use instead of abandoning the cell table for a hand-made FlexTable.
Source: GWT Javadoc — CellTable
Sorting
A cell table does not sort anything by itself; it provides the user interface and an event, and you decide where the sort happens.
-
column.setSortable(true)makes the column header clickable (role="button") and shows an arrow once it is the sort column. -
A click on the header pushes the column onto the table’s
ColumnSortListand fires aColumnSortEvent. -
Handlers registered with
table.addColumnSortHandler(...)react.
Two handlers ship with GWT, one for each kind of data provider.
ColumnSortEvent.ListHandler<T> |
ColumnSortEvent.AsyncHandler |
|
|---|---|---|
Use with |
|
|
What it does |
Looks up the |
Calls |
What you write |
One |
The provider reads |
ListHandler (CellLab.java)ColumnSortEvent.ListHandler<Loan> sorter = new ColumnSortEvent.ListHandler<>(provider.getList());
sorter.setComparator(title, Comparator.comparing(l -> l.title));
table.addColumnSortHandler(sorter);
// Mark the initial order in the header (this does not fire the event by itself):
table.getColumnSortList().push(title);
ColumnSortEvent.fire(table, table.getColumnSortList());
The loans came out ordered "Clean Code", "Effective Java", "Refactoring"; clicking the sorted header then reversed the order, because ColumnSortList.push(column) on the column that is already first flips its direction (ascending becomes descending and back).
That flip is why the lab pushed the column programmatically only once.
The sort list can hold more than one column (ColumnSortList.setLimit), which a server-side sorter can use for "sort by author, then by year".
Column.setDefaultSortAscending(false) makes the first click sort descending, for a "newest first" date column.
The server-side variant, with AsyncHandler, is the Bookshelf example further down.
Two behaviours of ListHandler follow from its source: a column that is setSortable(true) but has no comparator registered is silently ignored (the arrow moves, the rows stay), and onColumnSort sorts the list with Collections.sort, so the list must be modifiable.
DataGrid
DataGrid<T> is the CellTable for application screens: its Javadoc describes it as a table with a fixed header and footer and a scrollable data section between them. It was added in GWT 2.4.
Both extend AbstractCellTable, so columns, headers, footers, sorting, widths, row styles, selection, key providers and table builders work the same way, and a CellTable can usually be turned into a DataGrid by changing the class name and the place it is put in.
CellTable |
DataGrid |
|
|---|---|---|
Default page size |
15 |
50 |
Scrolling |
The table grows with its rows and the page scrolls |
The header and footer stay in place and the data section scrolls inside the widget |
Layout |
An ordinary widget: it takes the height of its content |
Implements |
Typical parent |
A |
A layout panel: |
Width control |
|
|
Resources |
|
|
Best for |
Short tables embedded in a page |
A list that takes a screen, or a pane in a split layout |
Source: GWT Javadoc — DataGrid
The consequence of RequiresResize is the most common DataGrid surprise: a DataGrid placed in a parent that does not give it a size shows only its header.
The lab put the same grid twice, once in a plain FlowPanel and once in a SimpleLayoutPanel of 20em by 10em, and asked the browser for the markup.
DataGrid in a FlowPanel: the data section has height 0 (s7), and in a SimpleLayoutPanel it has 104px (s8)<!-- FlowPanel: header at top: 0, data section starts at top: 26px and is 0px high -->
<div style="position: absolute; left: 0px; width: 100%; overflow: hidden; top: 26px; height: 0px;">
<!-- SimpleLayoutPanel of 20em x 10em: the same div is 104px high and the five rows fit -->
<div style="position: absolute; left: 0px; width: 100%; overflow: hidden; top: 26px; height: 104px;">
There was no error and no log message: the grid had been given data (the rows were in the markup) but no room to show them.
The fix is always the same: put the grid in a layout panel (RootLayoutPanel.get().add(grid), the center of a DockLayoutPanel, a SimpleLayoutPanel with a size) or give its host element a CSS height.
This is the same layout rule that makes a DockLayoutPanel inside a plain SimplePanel render blank, covered on the Panels & Layout page.
CellTree and CellBrowser
A tree cell widget shows hierarchical data without a widget per node.
The widgets know nothing about your hierarchy; a TreeViewModel answers two questions:
-
<T> NodeInfo<?> getNodeInfo(T value): "the children of this value are provided by this data provider, rendered by this cell, selected through this selection model". -
boolean isLeaf(Object value): "this value cannot be opened".
The tree asks getNodeInfo(rootValue) for the first level, where rootValue is the object you pass to the constructor and the tree never displays (null is accepted), and calls it again with the value of each node that the user opens.
NodeInfo<T> is an interface with a cell, a key provider, a selection model, a value updater and the setDataDisplay/unsetDataDisplay that attach a list to the node; TreeViewModel.DefaultNodeInfo<T> implements it from a data provider and a cell (and optionally a selection model and a value updater), which covers nearly every case.
CatalogTreeModel.java)public class CatalogTreeModel implements TreeViewModel {
private final List<Book> books;
private final SingleSelectionModel<Book> bookSelection = new SingleSelectionModel<>(b -> b.getId());
public CatalogTreeModel(List<Book> books) {
this.books = books;
}
@Override
public <T> NodeInfo<?> getNodeInfo(T value) {
if (value == null) { // the hidden root: its children are the authors
ListDataProvider<String> authors = new ListDataProvider<>();
authors.getList().addAll(new TreeSet<>(authorNames()));
return new DefaultNodeInfo<>(authors, new TextCell());
}
String author = (String) value; // an author node: its children are the books
ListDataProvider<Book> byAuthor = new ListDataProvider<>(b -> b.getId());
for (Book b : books) {
if (b.getAuthor().equals(author)) {
byAuthor.getList().add(b);
}
}
return new DefaultNodeInfo<>(byAuthor, new AbstractCell<Book>() {
@Override
public void render(Context context, Book book, SafeHtmlBuilder sb) {
if (book != null) {
sb.appendEscaped(book.getTitle());
}
}
}, bookSelection, null);
}
@Override
public boolean isLeaf(Object value) {
return value instanceof Book;
}
}
The model is the same for both widgets.
CellTree shows the levels one under the other, with an open/close arrow and an optional animation; CellBrowser shows each opened level in a column of its own, side by side like a file browser.
CellLab.java)TreeViewModel model = new CatalogTreeModel(books);
CellTree tree = new CellTree(model, null);
tree.setAnimationEnabled(false);
tree.getRootTreeNode().setChildOpen(0, true); // open the first author
CellBrowser browser = new CellBrowser(model, null);
browser.setAnimationEnabled(false);
browser.getRootTreeNode().setChildOpen(0, true);
browser.setHeight("12em");
A TreeNode (from getRootTreeNode()) lets code walk and change the tree: getChildCount(), getChildValue(int), isChildLeaf(int), isChildOpen(int) and setChildOpen(int, boolean).
Headless Chrome showed the markup of the tree with the first author open: an ARIA role="tree" root, one role="treeitem" per node with aria-level, aria-setsize, aria-posinset and, for inner nodes, aria-expanded, and the book of that author as a level-2 item.
CellTree markup (excerpt, style names omitted)<div role="tree" …>
<div aria-selected="false" role="treeitem" aria-setsize="4" aria-posinset="1" aria-expanded="true" aria-level="1">
<div …><div …><div …><img …></div><div …>Eve & Co</div></div></div>
<div style="overflow: hidden;">
<div aria-selected="false" role="treeitem" aria-setsize="1" aria-posinset="1" aria-level="2">
<div …><div …><div style="position:absolute;display:none;"></div><div …><script>alert(1)</script> Tricks</div></div></div>
</div>
<div style="display: none;">Empty</div>
<a href="javascript:;" style="display: none;">Show more</a>
</div>
</div>
<div aria-selected="false" role="treeitem" aria-setsize="4" aria-posinset="2" aria-expanded="false" aria-level="1">…Joshua Bloch…</div>
…
</div>
The hidden "Empty" and "Show more" elements belong to each node: a node with more children than CellTree.getDefaultNodeSize() (25 by default; setDefaultNodeSize changes it) shows a "Show more" link, and a node with no children shows "Empty".
The CellBrowser has the same limit per column, with "Show More" and "Show Less" links.
A tree node’s children are a data display like any other, so a node can use an AsyncDataProvider: getNodeInfo returns a DefaultNodeInfo over a provider that fetches the children of the opened node from the server.
Because getNodeInfo is called each time a node is opened, create the provider inside it (as above) or cache it, but do not share one provider between two nodes.
Data providers, ranges and pagers
HasData, HasRows and Range
A cell widget exposes its paging state through HasRows: getVisibleRange(), setVisibleRange(int start, int length), getRowCount(), isRowCountExact(), setRowCount(int) and setRowCount(int, boolean), plus handlers for RangeChangeEvent and RowCountChangeEvent.
HasData<T> adds setRowData(int start, List<? extends T>), getVisibleItem(int), getVisibleItems() and setSelectionModel.
A Range is the pair start index, length (new Range(0, 10)), a plain serializable value class, so it can also be an argument of an RPC method.
The row count is separate from the rows: a widget can show rows 10 to 19 and still know that there are 25 in all. The count can be exact or an estimate: when a count is not exact, the pager shows "of over N" and lets the user keep going while data keeps arriving, which suits a stream or a server that cannot count cheaply.
ListDataProvider
ListDataProvider<T> is a provider whose data is a java.util.List in the browser.
getList() returns a wrapper of the list: every change made through it (add, set, remove, clear, sort) is noticed and pushed to the displays, at the end of the current event loop, so several changes cost one refresh.
flush() pushes immediately, refresh() re-sends every row, and setList(List) replaces the underlying list.
The page size of the display decides which rows of the list are shown, so a list of 200 items and a CellList of 25 produce eight pages without further code.
The wrapper cannot see changes inside the items: the Javadoc says so, and the lab measured it with a CellList showing one Book.
| Step | Row 0 after the browser rendered it |
|---|---|
rendered with the title "Before" |
|
|
|
|
|
|
|
getList().set(index, item) is the cheaper way when one row changed: the provider knows which rows were modified and pushes only those, while refresh() pushes all.
After a FieldUpdater changed an item in place, either of the two (or display.redrawRow(index) for one row, as the pitfalls table shows) is needed.
AsyncDataProvider
AsyncDataProvider<T> is for data that lives on a server or is expensive to produce.
It is deliberately small: you subclass it and implement a single method.
protected abstract void onRangeChanged(HasData<T> display)
GWT calls that method whenever a display needs a different range: the moment the display is added with addDataDisplay (the display’s current visible range is "initialised" right away, which is why adding the display is the signal to load the first page), when the user pages, when the page size changes, and after a sort when AsyncHandler clears the data.
The method’s job is to read display.getVisibleRange(), start the request and, when the answer comes, call two methods that AsyncDataProvider makes public:
-
updateRowCount(int count, boolean exact)tells all displays how many rows exist. -
updateRowData(int start, List<T> values)gives them the rows that start at indexstart.
The provider does not hold on to the data: it forwards to each display only the rows that fall inside that display’s visible range (an empty list that starts at the page start is delivered too) and ignores the rest, so a late answer for an old range does not put foreign rows on the page. The only state it keeps is the last row count it was told, which it passes to a display that is added later.
The guide’s own example simulates the server with a Timer and ends with cellList.setRowData(start, dataInRange) on the widget.
That works for one display, but updateRowData on the provider is the call that serves every display attached to it (a table and a list showing the same query) and applies the range check, so use it in real code.
The guide also describes a provider-less way: a widget fires RangeChangeEvent`s, so a presenter can listen with `addRangeChangeHandler, send a request and push rows with setRowData; force the first event with setVisibleRangeAndClearData(new Range(0, 25), true).
A data provider is just that code, written once.
Out-of-order answers
Two requests can be in flight at once (the user pages twice, or sorts while a page is loading), and the network does not promise to answer them in order. The provider cannot know which answer is newer, so the code that owns the request does: the Bookshelf presenter below numbers each request and drops a response that is not the latest, and a JVM unit test delays the first answer and checks that the late one is ignored.
Pagers
A pager is a widget that is connected to any HasRows with setDisplay(HasRows) and calls setVisibleRange on it.
Because it listens to RangeChangeEvent and RowCountChangeEvent, it works the same on a CellList, a CellTable, a DataGrid, a custom HasRows, and when the data comes from either provider.
| Pager | Behaviour |
|---|---|
|
First/previous/next/last-page buttons (image buttons with |
|
"Show More" and "Show Less" links that grow or shrink the page size by an increment ( |
|
The base class for your own: a |
AbstractPager.setRangeLimited(boolean) decides whether the paging operations adjust so that there is always data visible on the page ("range limited") or not.
The label depends on whether the provider told the widget the exact row count.
The lab connected a SimplePager to a CellTable of page size 5 over five books, once with an AsyncDataProvider that called only updateRowData, and once with updateRowCount(5, true) first:
| Provider calls | Label | getRowCount() and isRowCountExact() |
|---|---|---|
|
|
5 and |
|
|
5 and |
Without the count the pager has no way to know that the data ends, and the next-page button stays enabled.
Always call updateRowCount with the real total as soon as the server knows it (the Bookshelf page carries the total in BookPage).
Selection and keys
A cell widget does not store which rows are selected; it asks its selection model, and tells it when the user selects.
The model decides what "selected" means, can be shared by several widgets, and fires SelectionChangeEvent.
| Model | Behaviour |
|---|---|
|
At most one selected item: |
|
Any number of selected items: |
|
Nothing is ever selected, but the widget still fires events: |
|
An abstract base class that selects by default and records the exceptions: you implement |
A model is attached with setSelectionModel(model), optionally with a second argument, an event manager, that decides which user actions select.
DefaultSelectionEventManager is a CellPreviewEvent.Handler: the widget fires a CellPreviewEvent for every cell event before the cell sees it, and the manager turns clicks and the space bar into SelectionModel.setSelected calls.
| Factory | Selection happens when |
|---|---|
|
A click or the space bar on a row. The control key toggles; with a |
|
A click on a check box (in any column, or in the given one). Clicking elsewhere does not select, as the lab measured. |
|
A click on a column that is not in the blacklist, or that is in the whitelist. |
|
Whatever your |
ProvidesKey
Every selection model, data provider and cell widget can be given a ProvidesKey<T>, whose single method Object getKey(T item) returns the identity of an item.
Without one, an item is its own key and the model uses equals and hashCode (SimpleKeyProvider is that default).
The keys matter in three places:
-
Selection survives new instances. A server answer creates new
Bookobjects every time, so a model that remembers the objects would forget the selection at each page load. With the book id as key it recognises them. -
Edits survive a redraw.
EditTextCelland the other input cells keep their pending text as view data under the key. -
Updates reach the right row. Row events carry the key in the
Context.
The lab compared two SingleSelectionModel<Book> instances, one without and one with a key provider that returns the id, after setSelected(book) and then isSelected(sameBookAgain) with a new Book object with the same id:
| Model | isSelected(newInstanceWithTheSameId) |
|---|---|
|
|
|
|
Give the same key provider to the widget, the data provider and the selection model, and make getKey null-safe, because widgets call it with null for filler rows (the guide says "always do a null check").
The Book::getId method reference used in the Bookshelf DataGrid constructor is not null-safe, but there it only ever receives books that the grid is rendering.
Keyboard
setKeyboardSelectionPolicy takes a KeyboardSelectionPolicy: DISABLED, ENABLED (the default: the arrow keys move a focus highlight, the first row has tabindex="0") or BOUND_TO_SELECTION (the arrow keys also change the selection).
setKeyboardPagingPolicy takes CURRENT_PAGE, CHANGE_PAGE (the default: the arrow keys can move into the next page) or INCREASE_RANGE (the page grows).
The focus highlight is a style of its own (cellListKeyboardSelectedItem in the CellList output above), separate from the selected style, which is what the tabindex="0" row and the role="button" headers make reachable without a mouse.
A cell that handles the Enter key through onEnterKeyDown (see below) is activated from the keyboard the same way.
Custom cells
The built-in cells cover forms and tables; a custom cell is how a book, an author or a rating gets its own look without a widget per row. The recipe has four steps, and every step is a decision the guide spells out.
-
Extend
AbstractCell<C>, withCthe type of value the cell shows. The constructor lists the events the cell wants:super("click", "keydown"). -
Implement
render(Context, C, SafeHtmlBuilder). Check fornull, escape anything that came from a user, and append HTML. -
Override
onBrowserEvent(...)if the cell reacts to events, andonEnterKeyDown(...)for the keyboard. -
Report changes by calling the
ValueUpdaterthat is passed in (it can benull).
Rendering safely: SafeHtmlBuilder and SafeHtmlTemplates
The HTML a cell appends is assigned to the page without any further escaping, so the cell is where cross-site scripting is prevented or allowed.
SafeHtmlBuilder supports the safe habit: appendEscaped(String) turns < and & into entities, appendHtmlConstant(String) is for literals written by the developer (never for data) and append(SafeHtml) adds something already known to be safe (the SafeHtml & XSS page covers the type system behind it).
The lab fed the title <script>alert(1)</script> Tricks to a cell that used appendEscaped, and the browser received <script>alert(1)</script> Tricks, displayed as text.
For anything with attributes, string concatenation quickly gets unreadable and unsafe, so GWT generates the code from a template: an interface that extends SafeHtmlTemplates, with one method per fragment and the HTML in an annotation.
The type of each parameter says how it is escaped: a String is escaped as text or as an attribute value, a SafeHtml is inserted as is, SafeStyles goes in a style attribute and SafeUri in a href or src.
The lab’s CopiesBadgeCell shows the number of copies of a book as a small badge; clicking it (or pressing Enter on it) asks to borrow a copy.
CopiesBadgeCell.java)public class CopiesBadgeCell extends AbstractCell<Integer> {
interface Templates extends SafeHtmlTemplates {
@SafeHtmlTemplates.Template("<span class=\"{0}\" title=\"{1}\">{2}</span>")
SafeHtml badge(String styleName, String tooltip, String text);
}
private static final Templates TEMPLATES = GWT.create(Templates.class);
public CopiesBadgeCell() {
super("click", "keydown"); // the events this cell wants to receive
}
@Override
public void render(Context context, Integer copies, SafeHtmlBuilder sb) {
if (copies == null) {
return; // widgets pass null for filler cells
}
String style = copies == 0 ? "bk-badge bk-out" : "bk-badge";
String text = copies == 0 ? "none left" : copies + " left";
sb.append(TEMPLATES.badge(style, "Row " + context.getIndex(), text));
}
@Override
public void onBrowserEvent(Context context, Element parent, Integer copies, NativeEvent event,
ValueUpdater<Integer> updater) {
super.onBrowserEvent(context, parent, copies, event, updater); // Enter -> onEnterKeyDown
if ("click".equals(event.getType())
&& parent.getFirstChildElement().isOrHasChild(Element.as(event.getEventTarget()))) {
borrow(copies, updater);
}
}
@Override
protected void onEnterKeyDown(Context context, Element parent, Integer copies, NativeEvent event,
ValueUpdater<Integer> updater) {
borrow(copies, updater);
}
private void borrow(Integer copies, ValueUpdater<Integer> updater) {
if (copies != null && copies > 0 && updater != null) {
updater.update(copies - 1);
}
}
}
The pieces to notice:
-
GWT.create(Templates.class)is deferred binding (see Deferred Binding & Generators): the compiler generates the implementation and checks the template while it compiles, so a typo in a placeholder is a build error and not a runtime surprise. The messages are in the pitfalls table. -
The
titleattribute is aStringparameter, so a book title with a quote in it cannot break out of the attribute. -
parentinonBrowserEventis the element that contains the cell’s HTML, not the outermost element the cell wrote (aTextCellwrites no element at all).parent.getFirstChildElement().isOrHasChild(target)is the guide’s test for "the click really hit what I rendered". -
super.onBrowserEvent(...)is called first becauseAbstractCellis what turns akeydownwith the Enter key into a call ofonEnterKeyDown. The cell must list"keydown"among its events for that to happen.
The cell is used like any other:
Column<Book, Integer> copies = new Column<Book, Integer>(new CopiesBadgeCell()) {
@Override
public Integer getValue(Book b) { return b.getCopies(); }
};
copies.setFieldUpdater((index, book, value) -> {
book.setCopies(value);
table.redrawRow(index); // the updater does not redraw anything by itself
});
table.addColumn(copies, "Copies");
and rendered, for the first two books of the lab (the second has no copies left), as this:
CellTable<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-27" tabindex="0"><span class="bk-badge" title="Row 0">3 left</span></div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-27"><span class="bk-badge bk-out" title="Row 1">none left</span></div></td>
What happens when the user clicks
A cell widget does not attach a handler to each cell.
When a column (or a CellList) is created, the widget asks every cell for getConsumedEvents() once and sinks exactly those events on its root element.
When the browser delivers one, the widget finds the row and column from the attributes in the markup (__idx, __gwt_row, __gwt_cell), builds the Context, offers the event to the selection manager as a CellPreviewEvent, and hands it to the cell.
Because the event list is read once, a cell that returns a different set later gets no new events: the guide’s warning is that the value of getConsumedEvents() must not change.
A cell that does not list an event type never receives it, however its HTML looks, and a Column whose cell takes events needs the FieldUpdater for a change to reach the row.
The lab clicked the badge of a table (the FieldUpdater only stored the new count) and of a second table (the updater also called redrawRow(index)), and read the cell right after:
| Updater stores the value only | Updater stores and calls redrawRow(index) |
|
|---|---|---|
Text of the badge before |
|
|
Text of the badge after the click |
|
|
Combining cells: CompositeCell and HasCell
One table cell can hold several cells.
CompositeCell<C> takes a List<HasCell<C, ?>>; each HasCell<C, X> names a Cell<X>, how to get its value X from the cell’s value C and an optional FieldUpdater<C, X>.
Each part is rendered in a <span> of its own, and the composite hands every event to the part that rendered the target.
The loan table above ends with an "Actions" column built this way (rendered <span>#3</span><span><button …>Renew</button></span>): a TextCell that shows the loan number and an ActionCell whose Delegate extends the due date.
IdentityColumn<T> is the column to use with it, because the composite’s value is the whole row.
IconCellDecorator<C> wraps one cell and puts an ImageResource in front of it (the constructor can also set the vertical alignment); it is the lightweight way to add an icon to a built-in cell without writing a new one.
Rules of thumb
-
Cells are shared: a field of the cell holds configuration (a format, a style name), never the row being rendered.
-
Escape everything that came from outside, including titles, author names, file names and data that comes from the server. Use
appendEscaped, aStringtemplate parameter, or aSafeHtmlRenderer; do not build markup by string concatenation and do not useappendHtmlConstanton data. -
Return early on
null. -
Keep a cell’s HTML small; it is generated for every visible row, on every redraw.
-
Make interactive parts reachable by keyboard: a
<button>is focusable, a<span>with a click handler is not; addtabindexand handle Enter, asClickableTextCelldoes, or use a button.
@UiRenderer: cells from UiBinder templates
Writing HTML in Java strings, even with templates, is awkward for anything with structure.
GWT 2.5 added a second way to produce a cell’s HTML: a UiBinder template with no widgets, only HTML elements, and a generated renderer.
You declare an interface that extends UiRenderer with a render method whose first parameter is a SafeHtmlBuilder, and the compiler writes the implementation from OwnerClass.ui.xml.
The lab’s AuthorChipCell renders an "author chip" (name and number of titles) for a value of the nested type Author.
AuthorChipCell.ui.xml<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder">
<ui:with field="name" type="java.lang.String"/>
<ui:with field="titles" type="java.lang.String"/>
<span ui:field="chip" class="bk-chip"><b><ui:text from="{name}"/></b> (<ui:text from="{titles}"/>)</span>
</ui:UiBinder>
AuthorChipCell.java)public class AuthorChipCell extends AbstractCell<AuthorChipCell.Author> {
public static class Author {
final String name;
final int titles;
public Author(String name, int titles) { this.name = name; this.titles = titles; }
}
interface Renderer extends UiRenderer {
void render(SafeHtmlBuilder sb, String name, String titles); // names match the ui:with fields
void onBrowserEvent(AuthorChipCell owner, NativeEvent event, Element parent, Author author);
SpanElement getChip(Element parent); // "get" + the ui:field name
}
private static final Renderer RENDERER = GWT.create(Renderer.class);
public AuthorChipCell() {
super("click");
}
@Override
public void render(Context context, Author author, SafeHtmlBuilder sb) {
if (author != null) {
RENDERER.render(sb, author.name, author.titles + (author.titles == 1 ? " title" : " titles"));
}
}
@Override
public void onBrowserEvent(Context context, Element parent, Author author, NativeEvent event,
ValueUpdater<Author> updater) {
RENDERER.onBrowserEvent(this, event, parent, author); // dispatches to @UiHandler methods
}
@UiHandler("chip")
void onChipClick(ClickEvent event, Element parent, Author author) {
RENDERER.getChip(parent).setTitle("Filter by " + author.name);
}
}
The rules, from the guide and from the generator’s source (UiBinderWriter):
-
The renderer interface may declare one
rendermethod, which must returnvoidand whose first parameter is aSafeHtmlBuilder. The other parameters are matched by name with the<ui:with>fields of the template. -
An
onBrowserEventmethod takes the owner (the object whose@UiHandlermethods are called), theNativeEvent, the parentElement, and then any arguments you like, which are passed on unchanged to the handlers. A handler is a method with@UiHandler("fieldName"); its first parameter selects the event type (ClickEvent) and the others repeat the extra arguments ofonBrowserEvent(here theElementand theAuthor). -
getFieldName(Element parent)returns an element markedui:field, so the cell can change the rendered markup after the fact (above, the handler sets atitleon the chip). A getter with no parameters whose name matches a<ui:style>returns the style object. -
The cell must still list the events (
super("click")) and forward the browser event to the renderer.
Two chips rendered like this (the second name is the text Eve & <Co>, escaped by the generated code):
<div __idx="0" class="cellListEvenItem cellListKeyboardSelectedItem" style="outline:none;" tabindex="0"><span class="bk-chip" gwtuirendered="gwt-uid-28"><b>Robert C. Martin</b> (2 titles)</span></div>
<div __idx="1" class="cellListOddItem" style="outline:none;"><span class="bk-chip" gwtuirendered="gwt-uid-29"><b>Eve & <Co></b> (1 title)</span></div>
Clicking the first chip ran the handler: the element’s title changed from the empty string to Filter by Robert C. Martin.
The Javadoc of UiRenderer in 2.13.1 still labels it experimental and unsupported, with an API that may change, and the guide gives it only a short section.
The guide documents it without that caveat; if you do not want to depend on an API with that label, a SafeHtmlTemplates interface gives the same safety with a little more Java.
The Bookshelf catalogue
In the running scenario, the Bookshelf’s catalogue is a DataGrid with a pager, three sortable columns and a button column, in which the browser never holds more than one page and the server does the paging and the sorting.
The pieces are the ones of the sequence diagram above.
Shared and server
bookshelf-shared)@RemoteServiceRelativePath("catalog")
public interface CatalogService extends XsrfProtectedService {
/** One page of the catalogue; sortField is one of title, author, year or copies (null = id order). */
BookPage getBooks(int start, int length, String sortField, boolean ascending);
Book getBook(long id) throws BookNotFoundException;
Book saveBook(Book book) throws BookNotFoundException;
}
public interface CatalogServiceAsync {
void getBooks(int start, int length, String sortField, boolean ascending, AsyncCallback<BookPage> callback);
void getBook(long id, AsyncCallback<Book> callback);
void saveBook(Book book, AsyncCallback<Book> callback);
}
public class BookPage implements Serializable {
private ArrayList<Book> books = new ArrayList<>();
private int total;
public BookPage() {}
public BookPage(List<Book> books, int total) { this.books = new ArrayList<>(books); this.total = total; }
public List<Book> getBooks() { return books; }
public int getTotal() { return total; }
}
The server maps the client’s text to a comparator from a fixed list, never to a property name chosen by the client, so a forged sortField cannot reach anything else, and it limits length so a client cannot ask for the whole table in one call.
(Authorisation of the call belongs on the server too, as always.)
bookshelf-server)public class CatalogServiceImpl extends XsrfProtectedServiceServlet implements CatalogService {
@Override
public BookPage getBooks(int start, int length, String sortField, boolean ascending) {
return CatalogRepository.INSTANCE.page(start, Math.min(length, 100), sortField, ascending);
}
…
}
/** Sorts on a whitelisted field only; an unknown field falls back to id order. */
public BookPage page(int start, int length, String sortField, boolean ascending) {
List<Book> all = new ArrayList<>(books.values());
Comparator<Book> order = comparatorFor(sortField);
if (order != null) {
all.sort(ascending ? order : order.reversed());
}
int from = Math.max(0, Math.min(start, all.size()));
int to = Math.max(from, Math.min(all.size(), from + Math.max(0, length)));
return new BookPage(all.subList(from, to), all.size());
}
private static Comparator<Book> comparatorFor(String field) {
if (field == null) {
return null;
}
switch (field) {
case "title": return Comparator.comparing(Book::getTitle).thenComparing(Book::getId);
case "author": return Comparator.comparing(Book::getAuthor).thenComparing(Book::getId);
case "year": return Comparator.comparingInt(Book::getYear).thenComparing(Book::getId);
case "copies": return Comparator.comparingInt(Book::getCopies).thenComparing(Book::getId);
default: return null;
}
}
The tie-break by id makes the order total: without it, two rows with the same author could swap places between two requests, and a row would appear on two pages or on none.
In a real application the sort would be a database ORDER BY with the same whitelist, and start and length a LIMIT and OFFSET (or, better for large tables, a keyset condition).
The presenter
The presenter is plain Java: it has no GWT.create call, so a JVM unit test runs it with a fake service.
It numbers the requests and ignores the answer of a request that is no longer the latest (the out-of-order problem described with the provider).
CatalogPresenter.javapublic class CatalogPresenter {
private final CatalogServiceAsync service;
private final CatalogDisplay display;
private int latestRequest;
public CatalogPresenter(CatalogServiceAsync service, CatalogDisplay display) {
this.service = service;
this.display = display;
}
public void load(int start, int length, String sortField, boolean ascending) {
final int request = ++latestRequest;
service.getBooks(start, length, sortField, ascending, new AsyncCallback<BookPage>() {
@Override
public void onSuccess(BookPage page) {
if (request == latestRequest) { // a newer range or sort order was requested meanwhile
display.showRows(start, page.getBooks(), page.getTotal());
}
}
@Override
public void onFailure(Throwable caught) {
if (request == latestRequest) {
display.showError(caught.getMessage());
}
}
});
}
}
Source: GWT Javadoc — AsyncCallback
The view
The view owns the DataGrid and the AsyncDataProvider; its onRangeChanged is the only place that knows about sorting.
The key provider (Book::getId) is given to the grid.
CatalogView.java (the data flow, abridged)public class CatalogView extends Composite implements CatalogDisplay {
public interface Listener {
void onRange(int start, int length, String sortField, boolean ascending);
void onOpen(Book book);
}
private final DataGrid<Book> grid = new DataGrid<>(10, Book::getId);
private final Map<Column<Book, ?>, String> sortFields = new HashMap<>();
private final AsyncDataProvider<Book> provider;
public CatalogView(Listener listener) {
provider = new AsyncDataProvider<Book>() {
@Override
protected void onRangeChanged(HasData<Book> display) {
Range range = display.getVisibleRange();
ColumnSortList sorts = grid.getColumnSortList();
String field = null;
boolean ascending = true;
if (sorts.size() > 0) {
field = sortFields.get(sorts.get(0).getColumn());
ascending = sorts.get(0).isAscending();
}
listener.onRange(range.getStart(), range.getLength(), field, ascending);
}
};
addSortable(titleColumn, msg.title(), "title");
addSortable(authorColumn, msg.author(), "author");
addSortable(copiesColumn, msg.copiesHeader(), "copies"); // copiesColumn also styles books with no copies
grid.addColumn(openColumn); // a ButtonCell column with a FieldUpdater
// Sorting clears the data and fires a RangeChangeEvent, so onRangeChanged runs again.
grid.addColumnSortHandler(new ColumnSortEvent.AsyncHandler(grid));
SimplePager pager = new SimplePager();
pager.setDisplay(grid);
… // DockLayoutPanel: pager south, grid center
}
private void addSortable(Column<Book, ?> column, String header, String sortField) {
column.setSortable(true);
sortFields.put(column, sortField);
grid.addColumn(column, header);
}
/** Starts the data flow. Call it once the presenter exists: this triggers the first range request. */
public void connect() { provider.addDataDisplay(grid); }
@Override
public void showRows(int start, List<Book> rows, int total) {
provider.updateRowCount(total, true);
provider.updateRowData(start, rows);
}
}
Three details are easy to get wrong and were all exercised:
-
connect()is separate from the constructor.addDataDisplaycallsonRangeChangedat once. In an earlier version of the view the display was added in the constructor, before the activity had created the presenter; the first range request hit anulllistener, the exception was thrown insideActivity.start, the widget was never attached, and nothing was shown, silently, because the uncaught-exception handler only logged. The activity now creates the view, then the presenter, shows the view and then callsconnect(). -
The sort field comes from the column, through a map filled in
addSortable, so adding a sortable column is one line and the provider never learns column positions. -
Sorting keeps the page.
AsyncHandlerreuses the current range, so sorting on page 2 asks for rows 10 to 19 of the sorted list. If the user expects to go back to the top, callgrid.setVisibleRange(0, grid.getPageSize())(orpager.firstPage()) in a sort handler of your own.
It running
The scratch application ran with mvn package and the Jetty 12 plugin (jetty-ee10-maven-plugin) with the XSRF flow in place.
Three interactions in the browser produced these requests: a click on "Title", then the next-page button, then a click on "Copies" while on page 2:
… |com.example.bookshelf.shared.CatalogService|getBooks|I|java.lang.String/2004016611|Z|title|1|2|3|4|5|6|4|7|7|8|9|0|10|10|1|
… |com.example.bookshelf.shared.CatalogService|getBooks|I|java.lang.String/2004016611|Z|1|2|3|4|5|6|4|7|7|8|9|10|10|0|1|
… |com.example.bookshelf.shared.CatalogService|getBooks|I|java.lang.String/2004016611|Z|copies|1|2|3|4|5|6|4|7|7|8|9|10|10|10|1|
In the first line the four arguments are I, I, String and Z (the types are listed after the method name), then four values: 0 (start), 10 (length), the string number 10 (title) and 1 (ascending is true).
The second line is the request for page 2 (start 10, length 10, then 0 for the null sort field, ascending).
The third is the click on "Copies" while on page 2: start 10, length 10, copies, ascending, as noted above.
Each answer was //OK[…] with a BookPage of ten books and the total 25.
| Action | Result |
|---|---|
Load |
Pager "1-10 of 25", ten rows of the 25 seeded books in id order |
Click "Title" |
First row "Clean Code" (the books sorted by title) |
Click "Title" again |
First row "Working Effectively with Legacy Code (copy 4)" (descending) |
Next page |
Pager "11-20 of 25" |
The grid’s markup is what the earlier excerpts showed, with a sort arrow image in the sorted header, a row per book and the Open button column:
<tr __gwt_row="0" __gwt_subrow="0" class="…">
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-55">Working Effectively with Legacy Code (copy 4)</div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-56">Michael Feathers</div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-57">3 copies</div></td>
<td class="…"><div style="outline-style:none;" __gwt_cell="cell-gwt-uid-58" tabindex="0"><button type="button" tabindex="-1">Open</button></div></td>
</tr>
Tests
The scratch application’s tests ran with mvn verify on JDK 21: 4 GWTTestCase tests of the client (HtmlUnit), 1 for Spanish messages, 4 plain JUnit tests of the presenter and 3 of the server, with no failures.
The ones that matter for this page are
-
a JUnit test that the presenter passes
yearanddescto the service, -
a JUnit test with a fake service that holds both answers back, answers the second request first and then the first, and checks that the display shows the second,
-
a
GWTTestCasetest that builds theCatalogView, connects it, checks that the first request is0/10/null/true, pushes the author column on the sort list, firesColumnSortEventand checks that the listener received0/10/author/true.
The Testing page describes GWTTestCase, HtmlUnit and the Maven setup.
Accessibility and keyboard use
The cell widgets bring some of the accessibility work, and leave the rest to the cells.
What the lab’s markup showed: sortable table headers have role="button" and tabindex="-1"; the SimplePager buttons are <img> elements with role="button", an aria-label ("First page", "Previous page", "Next page", "Fast forward") and aria-disabled; CellTree sets role="tree" and gives each node role="treeitem" with aria-level, aria-setsize, aria-posinset, aria-expanded and aria-selected; the focused row or cell has tabindex="0".
The pager labels come from SimplePager.ImageButtonsConstants, a Constants interface, so they are translated with the module’s locale.
What to do in your own cells: use a real <button> or <input> for actions (they are focusable and announced), give an icon-only control an aria-label in the template, handle Enter and not only the click, and keep KeyboardSelectionPolicy at ENABLED or BOUND_TO_SELECTION rather than DISABLED unless another way to reach the rows exists.
A table also needs an accessible name; give the widget’s element an aria-label or aria-labelledby yourself.
The ARIA roles and states API of GWT (com.google.gwt.aria.client.Roles) and the rest of the guidance are on the Accessibility & ARIA page.
Source: GWT Javadoc — SimplePager
Pitfalls
Most mistakes with cell widgets are silent: the page is blank, a number is stale or a selection disappears, and nothing is logged. The first column quotes what the compiler or the browser printed, or says what was observed; each row was reproduced with the 2.13.1 compiler and headless Chrome, or comes from the source named in the last column.
| Symptom (quoted or observed) | Cause | Fix | Evidence |
|---|---|---|---|
A |
|
Put it in a layout panel ( |
Lab: |
A cell shows |
A |
Change the model, then |
Lab: |
Changing a field of an item in the provider’s list and calling |
The provider only sees changes made through the list wrapper, not changes inside the items. |
|
Lab: |
The pager says |
The provider pushed rows but never called |
Call |
Lab: |
The selection disappears every time a page is loaded. |
No |
Pass the same key provider to the widget, the provider and the selection model. |
Lab: |
The grid is blank, and the uncaught exception handler only logged: a |
|
Add the display in a separate |
Seen in the Bookshelf scratch application |
Clicking a header changes the arrow but not the rows. |
|
|
Source: |
A sortable column on a server-backed table sorts nothing. |
No |
|
Source: |
Sorting while on page 2 shows rows 11 to 20 of the sorted list, not the top. The request had start |
|
Reset the range in your own handler if you want the first page. |
Browser run, third payload above |
A slow old answer replaces a newer one: a sorted page flashes back to the unsorted rows. |
Responses arrive out of order; the provider does not know which request is newer. |
Number the requests in the presenter and drop stale answers. |
JUnit test with a fake service |
A click on a custom cell does nothing, or Enter does not work. |
The event type was not passed to |
List every event the cell uses and keep the set constant. |
Source: |
|
A |
Make the number of parameters match the highest placeholder. |
Compiler output |
|
A |
Pass a |
Compiler output |
|
The |
Declare |
Compiler output |
|
A |
Use the type declared in |
Compiler output |
A |
A |
Name the parameters exactly like the fields. |
Lab: the span was empty |
|
A style class used by the cells (for example through |
Add an accessor ( |
Reproduced earlier in the scratch application (GSS styles) |
|
|
Set |
Reproduced with |
What changed since the older material
-
The book predates cell widgets. The 2007 first edition (GWT 1.3 and 1.4) builds tables from
GridandFlexTablepanels with widgets in the cells and listener interfaces for events, as did every GWT application until 2010. Cell widgets arrived with the first 2.1 milestone (19 May 2010; 2.1.0 final 27 October 2010), announced together with an application framework for large business applications. None of the cell classes in this page is in the book. -
DataGridis a 2.4.0 feature (7 September 2011): a scrolling table with a fixed header and footer. -
CellTableBuilder, custom headers and footers, and UiBinder rendering of cells (theUiRendererof this page) came with the 2.5.0 release candidates (27 June 2012; 2.5.0 final 25 October 2012). -
Selection by event manager.
DefaultSelectionEventManagerand its factories are how current code chooses what selects. The 2.5.0 release notes listAbstractCellTable.doSelection(Event,Object,int,int),AbstractHasData.onUpdateSelection()andCellList.doSelection(Event, Object, int)among the removed methods, so code from the 2.1 and 2.2 era that overrides them does not compile any more. The same notes record that theLEFTandRIGHTplacement of the text inSimplePagerwas fixed, which may swap it for applications that had worked around the bug. -
Deprecated or stale API in the guide’s examples.
CheckboxCell(boolean isSelectBox)is deprecated in favour of the two-argument constructor. The guide saysActionCellacts on mouseUp; in 2.13.1 it consumesclickandkeydown. The sort examples refer to aTester.Contacttype that exists only in the file they were copied from. The table of contents of the cell widget guide links to#cellbrowser, an anchor the page does not define, and the guide’sAsyncDataProviderexample pushes rows withcellList.setRowDatainstead ofupdateRowDataon the provider. -
Showcase link. The guide’s demo link uses
http://samples.gwtproject.org/samples/Showcase/Showcase.html#!CwCellSampler; the same page over https is listed in the references. -
J2CL. Cell widgets ship in
gwt-user. Among theorg.gwtproject.*libraries that are published for J2CL (all still 1.0.0 release candidates when checked on 2026-10-08) there is nogwt-user-style module that contains them, so a cell-widget application is a GWT application today (see J2CL & the Future).
Source: GWT — Release Notes