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.

Table 1. The DOM-facing APIs available in GWT 2.13.1
Layer Where it lives State in 2.13.1 Use it for

Widgets and panels

com.google.gwt.user.client.ui (gwt-user)

current

everything with a ready-made widget; reach the element with getElement() only when needed

com.google.gwt.dom.client

gwt-user, module com.google.gwt.dom.DOM, inherited by com.google.gwt.user.User

current and stable; typed JavaScript overlay types

creating and changing elements, styles and nodes inside your own widgets

DOM, Event, EventListener and the old Element

com.google.gwt.user.client (gwt-user), module com.google.gwt.user.DOM

Event, sinkEvents and onBrowserEvent are the live widget event mechanism; the old Element class and most static DOM helpers are @Deprecated

sinking events in a custom widget, native event preview

JSNI

/*-{ ... }-*/ methods

legacy

existing code only; see JSNI & Overlay Types

Elemental2

com.google.elemental2:elemental2-*, generated from the Closure Compiler externs

maintained; 1.3.2 stable, 1.4.0-RC1 published 2026-07-07

fetch, Promise, JSON, URL, WebSocket, web storage, querySelector, addEventListener, everything a browser offers that dom.client lacks

Elemental (1)

com.google.gwt:gwt-elemental, last release 2.9.0 (2021-02-12)

deprecated since GWT 2.9, not in the 2.13.1 source tree

nothing new; migrate to Elemental2

flowchart TD start["I need to touch the DOM"] --> q1{"Is there a widget or a UiBinder element for it?"} q1 -- "yes" --> w["Use the widget and read getElement() only if needed"] q1 -- "no" --> q2{"Is it element, style, node or event work inside a widget?"} q2 -- "yes" --> d["com.google.gwt.dom.client with Event.sinkEvents and onBrowserEvent"] q2 -- "no" --> q3{"Is it a Web API: fetch, WebSocket, URL, storage, Promise, JSON?"} q3 -- "yes" --> e["Elemental2 (com.google.elemental2)"] q3 -- "no" --> j["Bind the library with a JsInterop native type"]

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:

  • Node has the tree operations: appendChild, insertBefore, insertFirst, removeChild, removeFromParent, replaceChild, cloneNode, getFirstChild, getParentElement, getChildCount, isOrHasChild, getOwnerDocument

  • Element adds identity and presentation: getId/setId, getClassName/setClassName, addClassName, removeClassName, hasClassName, toggleClassName, attributes (getAttribute, setAttribute, hasAttribute, removeAttribute), properties (getPropertyString, setPropertyString, getPropertyInt and friends), geometry (getOffsetWidth, getClientHeight, getAbsoluteLeft, getScrollTop), getStyle, setInnerText, setInnerHTML, setInnerSafeHtml, focus, blur, scrollIntoView, dispatchEvent

  • Document is the factory and the entry point: Document.get(), getBody, getHead, getElementById, getElementsByTagName, getTitle, getURL, getReferrer, createUniqueId, isCSS1Compat, and one createXxxElement() method per HTML tag, plus createClickEvent, createKeyDownEvent and the other synthetic-event factories

  • Style is the inline style attribute with typed setters, Style.Unit (PX, EM, PCT and the rest), the enums Style.Display, Style.Cursor, Style.Float, Style.FontWeight and others, and the escape hatch setProperty(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 &lt;script&gt;alert(1)&lt;/script&gt;</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

Points worth knowing before you write more:

  • Narrowing is explicit. Document.getElementById returns Element. Use the static as of the specific class (DivElement.as(found)) when you know the tag, Element.is(JavaScriptObject) to test whether an object is an element at all, and Element.as(JavaScriptObject) or the generic cast() of JavaScriptObject to go from an event target to an element. Because overlay types carry no runtime type, a wrong as does not fail at once: it fails later, when you call a method the real object lacks.

  • Style is the inline style only. card.getStyle().getWidth() returned 12em, the inline value (the generated code assigns 12.0em, which the browser normalises), not the computed pixel width (getOffsetWidth() returned 196). Computed styles are not part of dom.client; prefer CSS classes (setStyleName, addClassName) and the ClientBundle/GSS pages of this section for presentation, as the Developer Guide recommends.

  • Property names for setProperty are the camel-case DOM names (borderLeft, not border-left); the string is assigned to element.style[name].

  • Text and markup are different methods. setInnerText escapes; setInnerHTML is annotated @IsSafeHtml and is meant to receive a constant or a SafeHtml value, and setInnerSafeHtml(SafeHtml) takes the type directly. Never concatenate user data into setInnerHTML; the SafeHtml and XSS page of this section covers the builders and sanitisers.

  • getElement() still returns the old com.google.gwt.user.client.Element, a deprecated subclass of dom.client.Element. Assign it to a com.google.gwt.dom.client.Element variable 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

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:

Table 2. Event bits of com.google.gwt.user.client.Event (2.13.1)
Constant Value Constant Value

ONCLICK

0x00001

ONFOCUS

0x00800

ONDBLCLICK

0x00002

ONBLUR

0x01000

ONMOUSEDOWN

0x00004

ONLOSECAPTURE

0x02000

ONMOUSEUP

0x00008

ONSCROLL

0x04000

ONMOUSEOVER

0x00010

ONLOAD

0x08000

ONMOUSEOUT

0x00020

ONERROR

0x10000

ONMOUSEMOVE

0x00040

ONMOUSEWHEEL

0x20000

ONKEYDOWN

0x00080

ONCONTEXTMENU

0x40000

ONKEYPRESS

0x00100

ONPASTE

0x80000

ONKEYUP

0x00200

ONTOUCHSTART to ONTOUCHCANCEL

0x100000 to 0x800000

ONCHANGE

0x00400

ONGESTURESTART to ONGESTUREEND

0x1000000 to 0x4000000

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);
  }
}

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:

