DOM & Elemental2
|
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 Elemental2 1.3.2 (the latest stable release, 2025-09-15), checked on 2026-10-08.
It is client knowledge: everything here is Java that the GWT compiler translates to JavaScript and that runs inside the browser.
The shared and server modules of the Bookshelf never touch the DOM, and the build only needs three extra Maven dependencies and four <inherits> lines.
Every output block was produced by compiling a probe module with the 2.13.1 compiler (and, for the Elemental2 dependencies, with mvn package of the Bookshelf client) and loading it in headless Chrome 155; the numbers and messages are quoted as they appeared.
Which API for which job
A GWT application can reach the browser through six different layers.
They are not alternatives that replace each other: a normal application uses widgets for the user interface, com.google.gwt.dom.client inside its own widgets, and Elemental2 for the Web APIs that GWT never wrapped.
| Layer | Where it lives | State in 2.13.1 | Use it for |
|---|---|---|---|
Widgets and panels |
|
current |
everything with a ready-made widget; reach the element with |
|
|
current and stable; typed JavaScript overlay types |
creating and changing elements, styles and nodes inside your own widgets |
|
|
|
sinking events in a custom widget, native event preview |
JSNI |
|
legacy |
existing code only; see JSNI & Overlay Types |
Elemental2 |
|
maintained; 1.3.2 stable, 1.4.0-RC1 published 2026-07-07 |
fetch, Promise, JSON, URL, WebSocket, web storage, |
Elemental (1) |
|
deprecated since GWT 2.9, not in the 2.13.1 source tree |
nothing new; migrate to Elemental2 |
The Developer Guide page on the DOM gives the same advice in one sentence: the widgets wrap the DOM so that you do not have to care about the browser, but you may need the DOM to add a feature GWT lacks, write a new widget, reach an element of the host page or handle events at a low level. This page covers those low-level cases and the modern replacement for the parts of the browser API that GWT never wrapped.
com.google.gwt.dom.client: typed overlays
Every class in com.google.gwt.dom.client is a JavaScript overlay type: a Java class that extends JavaScriptObject and whose methods are final, mostly native, and compile to direct property accesses on the real browser object.
Document, Node, Element, Style, NodeList, NativeEvent and the element subclasses (DivElement, SpanElement, InputElement, ButtonElement, TableElement and so on) are therefore not wrappers: card.getId() becomes card.id, no object is allocated, and a DivElement and the HTML div it stands for are the same value.
The class layout follows the DOM interfaces:
-
Nodehas the tree operations:appendChild,insertBefore,insertFirst,removeChild,removeFromParent,replaceChild,cloneNode,getFirstChild,getParentElement,getChildCount,isOrHasChild,getOwnerDocument -
Elementadds identity and presentation:getId/setId,getClassName/setClassName,addClassName,removeClassName,hasClassName,toggleClassName, attributes (getAttribute,setAttribute,hasAttribute,removeAttribute), properties (getPropertyString,setPropertyString,getPropertyIntand friends), geometry (getOffsetWidth,getClientHeight,getAbsoluteLeft,getScrollTop),getStyle,setInnerText,setInnerHTML,setInnerSafeHtml,focus,blur,scrollIntoView,dispatchEvent -
Documentis the factory and the entry point:Document.get(),getBody,getHead,getElementById,getElementsByTagName,getTitle,getURL,getReferrer,createUniqueId,isCSS1Compat, and onecreateXxxElement()method per HTML tag, pluscreateClickEvent,createKeyDownEventand the other synthetic-event factories -
Styleis the inlinestyleattribute with typed setters,Style.Unit(PX,EM,PCTand the rest), the enumsStyle.Display,Style.Cursor,Style.Float,Style.FontWeightand others, and the escape hatchsetProperty(String, String)for anything not enumerated
The example creates a card for a book, styles it and finds it again.
The title goes through setInnerText, so the <script> in the book title stays text.
Document doc = Document.get();
DivElement card = doc.createDivElement();
card.setId("book-42");
card.setClassName("card");
card.setAttribute("data-isbn", "978-0-00-000000-0");
SpanElement title = doc.createSpanElement();
title.setInnerText("Dune <script>alert(1)</script>"); // text, never markup
card.appendChild(title);
card.getStyle().setDisplay(Display.INLINE_BLOCK);
card.getStyle().setWidth(12, Unit.EM);
card.getStyle().setBackgroundColor("#ffee80");
card.getStyle().setProperty("borderLeft", "4px solid #3366cc");
doc.getBody().appendChild(card);
Element found = doc.getElementById("book-42");
DivElement div = DivElement.as(found);
NodeList<Element> spans = doc.getElementsByTagName("span");
JavaScriptObject raw = card; // every overlay is a JavaScriptObject
The log of the probe module shows what the browser holds afterwards.
getString() is the serialisation of the element (it is not toString(), which is final in JavaScriptObject), and the script text was escaped:
outer : <div id="book-42" class="card" data-isbn="978-0-00-000000-0" style="display: inline-block; width: 12em; background-color: rgb(255, 238, 128); border-left: 4px solid rgb(51, 102, 204);"><span>Dune <script>alert(1)</script></span></div>
width : 12em / offsetWidth 196
found : true, tag DIV, isDiv true
children: 1, first SPAN
spans : 1, text 'Dune <script>alert(1)</script>'
is(raw) : true, as -> book-42
removed : parent false
Source: Javadoc — Element
Points worth knowing before you write more:
-
Narrowing is explicit.
Document.getElementByIdreturnsElement. Use the staticasof the specific class (DivElement.as(found)) when you know the tag,Element.is(JavaScriptObject)to test whether an object is an element at all, andElement.as(JavaScriptObject)or the genericcast()ofJavaScriptObjectto go from an event target to an element. Because overlay types carry no runtime type, a wrongasdoes not fail at once: it fails later, when you call a method the real object lacks. -
Styleis the inline style only.card.getStyle().getWidth()returned12em, the inline value (the generated code assigns12.0em, which the browser normalises), not the computed pixel width (getOffsetWidth()returned 196). Computed styles are not part ofdom.client; prefer CSS classes (setStyleName,addClassName) and the ClientBundle/GSS pages of this section for presentation, as the Developer Guide recommends. -
Property names for
setPropertyare the camel-case DOM names (borderLeft, notborder-left); the string is assigned toelement.style[name]. -
Text and markup are different methods.
setInnerTextescapes;setInnerHTMLis annotated@IsSafeHtmland is meant to receive a constant or aSafeHtmlvalue, andsetInnerSafeHtml(SafeHtml)takes the type directly. Never concatenate user data intosetInnerHTML; the SafeHtml and XSS page of this section covers the builders and sanitisers. -
getElement()still returns the oldcom.google.gwt.user.client.Element, a deprecated subclass ofdom.client.Element. Assign it to acom.google.gwt.dom.client.Elementvariable and the deprecation never leaks into your code.
What dom.client deliberately lacks
The overlay model keeps the API small, and the gaps are the reason Elemental2 exists.
dom.client has no addEventListener, no removeEventListener, no querySelector/querySelectorAll, no getElementsByClassName, no classList, no dataset, no fetch.
The Javadoc of EventTarget states the first one openly: the class intentionally does not specify dispatchEvent, addEventListener and removeEventListener; events reach Java through sinkEvents and handlers instead (next section).
Calling a method that is not there fails at compile time with the usual message for a member that is missing from an overlay:
[ERROR] Line 10: The method addEventListener(String, null) is undefined for the type BodyElement
[ERROR] Line 11: The method querySelector(String) is undefined for the type BodyElement
Source: GWT 2.13.1 — EventTarget.java
DOM and Event: the widget event machinery
The package com.google.gwt.user.client holds the older, event-oriented half of the DOM support: the static helper class DOM, the class Event (which extends NativeEvent), the interface EventListener and the Window helpers.
Two things in it are current and you will meet them in every custom widget: sinking events and native preview.
The rest of the static DOM API (DOM.getInnerText, DOM.setAttribute, DOM.eventGetKeyCode, DOM.compare; about fifty methods of the class carry the annotation) is @Deprecated in 2.13.1 in favour of the methods on Element and NativeEvent, and DOM.addEventPreview/Event.addEventPreview give way to Event.addNativePreviewHandler.
Sinking events and onBrowserEvent
A browser element only fires an event to GWT if the element is told to.
sinkEvents(int bits) on a Widget (or DOM.sinkEvents(Element, int) for a bare element) registers interest in the event types whose bits are or-ed together; onBrowserEvent(Event) is then called on the widget when one of them reaches it.
The bit constants live on Event; the values are the ones the 2.13.1 DOMImplStandard switches on:
| Constant | Value | Constant | Value |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The group constants FOCUSEVENTS, KEYEVENTS, MOUSEEVENTS, TOUCHEVENTS and GESTUREEVENTS combine the related bits.
Event.UNDEFINED is deprecated.
For events that have no bit (for example dragstart, input, transitionend) the widget layer uses sinkBitlessEvent(String), which the addDomHandler mechanism calls for you.
Sinking is lazy for widgets: until a Widget is attached, sinkEvents only records the bits (eventsToSink), and onAttach applies them.
That detail matters for the lifecycle section below.
The following widget is built directly on a DOM element, counts clicks and still lets ordinary handlers be added to it:
static class ClickCounter extends Widget {
private int clicks;
ClickCounter() {
setElement(Document.get().createDivElement());
getElement().setId("counter");
sinkEvents(Event.ONCLICK);
render();
}
@Override
public void onBrowserEvent(Event event) {
switch (event.getTypeInt()) {
case Event.ONCLICK:
clicks++;
render();
Element target = Element.as(event.getEventTarget());
Log.log("onBrowserEvent: click on <" + target.getTagName().toLowerCase() + ">, clicks=" + clicks);
break;
default:
break;
}
super.onBrowserEvent(event); // lets registered handlers (addDomHandler) run
}
private void render() {
getElement().setInnerText("clicks: " + clicks);
}
}
Source: Javadoc — Widget
The caller adds an ordinary ClickHandler, a native preview handler and then dispatches a synthetic click built with Document.createClickEvent, which is also how GWT’s own tests drive widgets:
ClickCounter counter = new ClickCounter();
counter.addDomHandler(new ClickHandler() {
@Override public void onClick(ClickEvent e) {
Log.log("ClickHandler : client(" + e.getClientX() + "," + e.getClientY() + ")");
}
}, ClickEvent.getType());
HandlerRegistration preview = Event.addNativePreviewHandler(p -> {
NativeEvent ne = p.getNativeEvent();
if ("click".equals(ne.getType())) {
Log.log("native preview : " + ne.getType() + " firstHandler=" + p.isFirstHandler());
}
});
RootPanel.get().add(counter);
NativeEvent click = Document.get().createClickEvent(1, 0, 0, 12, 34, false, false, false, false);
counter.getElement().dispatchEvent(click);
Log.log("text after click: " + counter.getElement().getInnerText());
preview.removeHandler();
RootPanel.get().remove(counter);
native preview : click firstHandler=true
onBrowserEvent: click on <div>, clicks=1
ClickHandler : client(12,34)
text after click: clicks: 1
The order is the design: the native preview handlers see the event first, then the widget’s onBrowserEvent, and Widget.onBrowserEvent finally calls DomEvent.fireNativeEvent, which runs the handlers registered with addDomHandler (and therefore every addClickHandler).
Forgetting super.onBrowserEvent(event) in an override silently disables all of those handlers; this is the most common mistake in a hand-written widget.
The Events & Handlers page covers the handler types, HandlerManager and event buses in detail.
Suppressing the browser’s default action is done on the native event: NativeEvent.preventDefault() stops the default action (a link navigation, a form submit) and NativeEvent.stopPropagation() stops the event bubbling to ancestors; the handler types pass the NativeEvent through DomEvent.preventDefault() and DomEvent.stopPropagation().
Native preview
Event.addNativePreviewHandler(Event.NativePreviewHandler) registers a global handler for every native event that GWT sinks, before it is delivered to its widget.
It returns a HandlerRegistration.
Inside, NativePreviewEvent.cancel() cancels the native event so that it is not delivered, consume() marks it as handled and prevents any handler from cancelling it (even one that already called cancel()), and isFirstHandler() tells whether the current handler is the first to preview this event.
The Developer Guide’s keyboard-shortcut example uses exactly this API:
Event.addNativePreviewHandler(new NativePreviewHandler() {
public void onPreviewNativeEvent(NativePreviewEvent preview) {
NativeEvent event = preview.getNativeEvent();
Element elt = event.getEventTarget().cast();
int keycode = event.getKeyCode();
if (event.getCtrlKey() || event.getAltKey() || !isInterestingKeycode(keycode)) {
return; // let the event continue
}
handleKeycode(keycode);
preview.consume(); // mark the event as handled
}
});
A preview handler lives until it is removed, whatever happens to the widget that created it. This is the main source of leaks in real GWT applications and is the subject of the lifecycle section further down.
The other two examples of the Developer Guide page (changing a widget’s background colour through getElement().getStyle(), and finding anchor tags with a JSNI helper) map to today’s API like this:
| Developer Guide example | Today |
|---|---|
|
unchanged; a CSS class with |
JSNI helper that loops over |
|
|
unchanged |
Browser differences
The Developer Guide’s "Cross-Browser Support" page promises that a GWT application built from the built-in widgets works the same on the major browsers, that GWT’s Button is a real <button>, and that styling should be done in style sheets rather than through the API.
Its list of browsers (Internet Explorer, Firefox, Chrome, Safari, Opera) and the JSNI navigator.userAgent tip are older than the compiler: the 2.13.1 module com.google.gwt.useragent.UserAgent defines exactly two values of the user.agent property.
<define-property name="user.agent" values="gecko1_8" />
<extend-property name="user.agent" values="safari" />
<property-provider name="user.agent" generator="com.google.gwt.useragent.rebind.UserAgentPropertyGenerator"/>
Source: GWT 2.13.1 — UserAgent.gwt.xml
The property provider is a few lines of JavaScript that tests the lower-cased navigator.userAgent: it answers safari if the string contains webkit, gecko1_8 if it contains gecko, and the check for safari comes first.
Chrome, Edge and every other Chromium browser put both AppleWebKit and like Gecko in their user-agent string, so they select the safari permutation; Firefox (Gecko without WebKit) selects gecko1_8.
The compiler therefore produces two permutations per locale, which is why the Bookshelf (three locale values of its own module) compiled six, and the one-locale probe module two:
Compiling module com.example.bookshelf.DomDemo
Compiling 2 permutations
Compiling permutation 0...
Compiling permutation 1...
Compile of permutations succeeded
The probe module asks the generated UserAgent for its two values, and the browser that ran it was Chrome:
UserAgent ua = GWT.create(UserAgent.class);
Log.log("user.agent : compile-time=" + ua.getCompileTimeValue() + " runtime=" + ua.getRuntimeValue());
Log.log("navigator : " + DomGlobal.navigator.userAgent);
Source: Javadoc — UserAgent
user.agent : compile-time=safari runtime=safari
navigator : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/155.0.0.0 Safari/537.36
Only Chromium was available when this page was written, so the gecko1_8 permutation was compiled but not executed.
What the permutation selects is the implementation of the low-level DOM operations.
The module com.google.gwt.dom.DOM replaces the abstract class com.google.gwt.dom.client.DOMImpl by DOMImplWebkit for safari and by DOMImplMozilla for gecko1_8, and com.google.gwt.user.DOM does the same for the event system with com.google.gwt.user.client.impl.DOMImpl:
<replace-with class="com.google.gwt.dom.client.DOMImplWebkit">
<when-type-is class="com.google.gwt.dom.client.DOMImpl"/>
<when-property-is name="user.agent" value="safari"/>
</replace-with>
<replace-with class="com.google.gwt.dom.client.DOMImplMozilla">
<when-type-is class="com.google.gwt.dom.client.DOMImpl"/>
<when-property-is name="user.agent" value="gecko1_8"/>
</replace-with>
Source: GWT 2.13.1 — dom/DOM.gwt.xml
The differences these classes still absorb are small and visible in the source: WebKit events sometimes target the text node inside an element, so DOMImplWebkit.eventGetTarget walks to the parent; WebKit needs the webkitUserDrag style to make an element draggable; the Gecko class works around a relatedTarget bug and carries version checks from Firefox 3 and 4.
The deferred-binding mechanism behind this (the GWT.create call, <replace-with> and <when-property-is>) has its own page, Deferred Binding & Generators, and the Compiler & Compilation page explains how the number of permutations is controlled.
What GWT does not hide, and what remains your job:
-
CSS and layout. Differences in default styles, flexbox/grid support and font metrics are browser behaviour.
Document.isCSS1Compat()reports standards versus quirks mode (getCompatMode()is"CSS1Compat"in standards mode); always give the host page a doctype. -
Web APIs that are absent or partial.
dom.clientwraps the 2000s-era DOM. Anything newer (fetch,WebSocket,IntersectionObserver,ResizeObserver) is a global that must exist in the user’s browser. Use feature detection (Js.isTruthyon a global, or atryaround the call) rather than the user-agent string. -
The
webApiUsageproperty.stableormodern(defaultmodern): withmodernsome widgets may use new, not yet standardised APIs. Choosestableif redeploying whenever a browser changes would be a problem. The property and theuser.agent.runtimeWarningconfiguration property are defined in the same module as above. -
Mismatch warnings. The module comment describes
user.agent.runtimeWarningas the switch for warnings about a compile-timeuser.agentthat differs from the runtime one; it istrueby default. This happens when a proxy serves one browser’s file to another, or when-setProperty user.agent=gecko1_8is used for a fast development compile and the page is then opened in Chrome.
For a browser-specific dependency the Developer Guide shows a JSNI navigator.userAgent method; the same value is a field in Elemental2 (DomGlobal.navigator.userAgent), as the log above shows.
Elemental2
Elemental2 gives Java code type-checked access to the browser’s API.
Its README explains the method: the types are generated from the externs files of the Closure Compiler and are native JsInterop types (@JsType(isNative = true, namespace = JsPackage.GLOBAL)), part of the JsInterop specification that GWT 2 and J2CL both implement.
A native type has no Java implementation and no JavaScript glue; the compiler emits direct accesses on the browser’s own objects (the next subsection shows the output).
Elemental2 requires GWT 2.9 or later (since its 1.0.0), and the same dependencies work on J2CL, which makes it the portable choice for new code.
What the compiler emits
Compiling the probe module with -style PRETTY for one permutation shows how thin both layers are.
The dom.client code from the first example became (the Java setInnerText is a textContent assignment, and the Style setters write the inline style object):
doc = $doc;
card = doc.createElement('div');
card.id = 'book-42';
card.className = 'card';
card.setAttribute('data-isbn', '978-0-00-000000-0');
title_0 = doc.createElement('span');
title_0.textContent = 'Dune <script>alert(1)<\/script>';
card.appendChild(title_0);
card.style['display'] = ($clinit_Style$Display() , 'inline-block');
card.style['width'] = ($clinit_Style$Unit() , '12.0em');
card.style['backgroundColor'] = '#ffee80';
and the Elemental2 example became:
box = castToNative(($clinit_DomGlobal() , document_0).createElement('div'), $wnd.HTMLDivElement);
box.id = 'box';
box.className = 'panel';
box.textContent = 'Add a book';
input_0 = castToNative(document_0.createElement('input'), $wnd.HTMLInputElement);
input_0.type = 'text';
input_0.placeholder = 'Title';
button = castToNative(document_0.createElement('button'), $wnd.HTMLButtonElement);
button.textContent = 'Add';
box.appendChild(input_0);
box.appendChild(button);
document_0.body.appendChild(box);
Member names of native types are never renamed, so the output is the JavaScript a person would write.
A Java cast to a native class is not free of checks: it becomes castToNative(value, $wnd.HTMLDivElement), a check against the browser’s own constructor.
Source: Elemental2 — README
Modules and coordinates
Each Elemental2 module is a separate Maven artifact (groupId com.google.elemental2) with its own GWT module.
The artifacts include their .java sources next to the classes, which GWT requires.
| Artifact | GWT module to inherit | Contents |
|---|---|---|
|
|
the JavaScript language objects: |
|
|
|
|
|
|
|
|
|
|
|
IndexedDB, media/audio/video, SVG DOM, WebGL; listed in the project README, not used on this page |
The artifact elemental2-webassembly is also on Maven Central at 1.3.2 but is not in the README table.
The dependencies of the four modules used here form a chain that Maven resolves for you: elemental2-dom depends on -core and -promise, -core on -promise, and -webstorage on -dom and -core.
All of them declare jsinterop-annotations 2.0.2, jsinterop-base 1.1.1 and jspecify 1.0.0.
The Bookshelf client POM adds the four artifacts with an explicit version, because the org.gwtproject:gwt BOM manages GWT and jsinterop-annotations but not Elemental2:
<dependency>
<groupId>com.google.elemental2</groupId>
<artifactId>elemental2-dom</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.google.elemental2</groupId>
<artifactId>elemental2-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.google.elemental2</groupId>
<artifactId>elemental2-promise</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.google.elemental2</groupId>
<artifactId>elemental2-webstorage</artifactId>
<version>1.3.2</version>
</dependency>
and the module file inherits the four GWT modules:
<inherits name="elemental2.dom.Dom" />
<inherits name="elemental2.core.Core" />
<inherits name="elemental2.promise.Promise" />
<inherits name="elemental2.webstorage.WebStorage" />
Source: Elemental2 — README
With both in place, mvn -B package -pl bookshelf-client -am compiled the Bookshelf client (a call to DomGlobal.console.log was added to App) with six permutations and BUILD SUCCESS.
Maven resolved jsinterop-annotations to 2.0.0 and jsinterop-base to 1.1.1: the GWT BOM pins the first below the 2.0.2 that the Elemental2 POMs declare, and jsinterop-base 1.2.0 (2026-06-30) exists but nothing forces it.
Both combinations compiled and ran; if you need a newer jsinterop-base, declare it in dependencyManagement.
Forgetting the GWT module is the usual first error. The classes are on the classpath, so javac is happy, but the GWT compiler only reads source that a module puts on its source path:
[ERROR] Line 8: No source code is available for type elemental2.dom.DomGlobal; did you forget to inherit a required module?
[ERROR] Hint: Check the inheritance chain from your module; it may not be inheriting a required module or a module may not be adding its source path entries properly
Elemental2 next to dom.client
Both families describe the same browser objects, so a value can be moved between them without a conversion: use Js.cast (from jsinterop-base) to treat a GWT overlay as an Elemental2 type, and Js.uncheckedCast to go back.
This is the practical way to get classList and querySelector on an element that a GWT widget owns:
com.google.gwt.dom.client.DivElement gwtDiv = Document.get().createDivElement();
gwtDiv.setInnerHTML("<p class=\"lead\">Hello</p>"); // constant markup only
HTMLElement el = Js.cast(gwtDiv); // GWT overlay -> Elemental2
el.classList.add("highlight");
Log.log("e2 bridge : " + el.querySelector("p.lead").textContent + " / " + gwtDiv.getClassName());
com.google.gwt.dom.client.Element back = Js.uncheckedCast(el); // Elemental2 -> GWT overlay
Log.log("e2 bridge : back to GWT overlay, same object = " + (back == gwtDiv));
e2 bridge : Hello / highlight
e2 bridge : back to GWT overlay, same object = true
The two libraries use the same simple names (Element, Event, Document, Window, Node, Storage), so a class that imports both must fully qualify one of them, as the bridge example does.
Prefer to import the Elemental2 names in classes that use Elemental2 and keep GWT’s Event out of them; the compiler error for a clash is an ordinary Java "already defined" or "cannot be resolved" message.
DOM and events with Elemental2
Creating elements and listening to events with Elemental2 looks like the JavaScript it replaces, with Java types:
HTMLDivElement box = (HTMLDivElement) DomGlobal.document.createElement("div");
box.id = "box";
box.className = "panel";
box.textContent = "Add a book";
HTMLInputElement input = (HTMLInputElement) DomGlobal.document.createElement("input");
input.type = "text";
input.placeholder = "Title";
HTMLButtonElement button = (HTMLButtonElement) DomGlobal.document.createElement("button");
button.textContent = "Add";
box.appendChild(input);
box.appendChild(button);
DomGlobal.document.body.appendChild(box);
onClick = e -> {
HTMLElement target = Js.cast(e.target);
Log.log("e2 click : type=" + e.type + " target=" + target.tagName);
};
button.addEventListener("click", onClick);
button.click();
button.removeEventListener("click", onClick);
button.click(); // nothing is logged: the listener is gone
e2 outerHTML : <div id="box" class="panel">Add a book<input type="text" placeholder="Title"><button>Add</button></div>
e2 click : type=click target=BUTTON
e2 keydown : Enter
e2 query : 2 children via querySelectorAll
Things to notice:
-
createElementreturnsHTMLElement; cast it to the specific class (HTMLDivElement,HTMLInputElement,HTMLButtonElement) to reachtype,placeholder,value. A cast to a specific class is checked against the browser’s constructor (see the compiler output above), so a wrong cast fails at once. -
Event targets are typed loosely.
e.targetis anEventTarget; convert withJs.cast. Event subtypes (KeyboardEvent,MouseEvent) are obtained the same way, and constructors exist for synthetic events:new KeyboardEvent("keydown", init)withKeyboardEventInit.create()andinit.setKey("Enter")produced the third log line. -
Removal needs the same instance.
removeEventListenercompares the listener object, so a second, identical-looking lambda removes nothing. The probe shows it:
int[] hits = {0};
button.addEventListener("click", e -> hits[0]++);
button.removeEventListener("click", e -> hits[0]++); // a different lambda instance: removes nothing
button.click();
EventListener same = e -> hits[0] += 10;
button.addEventListener("click", same);
button.removeEventListener("click", same);
button.click();
e2 identity : hits after remove with a new lambda = 1
e2 identity : hits after removing the same instance = 2
Keep the EventListener in a field (as onClick above) whenever you intend to remove it.
-
innerHTMLis a plainStringfield. Elemental2 has noSafeHtmltype, soelement.innerHTML = userTextis a DOM-based XSS hole. AssigntextContentfor text, or build markup with GWT’sSafeHtmlbuilders (see SafeHtml & XSS) and pass theasString()of aSafeHtmlonly.
fetch and Promise
DomGlobal.fetch(...) returns an Elemental2 Promise<Response>.
Promise has then, catch_ and finally_ (the underscore avoids the Java keywords), and the static resolve, reject, all and race.
There is no await in Java, so asynchronous steps are chained.
The Bookshelf REST endpoint of the server module is read like this; the first then converts an HTTP error into a rejection, the second parses the text with JSON.parse and walks the array through two small native types:
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
final class BookJson {
public double id;
public String title;
public String author;
public int year;
public int copies;
}
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
final class BookPageJson {
public int total;
public JsArray<BookJson> books;
}
static Promise<Void> fetchBooks() {
return DomGlobal.fetch("/api/books?start=0&length=3")
.then(res -> {
if (!res.ok) {
return Promise.reject("HTTP " + res.status + " " + res.statusText);
}
Log.log("e2 fetch : " + res.status + " " + res.headers.get("content-type"));
return res.text();
})
.then(text -> {
BookPageJson page = Js.uncheckedCast(Global.JSON.parse(text));
for (int i = 0; i < page.books.length; i++) {
BookJson b = page.books.getAt(i);
Log.log("e2 book : #" + (int) b.id + " " + b.title + " (" + b.year + ")");
}
return null;
});
}
Source: MDN — fetch()
e2 fetch : 200 application/json;charset=utf-8
e2 book : #1 The Pragmatic Programmer (1999)
e2 book : #2 Effective Java (2001)
e2 book : #3 Clean Code (2008)
The native types need no runtime support: name = "Object" says "treat this as a plain JavaScript object", the public fields are property accesses, and Js.uncheckedCast only tells the compiler the type.
The cast is not validated, so a response with a missing field yields undefined in Java; check untrusted data field by field before using it.
Two failure modes behave differently and both appear in the next listing.
A response with an error status (the servlet answers a POST with 405) resolves the promise with res.ok == false; only a network failure (connection refused, DNS, blocked by CORS) rejects it:
static Promise<Void> fetchFailures() {
RequestInit init = RequestInit.create();
init.setMethod("POST");
JsPropertyMap<String> headers = JsPropertyMap.of();
headers.set("Content-Type", "application/json");
init.setHeaders(headers);
init.setBody("{\"title\":\"Dune\"}");
return DomGlobal.fetch("/api/books", init)
.then(res -> {
Log.log("e2 POST : ok=" + res.ok + " status=" + res.status);
return DomGlobal.fetch("http://127.0.0.1:9/never");
})
.then(res -> null, err -> {
Log.log("e2 network err: " + err);
return null;
});
}
e2 POST : ok=false status=405
e2 network err: TypeError: Failed to fetch
Source: WHATWG Fetch Standard
Rules that keep fetch code safe and predictable:
-
Check
res.ok(orres.status) before using the body; do not assume a resolved promise means success. -
End a chain with
catch_so that a rejection reaches your code instead of the browser console as an unhandled rejection. -
credentialsdefaults tosame-origin, so session cookies are sent to the page’s own origin. For a state-changing request (POST, PUT, DELETE) send the XSRF token your server issued, as a header, exactly as XSRF, RPC & Server Security and REST & JSON Libraries describe; authorisation is checked on the server, never in the client. -
Use relative URLs or build them from
GWT.getModuleBaseURL(); cross-origin requests need CORS headers from the other server. -
fetchcan be cancelled with anAbortController(elemental2.dom.AbortController, passed throughRequestInit.setSignal).
Promise.all and typed arrays
Promise.all combines several promises. Its Java signature is static <V> Promise<V[]> all(IThenable<? extends V>... promises), and the array in the result is a JavaScript array.
Instantiated with V = String, GWT inserts a cast to String[] in the lambda and the call fails at run time; with V = Object it works and each element is converted with Js.cast:
static Promise<String> pageText(int start) {
return DomGlobal.fetch("/api/books?start=" + start + "&length=1").then(Response::text);
}
static Promise<Void> bothPages() {
return Promise.<Object>all(pageText(0), pageText(1)).then(results -> {
String first = Js.cast(results[0]);
Log.log("e2 Promise.all: " + results.length + " responses, first starts with " + first.substring(0, 24));
return null;
});
}
static Promise<Void> typedAll() {
return Promise.all(pageText(0), pageText(1)).then(texts -> {
Log.log("e2 typed all : " + texts.length);
return null;
}).catch_(err -> {
Log.log("e2 typed all : failed with " + err);
return null;
});
}
Source: MDN — Promise
e2 Promise.all: 2 responses, first starts with {"total":25,"books":[{"i
e2 typed all : failed with Error: java.lang.ClassCastException
JSON
Global.JSON (from elemental2-core) is the browser’s JSON object: parse(String) and stringify(Object).
Compared with the GWT-specific options it is the shortest path from text to a typed native object:
| API | Notes |
|---|---|
|
the browser’s strict parser; no |
|
|
|
|
|
|
AutoBeans, RequestFactory, REST libraries |
typed Java beans with generated serialisation; covered on their own pages |
String text = "{\"total\":2,\"books\":[{\"id\":1,\"title\":\"Dune\",\"author\":\"F. Herbert\",\"year\":1965,\"copies\":3}]}";
BookPageJson page = Js.uncheckedCast(Global.JSON.parse(text));
Log.log("e2 JSON.parse : total=" + page.total + " first=" + page.books.getAt(0).title);
Log.log("e2 stringify : " + Global.JSON.stringify(page.books.getAt(0)));
try {
Global.JSON.parse("{bad json");
} catch (JavaScriptException e) {
Log.log("e2 parse error: " + e.getMessage());
}
Source: MDN — JSON
e2 JSON.parse : total=2 first=Dune
e2 stringify : {"id":1,"title":"Dune","author":"F. Herbert","year":1965,"copies":3}
e2 parse error: (SyntaxError) : Expected property name or '}' in JSON at position 1 (line 1 column 2)
A JavaScript exception thrown from native code reaches Java as com.google.gwt.core.client.JavaScriptException; getMessage() carries the browser’s text, which differs between browsers.
The RequestBuilder, JSON & XML page covers the GWT-side JSONValue classes and JsonUtils in depth.
URL and URLSearchParams
URL and URLSearchParams build and parse addresses without string concatenation, which also gets the percent-encoding right:
URL url = new URL("/api/books", DomGlobal.location.href);
url.searchParams.set("start", "0");
url.searchParams.set("length", "3");
Log.log("e2 URL : " + url.pathname + url.search + " (search params: " + url.searchParams.get("length") + ")");
Source: MDN — URL
e2 URL : /api/books?start=0&length=3 (search params: 3)
url.href or url.toString() yields the absolute string for fetch, and fetch(URL) accepts the object directly.
URL.canParse(String) tests an address without exceptions, and URL.createObjectURL(Blob)/revokeObjectURL exist for downloads (revoke the object URL when done, it is a manual resource).
WebSocket
WebSocket is a native class with callback fields (onopen, onmessage, onclose, onerror) typed as @JsFunction interfaces, so lambdas work:
static void webSocket(String url) {
WebSocket ws = new WebSocket(url);
ws.onopen = e -> {
Log.log("e2 ws open : readyState=" + ws.readyState);
ws.send("{\"type\":\"ping\"}");
};
ws.onmessage = e -> {
Log.log("e2 ws message : " + e.data.asString());
ws.close();
};
ws.onclose = e -> Log.log("e2 ws close : code=" + e.code + " clean=" + e.wasClean);
ws.onerror = e -> Log.log("e2 ws error : " + e.type);
}
Source: MDN — WebSocket
The Bookshelf server has no WebSocket endpoint; the probe connected to a small echo server on the same origin as the host page:
e2 ws open : readyState=1
e2 ws message : echo: {"type":"ping"}
(The close callback reported code 1006 because that throwaway echo server closed the TCP connection without a complete closing handshake; a real server answers with 1000.)
e.data is a union type, so asString() is checked with isString() first when binary frames are possible.
Security rules for WebSockets are the same as for any endpoint: use wss:// on an HTTPS page (a secure page may not open an insecure WebSocket), authenticate and authorise on the server for every message, and verify the Origin header during the handshake so that another site cannot open an authenticated socket in the user’s browser.
Web storage
elemental2-webstorage adds the Storage interface and WebStorageWindow, a Window with the localStorage and sessionStorage fields.
GWT also has com.google.gwt.storage.client.Storage (getLocalStorageIfSupported()), which returns null where the feature is missing and is covered by the HTML5 Features page; the Elemental2 type assumes the browser has it.
Storage local = WebStorageWindow.of(DomGlobal.window).localStorage;
local.setItem("bookshelf.lastQuery", "dune");
Log.log("e2 storage : " + local.getItem("bookshelf.lastQuery") + ", length " + local.getLength()
+ ", missing -> " + local.getItem("nope"));
local.removeItem("bookshelf.lastQuery");
Source: MDN — Web Storage API
e2 storage : dune, length 1, missing -> null
getItem returns null for a missing key.
Everything in web storage is readable by any script on the origin, so a single XSS flaw exposes it: never store session tokens, passwords or personal data there.
Wrap reads and writes in try/catch; browsers throw when storage is disabled or the quota is exceeded.
Elemental (1): deprecated
The first Elemental was announced in the article "Introducing Elemental" (Ray Cromwell, updated June 2012). It generated Java from the WebKit IDL files, covered DOM, WebGL, WebAudio, WebSockets and more, included its own collections and JSON library, and was built purely from overlay types for speed. The article is honest about its limits, which are the reasons it was replaced: the generated code had no unit tests, it was generated from WebKit IDL and so contained vendor-prefixed extensions, and its native collections were not interoperable with Java collections.
The facts as of 2026-10-08:
| Question | Answer |
|---|---|
When was it deprecated? |
GWT 2.9.0: "Elemental has been officially deprecated - it is still included in this release, but may not appear in future releases", with the recommendation to use the Elemental2 libraries, which are compatible with both GWT 2 and J2CL |
Where is it? |
|
Does the article still apply? |
as history only; it lists the first release as "experimental" (GWT 2.5 release notes) and its code ( |
The migration is mechanical for the DOM and event parts and manual for the rest:
| Elemental (1) | Elemental2 |
|---|---|
|
|
|
the constructors themselves ( |
generated overlay types and interfaces |
native |
Elemental’s own native collections, which did not implement the |
|
the |
|
vendor-prefixed WebKit APIs |
the standard names, because Elemental2 follows the Closure externs, which track the standards |
If a library you depend on still imports elemental.*, it needs the old gwt-elemental 2.9.0 artifact on the classpath; plan its replacement as part of the migration described in What’s Changed & Migration (Elemental recipe); the same page lists the outdated material row by row.
The org.gwtproject.dom:gwt-dom module (1.0.0-RC2, 2022-04-14) is a further, J2CL-ready rewrite of com.google.gwt.dom.client; it is a release candidate, not a stable library, and J2CL & the Future explains where it fits.
Memory leaks and handler lifecycles
The Developer Guide-era article "DOM Events, Memory Leaks, and You" (Joel Webber, updated January 2009) explains why GWT widgets cannot attach listeners to arbitrary elements and use bit fields for events.
It opens with a note that, with the death of Internet Explorer, the information is obsolete, though it still explains the event handling mechanism of the Widget class.
Both statements are right, and the second is worth checking against the 2.13.1 source.
What the article describes and what the code does today
The leak in question is a reference cycle between a JavaScript object and a native DOM element: widget to element to a closure that points back to widget.
Old Internet Explorer’s garbage collectors could not free such cycles, and the common workaround (walking the DOM at window.onunload) missed detached elements and long-running applications.
GWT’s rule was therefore: a widget must not be in a cycle with a native element while it is detached.
Each widget has one root element; when the widget is attached, exactly one back reference from the element to the widget is created and, when it is detached, removed.
The code of 2.13.1 still follows this design.
DOMImpl.setEventListener stores the listener in an expando property named __listener on the element, Widget.onAttach calls it with the widget, and Widget.onDetach calls it with null inside a finally block.
sinkEvents assigns one shared dispatcher function to the element (elem.onclick = dispatchEvent, and likewise for the other events) instead of a closure per widget, which is why the article says you have to sink events with bits.
The dispatcher walks up from the event target to the first ancestor that has a __listener, so an event on a child element is delivered to the nearest widget root.
The probe checks the expando through jsinterop-base:
static Object listenerOf(Widget w) {
return Js.asPropertyMap(w.getElement()).get("__listener");
}
before attach : __listener on tidy element = false
after attach : __listener on tidy element = true
after detach : __listener on tidy element = false
Source: GWT 2.13.1 — DOMImpl.java
The consequence the article states, and the code confirms, is a design limit: a widget cannot hang listeners on elements inside itself. It receives the event and works out which child it came from, with Element.as(event.getEventTarget()) and the DOM tree (getParentElement, isOrHasChild), exactly as ClickCounter.onBrowserEvent did above.
Cell widgets rely on the same delegation: they receive the event at their root element and work out which cell it belongs to.
What the old guarantee does not cover is everything that is not the widget’s own root-element listener.
Those registrations are held by long-lived objects (the window, the event preview stack, a static event bus, a running timer) which keep your widget reachable through the handler’s closure. A modern engine’s collector handles cycles, so this is no longer a cycle problem; it is a lifetime problem: the widget is removed from the page but can never be collected, and its handler keeps running.
The attach and detach lifecycle
Widget exposes the lifecycle through four methods and one event, called in a fixed order:
| Step | What happens |
|---|---|
|
sets the attached flag, installs the |
|
the widget and its children are fully attached; the right place to register global handlers, start timers and add Elemental2 listeners |
|
called first when detaching, while everything is still attached; the right place to undo what |
|
calls |
addAttachHandler(Handler) is the non-subclassing form: it receives an AttachEvent on both transitions.
Subclasses that override onAttach or onDetach must call super; with assertions enabled GWT checks it when the parent attaches or detaches the child (the assertion message starts with "Failure of"), and a missing super.onDetach() leaves the expando in place, which is the leak the Javadoc of onDetach warns about.
Calling the methods in the wrong state throws:
-
Should only call onAttach when the widget is detached from the browser's document -
Should only call onDetach when the widget is attached to the browser's document -
Cannot set a new parent without first clearing the old parent, when a widget that already has a parent is added to a second panel
The Composite and Panel classes manage the lifecycle of their children, and Widget.removeFromParent() is the call that detaches a widget.
The older safety net, RootPanel.detachOnWindowClose(Widget) (a list of widgets to detach when the page unloads), is @Deprecated in 2.13.1: its Javadoc says it was introduced to combat memory leaks in old browsers, that it is no longer necessary, that the unload event it relies on is being removed from browsers and that it is unreliable for guaranteeing onUnload() calls (issue 9908).
So is Window.addCloseHandler, for the same reason; the 2.12.0 release notes record that unload-based calls were deprecated.
Registrations that outlive the widget
The probe module creates two widgets for each kind of global handler: a leaky one that registers in its constructor and never unregisters, and a tidy one that registers in onLoad and unregisters in onUnload.
First the GWT native preview handler:
/** Registers a global handler in the constructor and never removes it. */
static class LeakyBadge extends Widget {
LeakyBadge() {
setElement(Document.get().createDivElement());
Event.addNativePreviewHandler(p -> leakyHits++);
}
}
/** Registers in onLoad and removes in onUnload, so the registration follows the attach state. */
static class TidyBadge extends Widget {
private HandlerRegistration preview;
TidyBadge() {
setElement(Document.get().createDivElement());
}
@Override
protected void onLoad() {
preview = Event.addNativePreviewHandler(p -> tidyHits++);
}
@Override
protected void onUnload() {
if (preview != null) {
preview.removeHandler();
preview = null;
}
}
}
Then an Elemental2 listener on the window, with the same two shapes (the tidy one keeps its EventListener in a field so the same instance is removed):
static class ResizeAware extends Widget {
private final boolean tidy;
private final EventListener listener;
ResizeAware(boolean tidy) {
this.tidy = tidy;
setElement(Document.get().createDivElement());
listener = e -> { if (tidy) { tidyResize++; } else { leakyResize++; } };
if (!tidy) {
DomGlobal.window.addEventListener("resize", listener); // never removed
}
}
@Override
protected void onLoad() {
if (tidy) {
DomGlobal.window.addEventListener("resize", listener);
}
}
@Override
protected void onUnload() {
if (tidy) {
DomGlobal.window.removeEventListener("resize", listener);
}
}
}
The driver attaches both widgets, dispatches one event, detaches both, dispatches two more events and prints the counters, first for a click (picked up by the native preview handlers) and then for a window resize:
attach event : leaky attached=true
attach event : tidy attached=true
click 1 (both attached): leaky=1 tidy=1
attach event : leaky attached=false
attach event : tidy attached=false
click 3 (both removed) : leaky=3 tidy=1
resize 1 (attached) : leaky=1 tidy=1
resize 3 (removed) : leaky=3 tidy=1
After the widgets were removed from the page, the leaky handlers still ran for every later event (3 in total against 1), and each of them keeps its widget and the widget’s element tree alive. That is the whole story of a GWT memory leak in 2026, and the same pattern applies to every long-lived source:
| Registration | How to undo it |
|---|---|
|
keep the returned |
|
keep the |
handlers added to a static or application-wide event bus |
keep each |
Elemental2 |
keep the |
|
call |
|
|
pending |
pass an |
Handlers added with widget.addClickHandler(...) or addDomHandler are different: they are stored in the widget’s own HandlerManager, so they live and die with the widget and need no cleanup.
ResettableEventBus is the right tool when a presenter registers many handlers on a shared bus; the probe registers one handler, fires two events, calls removeHandlers() after the first, and the second event finds nothing:
EventBus app = new SimpleEventBus();
ResettableEventBus scoped = new ResettableEventBus(app);
int[] seen = {0};
scoped.addHandler(PingEvent.TYPE, e -> seen[0]++);
app.fireEvent(new PingEvent());
scoped.removeHandlers();
app.fireEvent(new PingEvent());
Log.log("ResettableEventBus: handled " + seen[0] + " of 2 pings");
ResettableEventBus: handled 1 of 2 pings
Source: Javadoc — ResettableEventBus
The MVP, Activities & Places page applies the same idea: an Activity gets an EventBus that is reset when the activity stops, so handlers registered through it disappear with the screen.
Bookshelf: a DOM and Elemental2 probe
The examples above ran in one throw-away module next to the Bookshelf client; nothing in the three Bookshelf modules was changed except the Elemental2 dependencies and the four <inherits> lines in the client POM and module.
The probe has a module file, a host page and eight small classes:
demo/com/example/bookshelf/DomDemo.gwt.xml
demo/com/example/bookshelf/domdemo/DomDemo.java entry point, runs every step
demo/com/example/bookshelf/domdemo/DomClientDemo.java dom.client card, styles, lookups
demo/com/example/bookshelf/domdemo/EventDemo.java ClickCounter, native preview
demo/com/example/bookshelf/domdemo/LifecycleDemo.java leaky and tidy handlers, ResettableEventBus
demo/com/example/bookshelf/domdemo/Elemental2Demo.java DOM, fetch, Promise, URL, JSON, WebSocket, storage
demo/com/example/bookshelf/domdemo/BookJson.java native shape of one book
demo/com/example/bookshelf/domdemo/BookPageJson.java native shape of the response
demo/com/example/bookshelf/domdemo/PingEvent.java event for the bus example
war/index.html host page with a pre element for the log
<module rename-to="domdemo">
<inherits name="com.google.gwt.user.User"/>
<inherits name="elemental2.dom.Dom"/>
<inherits name="elemental2.core.Core"/>
<inherits name="elemental2.promise.Promise"/>
<inherits name="elemental2.webstorage.WebStorage"/>
<source path="domdemo"/>
<entry-point class="com.example.bookshelf.domdemo.DomDemo"/>
</module>
<!doctype html>
<html>
<head><meta charset="utf-8"><title>DOM demo</title></head>
<body>
<pre id="log"></pre>
<script src="domdemo/domdemo.nocache.js"></script>
</body>
</html>
The classes log through a four-line helper that appends text (never markup) to the pre element:
static void log(String line) {
Element pre = Document.get().getElementById("log");
pre.setInnerText(pre.getInnerText() + line + "\n");
}
The module was compiled with the classpath that Maven resolves for the client module (so the versions are exactly the ones from the POM), served from a small static server that forwards /api/ to the Bookshelf server module running in Jetty 12, and loaded in headless Chrome 155, whose DevTools protocol was used to read the pre element after four seconds:
mvn -B dependency:build-classpath -Dmdep.outputFile=cp.txt -pl bookshelf-client -am
java -cp "demo:$(cat cp.txt)" com.google.gwt.dev.Compiler \
-sourceLevel 17 -strict -war war -workDir wk com.example.bookshelf.DomDemo
The first compile took about seven seconds for two permutations and produced two .cache.js files of about 61 KB each, which includes the Elemental2 types the probe actually uses (the compiler removes the rest of the 630 source files in elemental2-dom).
That is the practical difference from a hand-written wrapper: unused Elemental2 API costs nothing, because native types have no code.
Security notes
| Risk | Rule |
|---|---|
DOM-based XSS through |
user and server data goes through |
Unvalidated JSON |
|
Unsafe parsers |
use |
State-changing |
send the XSRF token header; enforce authorisation on the server; never rely on the client hiding a button |
WebSocket |
|
Web storage |
any script of the origin can read it; no tokens, passwords or personal data |
Cross-origin requests |
the server decides with CORS headers; the client cannot grant itself access |
The SafeHtml & XSS and XSRF, RPC & Server Security pages show the escaping and token machinery in full.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
The Maven dependency is present but the GWT module is not inherited |
Add |
|
|
|
|
|
|
Handlers added with |
|
Call |
|
The bit of that type was not sunk, or the widget is not attached yet (sinking is deferred until |
Add the bit to |
|
|
Never call |
A handler keeps firing after the widget was removed |
Registration made in the constructor or in |
Keep the |
|
A new lambda instance was passed; listeners are compared by identity |
Keep the |
|
|
Use |
|
Network failure, a blocked cross-origin request or mixed content; the promise rejects |
Handle it in |
|
|
Catch |
Two types called |
Both |
Import one, fully qualify the other |
|
The server sent a different shape and nothing checks it |
Validate field by field, or deserialise with AutoBeans or a typed REST client |
Build resolves |
The GWT BOM manages the annotation version |
Accept it (it compiled and ran), or override the managed version deliberately and test |
Source: GWT — FAQ: Troubleshooting
Where older material is outdated
The first-edition book and the 2009 article describe a world in which the DOM layer was a listener-based API on top of Internet Explorer 6 to 8.
| Older statement | Today |
|---|---|
Events are handled by |
The classes are still in |
Static |
Most are |
|
|
The browser matrix is IE, Firefox, Safari, Opera and the compiler produces a file per browser |
Two permutations only ( |
Cross-browser event differences must be understood by the developer |
|
Memory leaks come from JavaScript-to-DOM reference cycles in Internet Explorer, avoided by the single |
The mechanism is still in the source and still protects widget root elements, but the Javadoc of |
Widgets without a parent must be registered with |
Deprecated; call |
Elemental is the future of GWT browser access |
deprecated in 2.9.0; Elemental2 replaces it and is shared with J2CL |
Source: Manning — GWT in Action
The Developer Guide pages on the DOM and on browser support, and the articles on Elemental and on DOM memory leaks, are otherwise accurate descriptions of the code but predate Java 8, JsInterop and Elemental2: their code samples use anonymous classes, JSNI and the listener-era names, and the Elemental article is a historical document.
What changed recently
-
2.8.0: JsInterop and Java 8 syntax, which made native types such as Elemental2 usable from GWT.
-
2.9.0: compiles with
jsinterop-base1.0.0, Elemental2 1.0.0 andjsinterop-annotations2.0.0; Elemental (1) is deprecated. -
2.12.0 (also): source maps on by default in all browsers, which makes the Elemental2 and
dom.clientcode debuggable as Java (see Development Mode & Debugging). -
2.12.0: the browser
unloadevent is deprecated, so GWT deprecated the calls that require it (Window.addCloseHandler,RootPanel.detachOnWindowClose) and replaced themousewheelevent bywheelinternally, transparently for applications. -
2.13.0 and 2.13.1: the release notes list no change to the
dom.client,Eventoruser.agentAPI; the property still has the two valuesgecko1_8andsafari. -
Elemental2: 1.3.0 (2025-08-12), 1.3.1 (2025-08-20), 1.3.2 (2025-09-15, latest stable), 1.4.0-RC1 (2026-07-07, pre-release). The repository was last pushed on 2026-09-27 and is Apache-2.0 licensed.
Source: Elemental2 — Releases