Table 3. Developer Guide DOM examples and their 2.13 form
Developer Guide example Today

htmlWidget.getElement().getStyle().setBackgroundColor("#ffee80")

unchanged; a CSS class with addStyleName is usually better

JSNI helper that loops over getElementsByTagName("a") and copies ids into an ArrayList

Element.getElementsByTagName("a") returns a NodeList<Element> that you can index in Java; no JSNI is needed

Document.get().getElementById(id) plus getPropertyString("href")

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"/>

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

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.client wraps 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.isTruthy on a global, or a try around the call) rather than the user-agent string.

  • The webApiUsage property. stable or modern (default modern): with modern some widgets may use new, not yet standardised APIs. Choose stable if redeploying whenever a browser changes would be a problem. The property and the user.agent.runtimeWarning configuration property are defined in the same module as above.

  • Mismatch warnings. The module comment describes user.agent.runtimeWarning as the switch for warnings about a compile-time user.agent that differs from the runtime one; it is true by default. This happens when a proxy serves one browser’s file to another, or when -setProperty user.agent=gecko1_8 is 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.

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.

Table 4. Elemental2 modules (1.3.2)
Artifact GWT module to inherit Contents

elemental2-core

elemental2.core.Core

the JavaScript language objects: Global (with Global.JSON), JsArray, JsObject, JsString, JsNumber, JsDate, ArrayBuffer, typed arrays, JsMap, JsSet, JsRegExp

elemental2-promise

elemental2.promise.Promise

Promise, IThenable

elemental2-dom

elemental2.dom.Dom

DomGlobal (window, document, console, navigator, location, fetch, timers), Document, HTMLElement and the other HTML element classes, Event types, Response, RequestInit, Headers, URL, URLSearchParams, WebSocket, Blob, FormData, AbortController and the rest of the Web API

elemental2-webstorage

elemental2.webstorage.WebStorage

Storage, StorageEvent, WebStorageWindow (the Window with localStorage and sessionStorage fields)

elemental2-indexeddb, -media, -svg, -webgl

elemental2.indexeddb.IndexedDb, elemental2.media.Media, elemental2.svg.Svg, elemental2.webgl.WebGl

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" />

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:

  • createElement returns HTMLElement; cast it to the specific class (HTMLDivElement, HTMLInputElement, HTMLButtonElement) to reach type, 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.target is an EventTarget; convert with Js.cast. Event subtypes (KeyboardEvent, MouseEvent) are obtained the same way, and constructors exist for synthetic events: new KeyboardEvent("keydown", init) with KeyboardEventInit.create() and init.setKey("Enter") produced the third log line.

  • Removal needs the same instance. removeEventListener compares 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.

  • innerHTML is a plain String field. Elemental2 has no SafeHtml type, so element.innerHTML = userText is a DOM-based XSS hole. Assign textContent for text, or build markup with GWT’s SafeHtml builders (see SafeHtml & XSS) and pass the asString() of a SafeHtml only.

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;
      });
}
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

Rules that keep fetch code safe and predictable:

  • Check res.ok (or res.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.

  • credentials defaults to same-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.

  • fetch can be cancelled with an AbortController (elemental2.dom.AbortController, passed through RequestInit.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;
  });
}
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:

Table 5. JSON options in GWT 2.13.1
API Notes

Global.JSON.parse / stringify (Elemental2)

the browser’s strict parser; no eval; throws a JavaScript SyntaxError on invalid input; pairs with native @JsType shapes

JsonUtils.safeEval, JsonUtils.unsafeEval, JsonUtils.stringify

com.google.gwt.core.client.JsonUtils; the eval variants are for trusted input only

JSONParser.parseStrict

com.google.gwt.json.client; builds the JSONValue tree and uses the browser’s JSON.parse

JSONParser.parse, parseLenient

@Deprecated; they call eval and the Javadoc says never to pass an untrusted string

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());
}
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);
}

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");
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:

Table 6. Elemental (1) status
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?

com.google.gwt:gwt-elemental, versions 2.5.0-rc1 to 2.9.0 on Maven Central (last update 2021-02-12). There is no org.gwtproject:gwt-elemental, and the 2.13.1 source tree contains no Elemental directory

Does the article still apply?

as history only; it lists the first release as "experimental" (GWT 2.5 release notes) and its code (Browser.getWindow(), window.newAudioContext()) does not compile against 2.13

The migration is mechanical for the DOM and event parts and manual for the rest:

Table 7. Elemental (1) concepts and their Elemental2 equivalents
Elemental (1) Elemental2

Browser.getWindow() and the Window/Document interfaces

DomGlobal.window and DomGlobal.document (static fields)

window.newXxx() factory methods for JavaScript constructors, Document.createXxxElement()

the constructors themselves (new WebSocket(url), new URL(...)) and createElement("tag") cast to the specific HTMLXxxElement

generated overlay types and interfaces

native @JsType classes in elemental2.dom

Elemental’s own native collections, which did not implement the java.util interfaces

JsArray, JsMap, JsSet in elemental2-core, and jsinterop.base.Js helpers to convert; they still do not implement java.util interfaces

the elemental.json library (JsonValue and friends)

Global.JSON plus native types, or one of the options in the JSON table above

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
sequenceDiagram participant P as Panel or RootPanel participant W as Widget participant E as Root element participant G as Global registrations P->>W: add(widget) W->>E: onAttach: set listener expando, sink events W->>W: onLoad, then AttachEvent(true) Note over G: you register here: native preview, Window, Elemental2, Timer, event bus P->>W: remove(widget) W->>W: onUnload, then AttachEvent(false) W->>E: onDetach: clear listener expando Note over G: you must unregister here

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:

Table 8. The widget lifecycle in GWT 2.13.1
Step What happens

onAttach()

sets the attached flag, installs the __listener expando, applies the pending sinkEvents bits, calls doAttachChildren(), then onLoad(), then fires AttachEvent with isAttached() == true. Override onLoad, not onAttach

onLoad()

the widget and its children are fully attached; the right place to register global handlers, start timers and add Elemental2 listeners

onUnload()

called first when detaching, while everything is still attached; the right place to undo what onLoad did

onDetach()

calls onUnload(), fires AttachEvent with isAttached() == false, detaches children, clears the expando and the attached flag (the last two in finally blocks, so an exception in your onUnload does not leave the widget half-attached)

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:

Table 9. What to register in onLoad and unregister in onUnload
Registration How to undo it

Event.addNativePreviewHandler

keep the returned HandlerRegistration, call removeHandler()

Window.addResizeHandler, Window.addWindowScrollHandler

keep the HandlerRegistration, call removeHandler()

handlers added to a static or application-wide event bus

keep each HandlerRegistration, or register through a ResettableEventBus and call removeHandlers() once

Elemental2 addEventListener on window, document or another long-lived element

keep the EventListener instance and call removeEventListener with it

Timer with scheduleRepeating, or a RepeatingCommand given to the Scheduler

call cancel() on the timer; a RepeatingCommand stops when it returns false (see Delayed Logic & Scheduler)

WebSocket, EventSource, IntersectionObserver, ResizeObserver

close() or disconnect()

pending fetch whose callback touches the widget

pass an AbortSignal, or check isAttached() in the callback before using the widget

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

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

Table 10. Security rules for DOM and Elemental2 code
Risk Rule

DOM-based XSS through innerHTML, setInnerHTML, outerHTML

user and server data goes through setInnerText or textContent; markup comes from constants or SafeHtml builders. Elemental2’s innerHTML is an unchecked String

Unvalidated JSON

Js.uncheckedCast does not check shapes. Validate types and ranges before use; do not feed parsed values into innerHTML, URLs or eval

Unsafe parsers

use Global.JSON.parse or JSONParser.parseStrict; never JSONParser.parse, parseLenient or JsonUtils.unsafeEval on data from a network

State-changing fetch calls

send the XSRF token header; enforce authorisation on the server; never rely on the client hiding a button

WebSocket

wss:// only, check Origin during the handshake, authorise every message on the server

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

Table 11. Common problems with the DOM, events and Elemental2
Symptom Cause Fix

No source code is available for type elemental2.dom.DomGlobal; did you forget to inherit a required module?

The Maven dependency is present but the GWT module is not inherited

Add <inherits name="elemental2.dom.Dom" /> (and Core, Promise, WebStorage for the other artifacts) to the module XML

The method addEventListener(String, null) is undefined for the type BodyElement

dom.client has no addEventListener (the EventTarget class omits it on purpose)

sinkEvents plus onBrowserEvent inside a widget, or an Elemental2 type via Js.cast

The method querySelector(String) is undefined for the type BodyElement

dom.client has no selector API

Js.cast the element to elemental2.dom.Element or HTMLElement and call querySelector

Handlers added with addClickHandler stop working in a custom widget

onBrowserEvent was overridden without calling super.onBrowserEvent(event)

Call super last, as ClickCounter does

onBrowserEvent is never called for one event type

The bit of that type was not sunk, or the widget is not attached yet (sinking is deferred until onAttach)

Add the bit to sinkEvents (or register the handler with addDomHandler, which sinks for you) and add the widget to a panel

Should only call onAttach when the widget is detached from the browser's document

onAttach called twice, usually by hand or by an override that does not call super properly

Never call onAttach/onDetach yourself; override onLoad/onUnload

A handler keeps firing after the widget was removed

Registration made in the constructor or in onLoad and never undone

Keep the HandlerRegistration and call removeHandler() in onUnload

removeEventListener seems to do nothing

A new lambda instance was passed; listeners are compared by identity

Keep the EventListener in a field and remove that instance

java.lang.ClassCastException from a Promise.all(...).then(...) lambda

V[] is instantiated as String[], but the result is a JavaScript array

Use Promise.<Object>all(...) and Js.cast each element

TypeError: Failed to fetch

Network failure, a blocked cross-origin request or mixed content; the promise rejects

Handle it in catch_; an HTTP error such as 405 does not reject, check res.ok

(SyntaxError) : Expected property name or '}' in JSON at position 1 (line 1 column 2)

JSON.parse of invalid text; the wording differs by browser

Catch JavaScriptException; validate the response content type first

Two types called Element, Event or Storage in one file

Both com.google.gwt... and elemental2... imported

Import one, fully qualify the other

Js.uncheckedCast result has undefined fields

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 jsinterop-annotations 2.0.0 while the Elemental2 POMs say 2.0.2

The GWT BOM manages the annotation version

Accept it (it compiled and ran), or override the managed version deliberately and test

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.

Table 12. Outdated statements about the DOM and events
Older statement Today

Events are handled by ClickListener, ChangeListener, KeyboardListener and the other listener interfaces, plus EventPreview

The classes are still in gwt-user 2.13.1 but @Deprecated; use handlers (addClickHandler, addDomHandler) and Event.addNativePreviewHandler

Static DOM.xxx methods are how you create and change elements (DOM.createDiv, DOM.setStyleAttribute, DOM.eventGetType)

Most are @Deprecated; use Document factories, Element and Style, NativeEvent getters

com.google.gwt.user.client.Element is the element type

@Deprecated; dom.client.Element is the type, and getElement() is assignable to it

The browser matrix is IE, Firefox, Safari, Opera and the compiler produces a file per browser

Two permutations only (gecko1_8 and safari); Chromium and Edge use the safari one

Cross-browser event differences must be understood by the developer

DOMImplWebkit and DOMImplMozilla absorb the ones that remain; the widget and handler layers hide the rest

Memory leaks come from JavaScript-to-DOM reference cycles in Internet Explorer, avoided by the single __listener back reference

The mechanism is still in the source and still protects widget root elements, but the Javadoc of RootPanel.detachOnWindowClose says modern browsers do not have the leaks that required that extra safety net; the leaks you can cause today come from global registrations that are never removed

Widgets without a parent must be registered with RootPanel.detachOnWindowClose or they leak

Deprecated; call removeFromParent() when a widget is no longer needed

Elemental is the future of GWT browser access

deprecated in 2.9.0; Elemental2 replaces it and is shared with J2CL

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-base 1.0.0, Elemental2 1.0.0 and jsinterop-annotations 2.0.0; Elemental (1) is deprecated.

  • 2.12.0 (also): source maps on by default in all browsers, which makes the Elemental2 and dom.client code debuggable as Java (see Development Mode & Debugging).

  • 2.12.0: the browser unload event is deprecated, so GWT deprecated the calls that require it (Window.addCloseHandler, RootPanel.detachOnWindowClose) and replaced the mousewheel event by wheel internally, transparently for applications.

  • 2.13.0 and 2.13.1: the release notes list no change to the dom.client, Event or user.agent API; the property still has the two values gecko1_8 and safari.

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